
* wip
* wip
* wip
* test
* wip rollup pluginでsearchIndexの情報生成
* wip
* SPDX
* wip: markerIdを自動付与
* rollupでビルド時・devモード時に毎回uuidを生成するように
* 開発サーバーでだけ必要な挙動は開発サーバーのみで
* 条件が逆
* wip: childrenの生成
* update comment
* update comment
* rename auto generated file
* hashをパスと行数から決定
* Update privacy.vue
* Update privacy.vue
* wip
* Update general.vue
* Update general.vue
* wip
* wip
* Update SearchMarker.vue
* wip
* Update profile.vue
* Update mute-block.vue
* Update mute-block.vue
* Update general.vue
* Update general.vue
* childrenがduplicate key errorを吐く問題をいったん解決
* マーカーの形を成形
* loggerを置きかえ
* とりあえず省略記法に対応
* Refactor and Format codes
* wip
* Update settings-search-index.ts
* wip
* wip
* とりあえず不確定要因の仮置きidを削除
* hashの生成を正規化(絶対パスになっていたのを緩和)
* pathの入力を省略可能に
* adminでもパス生成できるように
* Update settings-search-index.ts
* Update privacy.vue
* wip
* build searchIndex
* wip
* build
* Update general.vue
* build
* Update sounds.vue
* build
* build
* Update sounds.vue
* 🎨
* 🎨
* Update privacy.vue
* Update privacy.vue
* Update security.vue
* create-search-indexを多少改善
* build
* Update 2fa.vue
* wip
* 必ずtransformCodeCacheを利用するように, キャッシュの明確な受け渡しを定義
* キャッシュはdevServerでなくても更新
* Revert "wip"
This reverts commit 41bffd3a13
.
* inlining
* wip
* Update theme.vue
* 🎨
* wip normalize
* Update theme.vue
* キャッシュのパス変換
* build
* wip
* wip
* Update SearchMarker.vue
* i18n.ts['key'] の形式が取り出せない問題のFix
* build
* 仮でpath入れ
* 必ず絶対パスが使われるように
* wip
* 🎨
* storybookビルド時はcreateSearchIndexをしない
* inliningの構造化
* format code
* Update index.vue
* wip
* wip
* 🎨
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* wip
* clean up
* wip
* wip
* wip
* Update rollup-plugin-unwind-css-module-class-name.test.ts
* Update navbar.vue
* clean up
* wip
* wip
* wip
* wip
* wip
* Update preferences-backups.vue
* Update common.ts
* Update preferences.ts
* wip
* wip
* wip
* wip
* Update MkPreferenceContainer.vue
* Update MkPreferenceContainer.vue
* Update MkPreferenceContainer.vue
* enhance: 検索で上下矢印を使用することで検索結果を移動できるように
* Update main-boot.ts
* refactor
* wip
* Update sounds.vue
* fix(frontend): PageWindowでSearchMarkerが動作するように
* enhance(frontend): SearchMarkerの点滅を一定時間で止める
* wip
* lint fix
* fix: 子要素監視が抜けていたのを修正
* アニメーションの回数はCSSで制御するように
* refactor
* enhance(frontend): 検索インデックス作成時のログを削減
* revert
* fix
* fix
* Update preferences.ts
* Update preferences.ts
* wip
* Update preferences.ts
* wip
* 🎨
* wip
* Update MkPreferenceContainer.vue
* wip
* Update preferences.ts
* wip
* Update preferences.ts
* Update preferences.ts
* wip
* wip
* Update preferences.ts
* wip
* wip
* Update preferences.ts
* Update CHANGELOG.md
* Update preferences.ts
* Update deck-store.ts
* deckStoreをdefaultStoreに統合
* wip
* defaultStore -> store
* Update profile.ts
* wip
* refactor
* wip: plugin
* plugin
* plugin
* plugin
* Update plugin.ts
* wip
* Update plugin.vue
* Update preferences.ts
* Update main-boot.ts
* wip
* fix test
* Update plugin.vue
* Update plugin.vue
* Update utility.ts
* wip
* wip
* Update utility.ts
* wip
* wip
* clean up
* Update utility.ts
---------
Co-authored-by: tai-cha <dev@taichan.site>
Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
289 lines
8.0 KiB
Vue
289 lines
8.0 KiB
Vue
<!--
|
|
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="500">
|
|
<MkLoading v-if="uiPhase === 'fetching'"/>
|
|
<MkExtensionInstaller v-else-if="uiPhase === 'confirm' && data" :extension="data" @confirm="install()">
|
|
<template #additionalInfo>
|
|
<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(--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="goBack()">{{ i18n.ts.goBack }}</MkButton>
|
|
<MkButton @click="goToMisskey()">{{ i18n.ts.goToMisskey }}</MkButton>
|
|
</div>
|
|
</div>
|
|
</MkSpacer>
|
|
</MkStickyContainer>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import { ref, computed, onActivated, onDeactivated, 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 '@/scripts/misskey-api.js';
|
|
import { parsePluginMeta, installPlugin } from '@/plugin.js';
|
|
import { parseThemeCode, installTheme } from '@/scripts/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 url = ref<string | null>(null);
|
|
const hash = ref<string | null>(null);
|
|
|
|
const data = ref<Extension | null>(null);
|
|
|
|
function goBack(): void {
|
|
history.back();
|
|
}
|
|
|
|
function goToMisskey(): void {
|
|
location.href = '/';
|
|
}
|
|
|
|
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();
|
|
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';
|
|
});
|
|
}
|
|
}
|
|
|
|
onActivated(() => {
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
url.value = urlParams.get('url');
|
|
hash.value = urlParams.get('hash');
|
|
fetch();
|
|
});
|
|
|
|
onDeactivated(() => {
|
|
uiPhase.value = 'fetching';
|
|
});
|
|
|
|
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(--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>
|