diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 4e764bc6351214f948ebd1d5c026cd81a908e22c..29a4c2d3d6a4bebb52e96665a7f072f4eff99847 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -476,6 +476,9 @@ state: "状態" sort: "ソート" ascendingOrder: "æ˜‡é †" descendingOrder: "é™é †" +scratchpad: "スクラッãƒãƒ‘ッド" +scratchpadDescription: "スクラッãƒãƒ‘ッドã¯ã€AiScriptã®å®Ÿé¨“環境をæä¾›ã—ã¾ã™ã€‚Misskeyã¨å¯¾è©±ã™ã‚‹ã‚³ãƒ¼ãƒ‰ã®è¨˜è¿°ã€å®Ÿè¡Œã€çµæžœã®ç¢ºèªãŒã§ãã¾ã™ã€‚" +output: "出力" _theme: explore: "テーマを探ã™" diff --git a/package.json b/package.json index 0214f8f3a93c0dfde2d723297e8a5d71440de994..2c9e570b1586f09421b2b3aff7b53e3a70fcf1c1 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "@koa/cors": "3.0.0", "@koa/multer": "2.0.2", "@koa/router": "8.0.8", - "@syuilo/aiscript": "0.0.0", + "@syuilo/aiscript": "0.0.2", "@types/bcryptjs": "2.4.2", "@types/bull": "3.12.1", "@types/cbor": "5.0.0", @@ -250,6 +250,7 @@ "vue-marquee-text-component": "1.1.1", "vue-meta": "2.3.3", "vue-prism-component": "1.1.1", + "vue-prism-editor": "0.5.1", "vue-router": "3.1.6", "vue-style-loader": "4.1.2", "vue-svg-inline-loader": "1.5.0", diff --git a/src/client/app.vue b/src/client/app.vue index 5523e1e758b5dc1e2be4073f7e21615cd97c7ffd..f3f99fe282fdc5180770d94bae5cfee7f871c285 100644 --- a/src/client/app.vue +++ b/src/client/app.vue @@ -156,7 +156,7 @@ <script lang="ts"> import Vue from 'vue'; -import { faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; +import { faTerminal, faGripVertical, faChevronLeft, faHashtag, faBroadcastTower, faFireAlt, faEllipsisH, faPencilAlt, faBars, faTimes, faSearch, faUserCog, faCog, faUser, faHome, faStar, faCircle, faAt, faListUl, faPlus, faUserClock, faUsers, faTachometerAlt, faExchangeAlt, faGlobe, faChartBar, faCloud, faGamepad, faServer, faFileAlt, faSatellite, faInfoCircle, faQuestionCircle } from '@fortawesome/free-solid-svg-icons'; import { faBell, faEnvelope, faLaugh, faComments } from '@fortawesome/free-regular-svg-icons'; import { ResizeObserver } from '@juggle/resize-observer'; import { v4 as uuid } from 'uuid'; @@ -470,6 +470,11 @@ export default Vue.extend({ to: '/games', icon: faGamepad, }, null] : []), { + type: 'link', + text: this.$t('scratchpad'), + to: '/scratchpad', + icon: faTerminal, + }, null, { type: 'link', text: this.$t('help'), to: '/docs', diff --git a/src/client/pages/scratchpad.vue b/src/client/pages/scratchpad.vue new file mode 100644 index 0000000000000000000000000000000000000000..2178e35b9bd3e05e4b4aec5e5546a6211e30180f --- /dev/null +++ b/src/client/pages/scratchpad.vue @@ -0,0 +1,154 @@ +<template> +<div class=""> + <portal to="icon"><fa :icon="faTerminal"/></portal> + <portal to="title">{{ $t('scratchpad') }}</portal> + + <div class="_panel"> + <prism-editor v-model="code" :line-numbers="false" language="js"/> + <mk-button style="position: absolute; top: 8px; right: 8px;" @click="run()" primary><fa :icon="faPlay"/></mk-button> + </div> + + <mk-container :body-togglable="true"> + <template #header><fa fixed-width/>{{ $t('output') }}</template> + <div class="bepmlvbi"> + <div v-for="log in logs" class="log" :key="log.id" :class="{ print: log.print }">{{ log.text }}</div> + </div> + </mk-container> + + <section class="_card" style="margin-top: var(--margin);"> + <div class="_content">{{ $t('scratchpadDescription') }}</div> + </section> +</div> +</template> + +<script lang="ts"> +import Vue from 'vue'; +import "prismjs"; +import "prismjs/themes/prism.css"; +import { faTerminal, faPlay } from '@fortawesome/free-solid-svg-icons'; +import PrismEditor from 'vue-prism-editor'; +import { AiScript, parse, utils, values } from '@syuilo/aiscript'; +import i18n from '../i18n'; +import MkContainer from '../components/ui/container.vue'; +import MkButton from '../components/ui/button.vue'; + +export default Vue.extend({ + i18n, + + metaInfo() { + return { + title: this.$t('scratchpad') as string + }; + }, + + components: { + MkContainer, + MkButton, + PrismEditor, + }, + + data() { + return { + code: '', + logs: [], + faTerminal, faPlay + } + }, + + watch: { + code() { + localStorage.setItem('scratchpad', this.code); + } + }, + + created() { + const saved = localStorage.getItem('scratchpad'); + if (saved) { + this.code = saved; + } + }, + + methods: { + async run() { + this.logs = []; + const aiscript = new AiScript({ + dialog: values.FN_NATIVE(async ([title, text, type]) => { + await this.$root.dialog({ + type: type ? type.value : 'info', + title: title.value, + text: text.value, + }); + }), + confirm: values.FN_NATIVE(async ([title, text]) => { + const confirm = await this.$root.dialog({ + type: 'warning', + showCancelButton: true, + title: title.value, + text: text.value, + }); + return confirm.canceled ? values.FALSE : values.TRUE + }), + }, { + in: (q) => { + return new Promise(ok => { + this.$root.dialog({ + title: q, + input: {} + }).then(({ canceled, result: a }) => { + ok(a); + }); + }); + }, + out: (value) => { + this.logs.push({ + id: Math.random(), + text: value.type === 'str' ? value.value : utils.valToString(value), + print: true + }); + }, + log: (type, params) => { + switch (type) { + case 'end': this.logs.push({ + id: Math.random(), + text: utils.valToString(params.val, true), + print: false + }); break; + default: break; + } + } + }); + + let ast; + try { + ast = parse(this.code); + } catch (e) { + this.$root.dialog({ + type: 'error', + text: 'Syntax error :(' + }); + return; + } + try { + await aiscript.exec(ast); + } catch (e) { + this.$root.dialog({ + type: 'error', + text: e + }); + } + } + } +}); +</script> + +<style lang="scss" scoped> +.bepmlvbi { + padding: 16px; + + > .log { + &:not(.print) { + opacity: 0.7; + } + } +} +</style> diff --git a/src/client/router.ts b/src/client/router.ts index 9644ede55ff72a74d10b35fa1d96ed89ab2ddcee..428be7ecca30c732639e85873883ffab06900fae 100644 --- a/src/client/router.ts +++ b/src/client/router.ts @@ -48,6 +48,7 @@ export const router = new VueRouter({ { path: '/my/antennas', component: page('my-antennas/index') }, { path: '/my/apps', component: page('apps') }, { path: '/preferences', component: page('preferences/index') }, + { path: '/scratchpad', component: page('scratchpad') }, { path: '/instance', component: page('instance/index') }, { path: '/instance/emojis', component: page('instance/emojis') }, { path: '/instance/users', component: page('instance/users') }, diff --git a/src/client/scripts/hotkey.ts b/src/client/scripts/hotkey.ts index 7d1bb16e792e5be9b8eda460331f01de0055869d..5f73aa58b941d619855dbab48570efe1a3f8a3cc 100644 --- a/src/client/scripts/hotkey.ts +++ b/src/client/scripts/hotkey.ts @@ -80,6 +80,7 @@ export default { el._keyHandler = (e: KeyboardEvent) => { const targetReservedKeys = document.activeElement ? ((document.activeElement as any)._misskey_reservedKeys || []) : []; if (document.activeElement && ignoreElemens.some(el => document.activeElement.matches(el))) return; + if (document.activeElement && document.activeElement.attributes['contenteditable']) return; for (const action of actions) { const matched = match(e, action.patterns);