diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2023-10-21 18:41:12 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-10-21 18:41:12 +0900 |
| commit | f51bca41c5f59f9ffce346a3ec32badaf1ccda31 (patch) | |
| tree | b5799527c2d3602da3592f7d6c1b65bb6ac8922c /packages/frontend/src | |
| parent | すべてのフォロー中の人のwithRepliesを変える機能 (#12049) (diff) | |
| download | sharkey-f51bca41c5f59f9ffce346a3ec32badaf1ccda31.tar.gz sharkey-f51bca41c5f59f9ffce346a3ec32badaf1ccda31.tar.bz2 sharkey-f51bca41c5f59f9ffce346a3ec32badaf1ccda31.zip | |
Feat: 外部サイトからテーマ・プラグインのインストールができるように (#12034)
* Feat: 外部サイトからテーマ・プラグインのインストールができるように
* Update Changelog
* Change Changelog
* Remove unnecessary imports
* Update fetch-external-resources.ts
* Update CHANGELOG.md
* Update CHANGELOG.md
Diffstat (limited to 'packages/frontend/src')
| -rw-r--r-- | packages/frontend/src/components/global/MkUrl.vue | 23 | ||||
| -rw-r--r-- | packages/frontend/src/pages/install-extentions.vue | 354 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/plugin.install.vue | 117 | ||||
| -rw-r--r-- | packages/frontend/src/pages/settings/theme.install.vue | 67 | ||||
| -rw-r--r-- | packages/frontend/src/router.ts | 4 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/install-plugin.ts | 129 | ||||
| -rw-r--r-- | packages/frontend/src/scripts/install-theme.ts | 37 |
7 files changed, 573 insertions, 158 deletions
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue index a8832cde01..db8a8399b5 100644 --- a/packages/frontend/src/components/global/MkUrl.vue +++ b/packages/frontend/src/components/global/MkUrl.vue @@ -31,23 +31,28 @@ import * as os from '@/os.js'; import { useTooltip } from '@/scripts/use-tooltip.js'; import { safeURIDecode } from '@/scripts/safe-uri-decode.js'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ url: string; rel?: string; -}>(); + showUrlPreview?: boolean; +}>(), { + showUrlPreview: true, +}); const self = props.url.startsWith(local); const url = new URL(props.url); if (!['http:', 'https:'].includes(url.protocol)) throw new Error('invalid url'); const el = ref(); -useTooltip(el, (showing) => { - os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { - showing, - url: props.url, - source: el.value, - }, {}, 'closed'); -}); +if (props.showUrlPreview) { + useTooltip(el, (showing) => { + os.popup(defineAsyncComponent(() => import('@/components/MkUrlPreviewPopup.vue')), { + showing, + url: props.url, + source: el.value, + }, {}, 'closed'); + }); +} const schema = url.protocol; const hostname = decodePunycode(url.hostname); diff --git a/packages/frontend/src/pages/install-extentions.vue b/packages/frontend/src/pages/install-extentions.vue new file mode 100644 index 0000000000..9674b522aa --- /dev/null +++ b/packages/frontend/src/pages/install-extentions.vue @@ -0,0 +1,354 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkStickyContainer> + <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> + <MkSpacer :contentMax="500"> + <MkLoading v-if="uiPhase === 'fetching'"/> + <div v-else-if="uiPhase === 'confirm' && data" class="_gaps_m" :class="$style.extInstallerRoot"> + <div :class="$style.extInstallerIconWrapper"> + <i v-if="data.type === 'plugin'" class="ti ti-plug"></i> + <i v-else-if="data.type === 'theme'" class="ti ti-palette"></i> + <i v-else class="ti ti-download"></i> + </div> + <h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${data.type}`].title }}</h2> + <div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div> + <MkInfo v-if="data.type === 'plugin'" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo> + <FormSection> + <template #label>{{ i18n.ts._externalResourceInstaller[`_${data.type}`].metaTitle }}</template> + <div class="_gaps_s"> + <FormSplit> + <MkKeyValue> + <template #key>{{ i18n.ts.name }}</template> + <template #value>{{ data.meta?.name }}</template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts.author }}</template> + <template #value>{{ data.meta?.author }}</template> + </MkKeyValue> + </FormSplit> + <MkKeyValue v-if="data.type === 'plugin'"> + <template #key>{{ i18n.ts.description }}</template> + <template #value>{{ data.meta?.description }}</template> + </MkKeyValue> + <MkKeyValue v-if="data.type === 'plugin'"> + <template #key>{{ i18n.ts.version }}</template> + <template #value>{{ data.meta?.version }}</template> + </MkKeyValue> + <MkKeyValue v-if="data.type === 'plugin'"> + <template #key>{{ i18n.ts.permission }}</template> + <template #value> + <ul :class="$style.extInstallerKVList"> + <li v-for="permission in data.meta?.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li> + </ul> + </template> + </MkKeyValue> + <MkKeyValue v-if="data.type === 'theme' && data.meta?.base"> + <template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template> + <template #value>{{ i18n.ts[data.meta.base] }}</template> + </MkKeyValue> + <MkFolder> + <template #icon><i class="ti ti-code"></i></template> + <template #label>{{ i18n.ts._plugin.viewSource }}</template> + + <MkCode :code="data.raw ?? ''"/> + </MkFolder> + </div> + </FormSection> + <FormSection> + <template #label>{{ i18n.ts._externalResourceInstaller._vendorInfo.title }}</template> + <div class="_gaps_s"> + <MkKeyValue> + <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template> + <template #value><MkUrl :url="url ?? ''" :showUrlPreview="false"></MkUrl></template> + </MkKeyValue> + <MkKeyValue> + <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template> + <template #value> + <!--この画面が出ている時点でハッシュの検証には成功している--> + <i class="ti ti-check" style="color: var(--accent)"></i> + </template> + </MkKeyValue> + </div> + </FormSection> + <div class="_buttonsCenter"> + <MkButton primary @click="install()"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton> + </div> + </div> + <div v-else-if="uiPhase === 'error'" class="_gaps_m" :class="[$style.extInstallerRoot, $style.error]"> + <div :class="$style.extInstallerIconWrapper"> + <i class="ti ti-circle-x"></i> + </div> + <h2 :class="$style.extInstallerTitle">{{ errorKV?.title }}</h2> + <div :class="$style.extInstallerNormDesc">{{ errorKV?.description }}</div> + <div class="_buttonsCenter"> + <MkButton @click="goBack()">{{ i18n.ts.goBack }}</MkButton> + <MkButton @click="goToMisskey()">{{ i18n.ts.goToMisskey }}</MkButton> + </div> + </div> + </MkSpacer> +</MkStickyContainer> +</template> + +<script lang="ts" setup> +import { ref, computed, onMounted, nextTick } from 'vue'; +import MkLoading from '@/components/global/MkLoading.vue'; +import MkButton from '@/components/MkButton.vue'; +import FormSection from '@/components/form/section.vue'; +import FormSplit from '@/components/form/split.vue'; +import MkCode from '@/components/MkCode.vue'; +import MkUrl from '@/components/global/MkUrl.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkKeyValue from '@/components/MkKeyValue.vue'; +import * as os from '@/os.js'; +import { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js'; +import { parseThemeCode, installTheme } from '@/scripts/install-theme.js'; +import { unisonReload } from '@/scripts/unison-reload.js'; +import { i18n } from '@/i18n.js'; +import { definePageMetadata } from '@/scripts/page-metadata.js'; + +const uiPhase = ref<'fetching' | 'confirm' | 'error'>('fetching'); +const errorKV = ref<{ + title?: string; + description?: string; +}>({ + title: '', + description: '', +}); + +const urlParams = new URLSearchParams(window.location.search); +const url = urlParams.get('url'); +const hash = urlParams.get('hash'); + +const data = ref<{ + type: 'plugin' | 'theme'; + raw: string; + meta?: { + // Plugin & Theme Common + name: string; + author: string; + + // Plugin + description?: string; + version?: string; + permissions?: string[]; + config?: Record<string, any>; + + // Theme + base?: 'light' | 'dark'; + }; +} | null>(null); + +function goBack(): void { + history.back(); +} + +function goToMisskey(): void { + location.href = '/'; +} + +async function fetch() { + if (!url || !hash) { + errorKV.value = { + title: i18n.ts._externalResourceInstaller._errors._invalidParams.title, + description: i18n.ts._externalResourceInstaller._errors._invalidParams.description, + }; + uiPhase.value = 'error'; + return; + } + const res = await os.api('fetch-external-resources', { + url, + hash, + }).catch((err) => { + switch (err.id) { + case 'bb774091-7a15-4a70-9dc5-6ac8cf125856': + errorKV.value = { + title: i18n.ts._externalResourceInstaller._errors._failedToFetch.title, + description: i18n.ts._externalResourceInstaller._errors._failedToFetch.parseErrorDescription, + }; + uiPhase.value = 'error'; + break; + case '693ba8ba-b486-40df-a174-72f8279b56a4': + errorKV.value = { + title: i18n.ts._externalResourceInstaller._errors._hashUnmatched.title, + description: i18n.ts._externalResourceInstaller._errors._hashUnmatched.description, + }; + uiPhase.value = 'error'; + break; + default: + errorKV.value = { + title: i18n.ts._externalResourceInstaller._errors._failedToFetch.title, + description: i18n.ts._externalResourceInstaller._errors._failedToFetch.fetchErrorDescription, + }; + uiPhase.value = 'error'; + break; + } + throw new Error(err.code); + }); + + if (!res) { + errorKV.value = { + title: i18n.ts._externalResourceInstaller._errors._failedToFetch.title, + description: i18n.ts._externalResourceInstaller._errors._failedToFetch.fetchErrorDescription, + }; + uiPhase.value = 'error'; + return; + } + + switch (res.type) { + case 'plugin': + try { + const meta = await parsePluginMeta(res.data); + data.value = { + type: 'plugin', + meta, + raw: res.data, + }; + } catch (err) { + errorKV.value = { + title: i18n.ts._externalResourceInstaller._errors._pluginParseFailed.title, + description: i18n.ts._externalResourceInstaller._errors._pluginParseFailed.description, + }; + console.error(err); + uiPhase.value = 'error'; + return; + } + break; + + case 'theme': + try { + const metaRaw = parseThemeCode(res.data); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const { id, props, desc: description, ...meta } = metaRaw; + data.value = { + type: 'theme', + meta: { + description, + ...meta, + }, + raw: res.data, + }; + } catch (err) { + switch (err.message.toLowerCase()) { + case 'this theme is already installed': + errorKV.value = { + title: i18n.ts._externalResourceInstaller._errors._themeParseFailed.title, + description: i18n.ts._theme.alreadyInstalled, + }; + break; + + default: + errorKV.value = { + title: i18n.ts._externalResourceInstaller._errors._themeParseFailed.title, + description: i18n.ts._externalResourceInstaller._errors._themeParseFailed.description, + }; + break; + } + console.error(err); + uiPhase.value = 'error'; + return; + } + break; + + default: + errorKV.value = { + title: i18n.ts._externalResourceInstaller._errors._resourceTypeNotSupported.title, + description: i18n.ts._externalResourceInstaller._errors._resourceTypeNotSupported.description, + }; + uiPhase.value = 'error'; + return; + } + + uiPhase.value = 'confirm'; +} + +async function install() { + if (!data.value) return; + + switch (data.value.type) { + case 'plugin': + if (!data.value.meta) return; + try { + await installPlugin(data.value.raw, data.value.meta as AiScriptPluginMeta); + os.success(); + nextTick(() => { + unisonReload('/'); + }); + } catch (err) { + errorKV.value = { + title: i18n.ts._externalResourceInstaller._errors._pluginInstallFailed.title, + description: i18n.ts._externalResourceInstaller._errors._pluginInstallFailed.description, + }; + console.error(err); + uiPhase.value = 'error'; + } + break; + case 'theme': + if (!data.value.meta) return; + await installTheme(data.value.raw); + os.success(); + nextTick(() => { + location.href = '/settings/theme'; + }); + } +} + +onMounted(() => { + fetch(); +}); + +const headerActions = computed(() => []); + +const headerTabs = computed(() => []); + +definePageMetadata({ + title: i18n.ts._externalResourceInstaller.title, + icon: 'ti ti-download', +}); +</script> + +<style lang="scss" module> +.extInstallerRoot { + border-radius: var(--radius); + background: var(--panel); + padding: 1.5rem; +} + +.extInstallerIconWrapper { + width: 48px; + height: 48px; + font-size: 24px; + line-height: 48px; + text-align: center; + border-radius: 50%; + margin-left: auto; + margin-right: auto; + + background-color: var(--accentedBg); + color: var(--accent); +} + +.error .extInstallerIconWrapper { + background-color: rgba(255, 42, 42, .15); + color: #ff2a2a; +} + +.extInstallerTitle { + font-size: 1.2rem; + text-align: center; + margin: 0; +} + +.extInstallerNormDesc { + text-align: center; +} + +.extInstallerKVList { + margin-top: 0; + margin-bottom: 0; +} +</style> diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue index 47ebe9cfd6..693e02d0ed 100644 --- a/packages/frontend/src/pages/settings/plugin.install.vue +++ b/packages/frontend/src/pages/settings/plugin.install.vue @@ -18,130 +18,35 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent, nextTick, ref } from 'vue'; -import { compareVersions } from 'compare-versions'; -import { Interpreter, Parser, utils } from '@syuilo/aiscript'; -import { v4 as uuid } from 'uuid'; +import { nextTick, ref } from 'vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkButton from '@/components/MkButton.vue'; import FormInfo from '@/components/MkInfo.vue'; import * as os from '@/os.js'; -import { ColdDeviceStorage } from '@/store.js'; +import { installPlugin } from '@/scripts/install-plugin.js'; import { unisonReload } from '@/scripts/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; -const parser = new Parser(); -const code = ref(null); - -function installPlugin({ id, meta, src, token }) { - ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({ - ...meta, - id, - active: true, - configData: {}, - token: token, - src: src, - })); -} - -function isSupportedAiScriptVersion(version: string): boolean { - try { - return (compareVersions(version, '0.12.0') >= 0); - } catch (err) { - return false; - } -} +const code = ref<string | null>(null); async function install() { - if (code.value == null) return; + if (!code.value) return; - const lv = utils.getLangVersion(code.value); - if (lv == null) { - os.alert({ - type: 'error', - text: 'No language version annotation found :(', - }); - return; - } else if (!isSupportedAiScriptVersion(lv)) { - os.alert({ - type: 'error', - text: `aiscript version '${lv}' is not supported :(`, - }); - return; - } - - let ast; try { - ast = parser.parse(code.value); - } catch (err) { - os.alert({ - type: 'error', - text: 'Syntax error :(', - }); - return; - } + await installPlugin(code.value); + os.success(); - const meta = Interpreter.collectMetadata(ast); - if (meta == null) { - os.alert({ - type: 'error', - text: 'No metadata found :(', + nextTick(() => { + unisonReload(); }); - return; - } - - const metadata = meta.get(null); - if (metadata == null) { - os.alert({ - type: 'error', - text: 'No metadata found :(', - }); - return; - } - - const { name, version, author, description, permissions, config } = metadata; - if (name == null || version == null || author == null) { + } catch (err) { os.alert({ type: 'error', - text: 'Required property not found :(', + title: 'Install failed', + text: err.toString() ?? null, }); - return; } - - const token = permissions == null || permissions.length === 0 ? null : await new Promise((res, rej) => { - os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), { - title: i18n.ts.tokenRequested, - information: i18n.ts.pluginTokenRequestedDescription, - initialName: name, - initialPermissions: permissions, - }, { - done: async result => { - const { name, permissions } = result; - const { token } = await os.api('miauth/gen-token', { - session: null, - name: name, - permission: permissions, - }); - res(token); - }, - }, 'closed'); - }); - - installPlugin({ - id: uuid(), - meta: { - name, version, author, description, permissions, config, - }, - token, - src: code.value, - }); - - os.success(); - - nextTick(() => { - unisonReload(); - }); } const headerActions = $computed(() => []); diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue index 155ce9d9da..7fa7b23e44 100644 --- a/packages/frontend/src/pages/settings/theme.install.vue +++ b/packages/frontend/src/pages/settings/theme.install.vue @@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only </MkTextarea> <div class="_buttons"> - <MkButton :disabled="installThemeCode == null" inline @click="() => preview(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> + <MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton> <MkButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton> </div> </div> @@ -18,60 +18,41 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { } from 'vue'; -import JSON5 from 'json5'; import MkTextarea from '@/components/MkTextarea.vue'; import MkButton from '@/components/MkButton.vue'; -import { applyTheme, validateTheme } from '@/scripts/theme.js'; +import { parseThemeCode, previewTheme, installTheme } from '@/scripts/install-theme.js'; import * as os from '@/os.js'; -import { addTheme, getThemes } from '@/theme-store'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; let installThemeCode = $ref(null); -function parseThemeCode(code: string) { - let theme; - +async function install(code: string): Promise<void> { try { - theme = JSON5.parse(code); - } catch (err) { - os.alert({ - type: 'error', - text: i18n.ts._theme.invalid, - }); - return false; - } - if (!validateTheme(theme)) { - os.alert({ - type: 'error', - text: i18n.ts._theme.invalid, - }); - return false; - } - if (getThemes().some(t => t.id === theme.id)) { + const theme = parseThemeCode(code); + await installTheme(code); os.alert({ - type: 'info', - text: i18n.ts._theme.alreadyInstalled, + type: 'success', + text: i18n.t('_theme.installed', { name: theme.name }), }); - return false; - } - - return theme; -} - -function preview(code: string): void { - const theme = parseThemeCode(code); - if (theme) applyTheme(theme, false); -} + } catch (err) { + switch (err.message.toLowerCase()) { + case 'this theme is already installed': + os.alert({ + type: 'info', + text: i18n.ts._theme.alreadyInstalled, + }); + break; -async function install(code: string): Promise<void> { - const theme = parseThemeCode(code); - if (!theme) return; - await addTheme(theme); - os.alert({ - type: 'success', - text: i18n.t('_theme.installed', { name: theme.name }), - }); + default: + os.alert({ + type: 'error', + text: i18n.ts._theme.invalid, + }); + break; + } + console.error(err); + } } const headerActions = $computed(() => []); diff --git a/packages/frontend/src/router.ts b/packages/frontend/src/router.ts index 2258edebbb..e73532f6b2 100644 --- a/packages/frontend/src/router.ts +++ b/packages/frontend/src/router.ts @@ -323,6 +323,10 @@ export const routes = [{ path: '/registry', component: page(() => import('./pages/registry.vue')), }, { + path: '/install-extentions', + component: page(() => import('./pages/install-extentions.vue')), + loginRequired: true, +}, { path: '/admin/user/:userId', component: iAmModerator ? page(() => import('./pages/admin-user.vue')) : page(() => import('./pages/not-found.vue')), }, { diff --git a/packages/frontend/src/scripts/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts new file mode 100644 index 0000000000..1310a0dc73 --- /dev/null +++ b/packages/frontend/src/scripts/install-plugin.ts @@ -0,0 +1,129 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { defineAsyncComponent } from 'vue'; +import { compareVersions } from 'compare-versions'; +import { v4 as uuid } from 'uuid'; +import { Interpreter, Parser, utils } from '@syuilo/aiscript'; +import type { Plugin } from '@/store.js'; +import { ColdDeviceStorage } from '@/store.js'; +import * as os from '@/os.js'; +import { i18n } from '@/i18n.js'; + +export type AiScriptPluginMeta = { + name: string; + version: string; + author: string; + description?: string; + permissions?: string[]; + config?: Record<string, any>; +}; + +const parser = new Parser(); + +export function savePlugin({ id, meta, src, token }: { + id: string; + meta: AiScriptPluginMeta; + src: string; + token: string; +}) { + ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({ + ...meta, + id, + active: true, + configData: {}, + token: token, + src: src, + } as Plugin)); +} + +export function isSupportedAiScriptVersion(version: string): boolean { + try { + return (compareVersions(version, '0.12.0') >= 0); + } catch (err) { + return false; + } +} + +export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta> { + if (!code) { + throw new Error('code is required'); + } + + const lv = utils.getLangVersion(code); + if (lv == null) { + throw new Error('No language version annotation found'); + } else if (!isSupportedAiScriptVersion(lv)) { + throw new Error(`Aiscript version '${lv}' is not supported`); + } + + let ast; + try { + ast = parser.parse(code); + } catch (err) { + throw new Error('Aiscript syntax error'); + } + + const meta = Interpreter.collectMetadata(ast); + if (meta == null) { + throw new Error('Meta block not found'); + } + + const metadata = meta.get(null); + if (metadata == null) { + throw new Error('Metadata not found'); + } + + const { name, version, author, description, permissions, config } = metadata; + if (name == null || version == null || author == null) { + throw new Error('Required property not found'); + } + + return { + name, + version, + author, + description, + permissions, + config, + }; +} + +export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { + if (!code) return; + + let realMeta: AiScriptPluginMeta; + if (!meta) { + realMeta = await parsePluginMeta(code); + } else { + realMeta = meta; + } + + const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => { + os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), { + title: i18n.ts.tokenRequested, + information: i18n.ts.pluginTokenRequestedDescription, + initialName: realMeta.name, + initialPermissions: realMeta.permissions, + }, { + done: async result => { + const { name, permissions } = result; + const { token } = await os.api('miauth/gen-token', { + session: null, + name: name, + permission: permissions, + }); + res(token); + }, + }, 'closed'); + }); + + savePlugin({ + id: uuid(), + meta: realMeta, + token, + src: code, + }); +} diff --git a/packages/frontend/src/scripts/install-theme.ts b/packages/frontend/src/scripts/install-theme.ts new file mode 100644 index 0000000000..394b642bf4 --- /dev/null +++ b/packages/frontend/src/scripts/install-theme.ts @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import JSON5 from 'json5'; +import { addTheme, getThemes } from '@/theme-store.js'; +import { Theme, applyTheme, validateTheme } from '@/scripts/theme.js'; + +export function parseThemeCode(code: string): Theme { + let theme; + + try { + theme = JSON5.parse(code); + } catch (err) { + throw new Error('Failed to parse theme json'); + } + if (!validateTheme(theme)) { + throw new Error('This theme is invaild'); + } + if (getThemes().some(t => t.id === theme.id)) { + throw new Error('This theme is already installed'); + } + + return theme; +} + +export function previewTheme(code: string): void { + const theme = parseThemeCode(code); + if (theme) applyTheme(theme, false); +} + +export async function installTheme(code: string): Promise<void> { + const theme = parseThemeCode(code); + if (!theme) return; + await addTheme(theme); +} |