parent
debc0086fa
commit
9b73e897df
13 changed files with 293 additions and 17 deletions
|
@ -523,6 +523,9 @@ themeEditor: "テーマエディター"
|
|||
description: "説明"
|
||||
author: "作者"
|
||||
leaveConfirm: "未保存の変更があります。破棄しますか?"
|
||||
manage: "管理"
|
||||
plugins: "プラグイン"
|
||||
pluginInstallWarn: "信頼できないプラグインはインストールしないでください。"
|
||||
deck: "デッキ"
|
||||
undeck: "デッキ解除"
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
"@koa/multer": "3.0.0",
|
||||
"@koa/router": "9.3.1",
|
||||
"@sinonjs/fake-timers": "6.0.1",
|
||||
"@syuilo/aiscript": "0.7.0",
|
||||
"@syuilo/aiscript": "0.7.2",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
"@types/bull": "3.14.0",
|
||||
"@types/cbor": "5.0.0",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
134
src/client/pages/preferences/plugins.vue
Normal file
134
src/client/pages/preferences/plugins.vue
Normal file
|
@ -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>
|
|
@ -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() {
|
||||
|
|
|
@ -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 });
|
||||
}),
|
||||
};
|
||||
}
|
|
@ -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';
|
||||
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -197,10 +197,10 @@
|
|||
dependencies:
|
||||
"@sinonjs/commons" "^1.7.0"
|
||||
|
||||
"@syuilo/aiscript@0.7.0":
|
||||
version "0.7.0"
|
||||
resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.7.0.tgz#1394511a789891e844d32e536a203fe0d92b3039"
|
||||
integrity sha512-X4TaP/FO7RD8MpFSPDFwKAI4KX7byn8ApqmSSmf2bxcwCTcdevsbyxsLrvkbNaWclIoqTgXwtJjY+2Tc2exeXA==
|
||||
"@syuilo/aiscript@0.7.2":
|
||||
version "0.7.2"
|
||||
resolved "https://registry.yarnpkg.com/@syuilo/aiscript/-/aiscript-0.7.2.tgz#2f30adb14ffa9f1180af83c059927ab306b175a5"
|
||||
integrity sha512-l8HVTJTq9KLzDqGswOIGlBepkacudUp70EScrLjL7nEL2NKcti7Ui5fwZCrmxazxgGz6NrVNX5UBIOFFyrwr0A==
|
||||
dependencies:
|
||||
autobind-decorator "2.4.0"
|
||||
chalk "4.0.0"
|
||||
|
|
Loading…
Reference in a new issue