<!-- SPDX-FileCopyrightText: syuilo and misskey-project SPDX-License-Identifier: AGPL-3.0-only --> <template> <MkStickyContainer> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <MkSpacer :contentMax="550"> <MkLoading v-if="uiPhase === 'fetching'"/> <MkExtensionInstaller v-else-if="uiPhase === 'confirm' && data" :extension="data" @confirm="install()" @cancel="close_()"> <template #additionalInfo> <FormSection> <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(--MI_THEME-accent)"></i> </template> </MkKeyValue> </div> </FormSection> </template> </MkExtensionInstaller> <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="close_()">{{ i18n.ts.close }}</MkButton> </div> </div> </MkSpacer> </MkStickyContainer> </template> <script lang="ts" setup> import { ref, computed, nextTick } from 'vue'; import type { Extension } from '@/components/MkExtensionInstaller.vue'; import type { AiScriptPluginMeta } from '@/plugin.js'; import MkLoading from '@/components/global/MkLoading.vue'; import MkExtensionInstaller from '@/components/MkExtensionInstaller.vue'; import MkButton from '@/components/MkButton.vue'; import MkKeyValue from '@/components/MkKeyValue.vue'; import MkUrl from '@/components/global/MkUrl.vue'; import FormSection from '@/components/form/section.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { parsePluginMeta, installPlugin } from '@/plugin.js'; import { parseThemeCode, installTheme } from '@/theme.js'; import { unisonReload } from '@/utility/unison-reload.js'; import { i18n } from '@/i18n.js'; import { definePage } from '@/page.js'; const uiPhase = ref<'fetching' | 'confirm' | 'error'>('fetching'); const errorKV = ref<{ title?: string; description?: string; }>({ title: '', description: '', }); const url = ref<string | null>(null); const hash = ref<string | null>(null); const data = ref<Extension | null>(null); function close_(): void { if (window.history.length === 1) { window.close(); } else { window.history.back(); } } async function fetch() { if (!url.value || !hash.value) { errorKV.value = { title: i18n.ts._externalResourceInstaller._errors._invalidParams.title, description: i18n.ts._externalResourceInstaller._errors._invalidParams.description, }; uiPhase.value = 'error'; return; } const res = await misskeyApi('fetch-external-resources', { url: url.value, hash: hash.value, }).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(); window.setTimeout(() => { close_(); }, 3000); } 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(); window.setTimeout(() => { close_(); }, 3000); } } const urlParams = new URLSearchParams(window.location.search); url.value = urlParams.get('url'); hash.value = urlParams.get('hash'); fetch(); const headerActions = computed(() => []); const headerTabs = computed(() => []); definePage(() => ({ title: i18n.ts._externalResourceInstaller.title, icon: 'ti ti-download', })); </script> <style lang="scss" module> .extInstallerRoot { border-radius: var(--MI-radius); background: var(--MI_THEME-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(--MI_THEME-accentedBg); color: var(--MI_THEME-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; } </style>