summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2020-07-12 00:38:55 +0900
committerGitHub <noreply@github.com>2020-07-12 00:38:55 +0900
commit9b73e897df134ba57d4ac4d0e6e6924f8ddbc23d (patch)
tree2885511bc46ab67c7ef7b7b40f7598e788edcd18 /src
parentfeat(client): 無限にダイアログを出すように (diff)
downloadsharkey-9b73e897df134ba57d4ac4d0e6e6924f8ddbc23d.tar.gz
sharkey-9b73e897df134ba57d4ac4d0e6e6924f8ddbc23d.tar.bz2
sharkey-9b73e897df134ba57d4ac4d0e6e6924f8ddbc23d.zip
Plugin system (#6479)
* wip * wip * wip * wip * Update store.ts
Diffstat (limited to 'src')
-rw-r--r--src/client/components/note.vue15
-rw-r--r--src/client/components/post-form.vue21
-rw-r--r--src/client/components/user-menu.vue12
-rw-r--r--src/client/init.ts31
-rw-r--r--src/client/pages/preferences/index.vue6
-rw-r--r--src/client/pages/preferences/plugins.vue134
-rw-r--r--src/client/pages/scratchpad.vue2
-rw-r--r--src/client/scripts/aiscript/api.ts (renamed from src/client/scripts/create-aiscript-env.ts)15
-rw-r--r--src/client/scripts/hpml/evaluator.ts2
-rw-r--r--src/client/store.ts59
10 files changed, 285 insertions, 12 deletions
diff --git a/src/client/components/note.vue b/src/client/components/note.vue
index badb9f12f3..63a803c7f4 100644
--- a/src/client/components/note.vue
+++ b/src/client/components/note.vue
@@ -89,7 +89,7 @@
<script lang="ts">
import Vue from 'vue';
-import { faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
+import { faBolt, faTimes, faBullhorn, faStar, faLink, faExternalLinkSquareAlt, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faQuoteRight, faInfoCircle, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
import { faCopy, faTrashAlt, faEdit, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
import { parse } from '../../mfm/parse';
import { sum, unique } from '../../prelude/array';
@@ -108,7 +108,6 @@ import { url } from '../config';
import copyToClipboard from '../scripts/copy-to-clipboard';
export default Vue.extend({
-
components: {
XSub,
XNoteHeader,
@@ -145,7 +144,7 @@ export default Vue.extend({
showContent: false,
hideThisNote: false,
noteBody: this.$refs.noteBody,
- faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faEllipsisH
+ faEdit, faBolt, faTimes, faBullhorn, faPlus, faMinus, faRetweet, faReply, faReplyAll, faEllipsisH, faHome, faUnlock, faEnvelope, faThumbtack, faBan, faBiohazard, faPlug
};
},
@@ -612,6 +611,16 @@ export default Vue.extend({
.filter(x => x !== undefined);
}
+ if (this.$store.state.noteActions.length > 0) {
+ menu = menu.concat([null, ...this.$store.state.noteActions.map(action => ({
+ icon: faPlug,
+ text: action.title,
+ action: () => {
+ action.handler(this.appearNote);
+ }
+ }))]);
+ }
+
this.$root.menu({
items: menu,
source: this.$refs.menuButton,
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index 392dd8c48b..f0de602c29 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -44,6 +44,7 @@
<button class="_button" @click="useCw = !useCw" :class="{ active: useCw }" v-tooltip="$t('useCw')"><fa :icon="faEyeSlash"/></button>
<button class="_button" @click="insertMention" v-tooltip="$t('mention')"><fa :icon="faAt"/></button>
<button class="_button" @click="insertEmoji" v-tooltip="$t('emoji')"><fa :icon="faLaughSquint"/></button>
+ <button class="_button" @click="showActions" v-tooltip="$t('plugin')" v-if="$store.state.postFormActions.length > 0"><fa :icon="faPlug"/></button>
</footer>
<input ref="file" class="file _button" type="file" multiple="multiple" @change="onChangeFile"/>
</div>
@@ -52,7 +53,7 @@
<script lang="ts">
import Vue from 'vue';
-import { faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard } from '@fortawesome/free-solid-svg-icons';
+import { faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard, faPlug } from '@fortawesome/free-solid-svg-icons';
import { faEyeSlash, faLaughSquint } from '@fortawesome/free-regular-svg-icons';
import insertTextAtCursor from 'insert-text-at-cursor';
import { length } from 'stringz';
@@ -133,7 +134,7 @@ export default Vue.extend({
draghover: false,
quoteId: null,
recentHashtags: JSON.parse(localStorage.getItem('hashtags') || '[]'),
- faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard
+ faReply, faQuoteRight, faPaperPlane, faTimes, faUpload, faPollH, faGlobe, faHome, faUnlock, faEnvelope, faEyeSlash, faLaughSquint, faPlus, faPhotoVideo, faCloud, faLink, faAt, faBiohazard, faPlug
};
},
@@ -580,6 +581,22 @@ export default Vue.extend({
vm.close();
});
},
+
+ showActions(ev) {
+ this.$root.menu({
+ items: this.$store.state.postFormActions.map(action => ({
+ text: action.title,
+ action: () => {
+ action.handler({
+ text: this.text
+ }, (key, value) => {
+ if (key === 'text') { this.text = value; }
+ });
+ }
+ })),
+ source: ev.currentTarget || ev.target,
+ });
+ }
}
});
</script>
diff --git a/src/client/components/user-menu.vue b/src/client/components/user-menu.vue
index 25937fb3c0..cbfa7b346d 100644
--- a/src/client/components/user-menu.vue
+++ b/src/client/components/user-menu.vue
@@ -4,7 +4,7 @@
<script lang="ts">
import Vue from 'vue';
-import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash } from '@fortawesome/free-solid-svg-icons';
+import { faAt, faListUl, faEye, faEyeSlash, faBan, faPencilAlt, faComments, faUsers, faMicrophoneSlash, faPlug } from '@fortawesome/free-solid-svg-icons';
import { faSnowflake, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import XMenu from './menu.vue';
import copyToClipboard from '../scripts/copy-to-clipboard';
@@ -80,6 +80,16 @@ export default Vue.extend({
}]);
}
+ if (this.$store.state.userActions.length > 0) {
+ menu = menu.concat([null, ...this.$store.state.userActions.map(action => ({
+ icon: faPlug,
+ text: action.title,
+ action: () => {
+ action.handler(this.user);
+ }
+ }))]);
+ }
+
return {
items: menu
};
diff --git a/src/client/init.ts b/src/client/init.ts
index d00b4f5cca..7e11efe37c 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -25,6 +25,8 @@ import { isDeviceDarkmode } from './scripts/is-device-darkmode';
import createStore from './store';
import { clientDb, get, count } from './db';
import { setI18nContexts } from './scripts/set-i18n-contexts';
+import { createPluginEnv } from './scripts/aiscript/api';
+import { AiScript } from '@syuilo/aiscript';
Vue.use(Vuex);
Vue.use(VueHotkey);
@@ -231,6 +233,35 @@ os.init(async () => {
//store.commit('instance/set', );
});
+ for (const plugin of store.state.deviceUser.plugins) {
+ console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
+
+ const aiscript = new AiScript(createPluginEnv(app, {
+ plugin: plugin,
+ storageKey: 'plugins:' + plugin.id
+ }), {
+ in: (q) => {
+ return new Promise(ok => {
+ app.dialog({
+ title: q,
+ input: {}
+ }).then(({ canceled, result: a }) => {
+ ok(a);
+ });
+ });
+ },
+ out: (value) => {
+ console.log(value);
+ },
+ log: (type, params) => {
+ },
+ });
+
+ store.commit('initPlugin', { plugin, aiscript });
+
+ aiscript.exec(plugin.ast);
+ }
+
if (store.getters.isSignedIn) {
const main = os.stream.useSharedConnection('main');
diff --git a/src/client/pages/preferences/index.vue b/src/client/pages/preferences/index.vue
index 2b34513865..56c4a5699e 100644
--- a/src/client/pages/preferences/index.vue
+++ b/src/client/pages/preferences/index.vue
@@ -9,6 +9,8 @@
<x-sidebar/>
+ <x-plugins/>
+
<section class="_card">
<div class="_title"><fa :icon="faMusic"/> {{ $t('sounds') }}</div>
<div class="_content">
@@ -115,6 +117,7 @@ import MkRadio from '../../components/ui/radio.vue';
import MkRange from '../../components/ui/range.vue';
import XTheme from './theme.vue';
import XSidebar from './sidebar.vue';
+import XPlugins from './plugins.vue';
import { langs } from '../../config';
import { clientDb, set } from '../../db';
@@ -146,11 +149,12 @@ export default Vue.extend({
components: {
XTheme,
XSidebar,
+ XPlugins,
MkButton,
MkSwitch,
MkSelect,
MkRadio,
- MkRange
+ MkRange,
},
data() {
diff --git a/src/client/pages/preferences/plugins.vue b/src/client/pages/preferences/plugins.vue
new file mode 100644
index 0000000000..afe7c8cafa
--- /dev/null
+++ b/src/client/pages/preferences/plugins.vue
@@ -0,0 +1,134 @@
+<template>
+<section class="_card">
+ <div class="_title"><fa :icon="faPlug"/> {{ $t('plugins') }}</div>
+ <div class="_content">
+ <details>
+ <summary><fa :icon="faDownload"/> {{ $t('install') }}</summary>
+ <mk-info warn>{{ $t('pluginInstallWarn') }}</mk-info>
+ <mk-textarea v-model="script" tall>
+ <span>{{ $t('script') }}</span>
+ </mk-textarea>
+ <mk-button @click="install()" primary><fa :icon="faSave"/> {{ $t('install') }}</mk-button>
+ </details>
+ </div>
+ <div class="_content">
+ <details>
+ <summary><fa :icon="faFolderOpen"/> {{ $t('manage') }}</summary>
+ <mk-select v-model="selectedPluginId">
+ <option v-for="x in $store.state.deviceUser.plugins" :value="x.id" :key="x.id">{{ x.name }}</option>
+ </mk-select>
+ <template v-if="selectedPlugin">
+ <div class="_keyValue">
+ <div>{{ $t('version') }}:</div>
+ <div>{{ selectedPlugin.version }}</div>
+ </div>
+ <div class="_keyValue">
+ <div>{{ $t('author') }}:</div>
+ <div>{{ selectedPlugin.author }}</div>
+ </div>
+ <div class="_keyValue">
+ <div>{{ $t('description') }}:</div>
+ <div>{{ selectedPlugin.description }}</div>
+ </div>
+ <mk-button @click="uninstall()" style="margin-top: 8px;"><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button>
+ </template>
+ </details>
+ </div>
+</section>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { faPlug, faSave, faTrashAlt, faFolderOpen, faDownload } from '@fortawesome/free-solid-svg-icons';
+import MkButton from '../../components/ui/button.vue';
+import MkTextarea from '../../components/ui/textarea.vue';
+import MkSelect from '../../components/ui/select.vue';
+import MkInfo from '../../components/ui/info.vue';
+import { AiScript, parse } from '@syuilo/aiscript';
+
+export default Vue.extend({
+ components: {
+ MkButton,
+ MkTextarea,
+ MkSelect,
+ MkInfo,
+ },
+
+ data() {
+ return {
+ script: '',
+ selectedPluginId: null,
+ faPlug, faSave, faTrashAlt, faFolderOpen, faDownload
+ }
+ },
+
+ computed: {
+ selectedPlugin() {
+ if (this.selectedPluginId == null) return null;
+ return this.$store.state.deviceUser.plugins.find(x => x.id === this.selectedPluginId);
+ },
+ },
+
+ methods: {
+ install() {
+ let ast;
+ try {
+ ast = parse(this.script);
+ } catch (e) {
+ this.$root.dialog({
+ type: 'error',
+ text: 'Syntax error :('
+ });
+ return;
+ }
+ const meta = AiScript.collectMetadata(ast);
+ console.log(meta);
+ if (meta == null) {
+ this.$root.dialog({
+ type: 'error',
+ text: 'No metadata found :('
+ });
+ return;
+ }
+ const data = meta.get(null);
+ if (data == null) {
+ this.$root.dialog({
+ type: 'error',
+ text: 'No metadata found :('
+ });
+ return;
+ }
+ const { id, name, version, author, description } = data;
+ if (id == null || name == null || version == null || author == null) {
+ this.$root.dialog({
+ type: 'error',
+ text: 'Required property not found :('
+ });
+ return;
+ }
+ this.$store.commit('deviceUser/installPlugin', {
+ meta: {
+ id, name, version, author, description
+ },
+ ast
+ });
+ this.$root.dialog({
+ type: 'success',
+ iconOnly: true, autoClose: true
+ });
+ },
+
+ uninstall() {
+ this.$store.commit('deviceUser/uninstallPlugin', this.selectedPluginId);
+ this.$root.dialog({
+ type: 'success',
+ iconOnly: true, autoClose: true
+ });
+ }
+ },
+});
+</script>
+
+<style lang="scss" scoped>
+
+</style>
diff --git a/src/client/pages/scratchpad.vue b/src/client/pages/scratchpad.vue
index 81d4e60459..025505295b 100644
--- a/src/client/pages/scratchpad.vue
+++ b/src/client/pages/scratchpad.vue
@@ -30,7 +30,7 @@ import PrismEditor from 'vue-prism-editor';
import { AiScript, parse, utils, values } from '@syuilo/aiscript';
import MkContainer from '../components/ui/container.vue';
import MkButton from '../components/ui/button.vue';
-import { createAiScriptEnv } from '../scripts/create-aiscript-env';
+import { createAiScriptEnv } from '../scripts/aiscript/api';
export default Vue.extend({
metaInfo() {
diff --git a/src/client/scripts/create-aiscript-env.ts b/src/client/scripts/aiscript/api.ts
index dfa38be385..29baa25b1a 100644
--- a/src/client/scripts/create-aiscript-env.ts
+++ b/src/client/scripts/aiscript/api.ts
@@ -40,3 +40,18 @@ export function createAiScriptEnv(vm, opts) {
}),
};
}
+
+export function createPluginEnv(vm, opts) {
+ return {
+ ...createAiScriptEnv(vm, opts),
+ 'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
+ vm.$store.commit('registerPostFormAction', { pluginId: opts.plugin.id, title: title.value, handler });
+ }),
+ 'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => {
+ vm.$store.commit('registerUserAction', { pluginId: opts.plugin.id, title: title.value, handler });
+ }),
+ 'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
+ vm.$store.commit('registerNoteAction', { pluginId: opts.plugin.id, title: title.value, handler });
+ }),
+ };
+}
diff --git a/src/client/scripts/hpml/evaluator.ts b/src/client/scripts/hpml/evaluator.ts
index f1fcdde0e5..a056884368 100644
--- a/src/client/scripts/hpml/evaluator.ts
+++ b/src/client/scripts/hpml/evaluator.ts
@@ -3,7 +3,7 @@ import * as seedrandom from 'seedrandom';
import { Variable, PageVar, envVarsDef, funcDefs, Block, isFnBlock } from '.';
import { version } from '../../config';
import { AiScript, utils, values } from '@syuilo/aiscript';
-import { createAiScriptEnv } from '../create-aiscript-env';
+import { createAiScriptEnv } from '../aiscript/api';
import { collectPageVars } from '../collect-page-vars';
import { initLib } from './lib';
diff --git a/src/client/store.ts b/src/client/store.ts
index eaa8ea6a69..31febc782b 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -3,6 +3,7 @@ import createPersistedState from 'vuex-persistedstate';
import * as nestedProperty from 'nested-property';
import { faTerminal, faHashtag, faBroadcastTower, faFireAlt, faSearch, faStar, faAt, faListUl, faUserClock, faUsers, faCloud, faGamepad, faFileAlt, faSatellite, faDoorClosed, faColumns } from '@fortawesome/free-solid-svg-icons';
import { faBell, faEnvelope, faComments } from '@fortawesome/free-regular-svg-icons';
+import { AiScript, utils, values } from '@syuilo/aiscript';
import { apiUrl, deckmode } from './config';
import { erase } from '../prelude/array';
@@ -43,6 +44,7 @@ export const defaultDeviceUserSettings = {
columns: [],
layout: [],
},
+ plugins: [],
};
export const defaultDeviceSettings = {
@@ -93,7 +95,13 @@ export default () => new Vuex.Store({
state: {
i: null,
pendingApiRequestsCount: 0,
- spinner: null
+ spinner: null,
+
+ // Plugin
+ pluginContexts: new Map<string, AiScript>(),
+ postFormActions: [],
+ userActions: [],
+ noteActions: [],
},
getters: {
@@ -224,8 +232,38 @@ export default () => new Vuex.Store({
state.i = x;
},
- updateIKeyValue(state, x) {
- state.i[x.key] = x.value;
+ updateIKeyValue(state, { key, value }) {
+ state.i[key] = value;
+ },
+
+ initPlugin(state, { plugin, aiscript }) {
+ state.pluginContexts.set(plugin.id, aiscript);
+ },
+
+ registerPostFormAction(state, { pluginId, title, handler }) {
+ state.postFormActions.push({
+ title, handler: (form, update) => {
+ state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
+ update(key.value, value.value);
+ })]);
+ }
+ });
+ },
+
+ registerUserAction(state, { pluginId, title, handler }) {
+ state.userActions.push({
+ title, handler: (user) => {
+ state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(user)]);
+ }
+ });
+ },
+
+ registerNoteAction(state, { pluginId, title, handler }) {
+ state.noteActions.push({
+ title, handler: (note) => {
+ state.pluginContexts.get(pluginId).execFn(handler, [utils.jsToVal(note)]);
+ }
+ });
},
},
@@ -546,6 +584,21 @@ export default () => new Vuex.Store({
column = x;
},
//#endregion
+
+ installPlugin(state, { meta, ast }) {
+ state.plugins.push({
+ id: meta.id,
+ name: meta.name,
+ version: meta.version,
+ author: meta.author,
+ description: meta.description,
+ ast: ast
+ });
+ },
+
+ uninstallPlugin(state, id) {
+ state.plugins = state.plugins.filter(x => x.id != id);
+ },
}
},