From ed89b4bd94fae695959b006122603bb28b94dbc9 Mon Sep 17 00:00:00 2001
From: FineArchs <133759614+FineArchs@users.noreply.github.com>
Date: Mon, 7 Oct 2024 09:46:04 +0900
Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=8B=A1=E5=BC=B5=E6=A9=9F?=
 =?UTF-8?q?=E8=83=BD=E3=82=A4=E3=83=B3=E3=82=B9=E3=83=88=E3=83=BC=E3=83=AB?=
 =?UTF-8?q?=E3=81=AE=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AE=E4=B8=80=E9=83=A8?=
 =?UTF-8?q?=E3=82=92=E3=82=B3=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=8D=E3=83=B3?=
 =?UTF-8?q?=E3=83=88=E3=81=A8=E3=81=97=E3=81=A6=E5=88=86=E9=9B=A2=20(#1465?=
 =?UTF-8?q?4)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* create MkExtensionInstaller.vue

* annotation

* add fallbacks

* storybook

* update annotations

* Update MkExtensionInstaller.vue

* use additonalInfo slot
---
 .../MkExtensionInstaller.stories.impl.ts      |  83 ++++++++++
 .../src/components/MkExtensionInstaller.vue   | 146 ++++++++++++++++++
 .../frontend/src/pages/install-extensions.vue | 117 +++-----------
 3 files changed, 250 insertions(+), 96 deletions(-)
 create mode 100644 packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts
 create mode 100644 packages/frontend/src/components/MkExtensionInstaller.vue

diff --git a/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts
new file mode 100644
index 0000000000..6763f7c546
--- /dev/null
+++ b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts
@@ -0,0 +1,83 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { StoryObj } from '@storybook/vue3';
+import MkExtensionInstaller from './MkExtensionInstaller.vue';
+import lightTheme from '@@/themes/_light.json5';
+
+export const Plugin = {
+	render(args) {
+		return {
+			components: {
+				MkExtensionInstaller,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkExtensionInstaller v-bind="props" />',
+		};
+	},
+	args: {
+		extension: {
+			type: 'plugin',
+			raw: '"do nothing"',
+			meta: {
+				name: 'do nothing plugin',
+				version: '1.0',
+				author: 'syuilo and misskey-project',
+				description: 'a plugin that does nothing',
+				permissions: ['read:account'],
+				config: {
+					'doNothing': true,
+				},
+			},
+		},
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkExtensionInstaller>;
+
+export const Theme = {
+	render(args) {
+		return {
+			components: {
+				MkExtensionInstaller,
+			},
+			setup() {
+				return {
+					args,
+				};
+			},
+			computed: {
+				props() {
+					return {
+						...this.args,
+					};
+				},
+			},
+			template: '<MkExtensionInstaller v-bind="props" />',
+		};
+	},
+	args: {
+		extension: {
+			type: 'theme',
+			raw: JSON.stringify(lightTheme),
+			meta: lightTheme,
+		},
+	},
+	parameters: {
+		layout: 'centered',
+	},
+} satisfies StoryObj<typeof MkExtensionInstaller>;
diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue
new file mode 100644
index 0000000000..0f7acd69e7
--- /dev/null
+++ b/packages/frontend/src/components/MkExtensionInstaller.vue
@@ -0,0 +1,146 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div class="_gaps_m" :class="$style.extInstallerRoot">
+	<div :class="$style.extInstallerIconWrapper">
+		<i v-if="isPlugin" class="ti ti-plug"></i>
+		<i v-else-if="isTheme" class="ti ti-palette"></i>
+		<!-- 拡張用? -->
+		<i v-else class="ti ti-download"></i>
+	</div>
+	<h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].title }}</h2>
+	<div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
+	<MkInfo v-if="isPlugin" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
+	<FormSection>
+		<template #label>{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].metaTitle }}</template>
+		<div class="_gaps_s">
+			<FormSplit>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.name }}</template>
+					<template #value>{{ extension.meta.name }}</template>
+				</MkKeyValue>
+				<MkKeyValue>
+					<template #key>{{ i18n.ts.author }}</template>
+					<template #value>{{ extension.meta.author }}</template>
+				</MkKeyValue>
+			</FormSplit>
+			<MkKeyValue v-if="isPlugin">
+				<template #key>{{ i18n.ts.description }}</template>
+				<template #value>{{ extension.meta.description ?? i18n.ts.none }}</template>
+			</MkKeyValue>
+			<MkKeyValue v-if="isPlugin">
+				<template #key>{{ i18n.ts.version }}</template>
+				<template #value>{{ extension.meta.version }}</template>
+			</MkKeyValue>
+			<MkKeyValue v-if="isPlugin">
+				<template #key>{{ i18n.ts.permission }}</template>
+				<template #value>
+					<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
+						<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
+					</ul>
+					<template v-else>{{ i18n.ts.none }}</template>
+				</template>
+			</MkKeyValue>
+			<MkKeyValue v-if="isTheme">
+				<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
+				<template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template>
+			</MkKeyValue>
+			<MkFolder>
+				<template #icon><i class="ti ti-code"></i></template>
+				<template #label>{{ i18n.ts._plugin.viewSource }}</template>
+
+				<MkCode :code="extension.raw"/>
+			</MkFolder>
+		</div>
+	</FormSection>
+	<slot name="additionalInfo"/>
+	<div class="_buttonsCenter">
+		<MkButton primary @click="emits('confirm')"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+export type Extension = {
+	type: 'plugin';
+	raw: string;
+	meta: {
+		name: string;
+		version: string;
+		author: string;
+		description?: string;
+		permissions?: string[];
+		config?: Record<string, any>;
+	};
+} | {
+	type: 'theme';
+	raw: string;
+	meta: {
+		name: string;
+		author: string;
+		base?: 'light' | 'dark';
+	};
+};
+</script>
+<script lang="ts" setup>
+import { computed } from '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 MkInfo from '@/components/MkInfo.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import { i18n } from '@/i18n.js';
+
+const isPlugin = computed(() => props.extension.type === 'plugin');
+const isTheme = computed(() => props.extension.type === 'theme');
+
+const props = defineProps<{
+	extension: Extension;
+}>();
+
+const emits = defineEmits<{
+	(ev: 'confirm'): void;
+}>();
+</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);
+}
+
+.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/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue
index 4bee437f65..83f16fce68 100644
--- a/packages/frontend/src/pages/install-extensions.vue
+++ b/packages/frontend/src/pages/install-extensions.vue
@@ -8,76 +8,26 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<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>
+		<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.name }}</template>
-							<template #value>{{ data.meta?.name }}</template>
+							<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
+							<template #value><MkUrl :url="url" :showUrlPreview="false"></MkUrl></template>
 						</MkKeyValue>
 						<MkKeyValue>
-							<template #key>{{ i18n.ts.author }}</template>
-							<template #value>{{ data.meta?.author }}</template>
+							<template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
+							<template #value>
+								<!-- この画面が出ている時点でハッシュの検証には成功している -->
+								<i class="ti ti-check" style="color: var(--accent)"></i>
+							</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>
+				</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>
@@ -96,14 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, computed, onActivated, onDeactivated, nextTick } from 'vue';
 import MkLoading from '@/components/global/MkLoading.vue';
+import MkExtensionInstaller, { type Extension } from '@/components/MkExtensionInstaller.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 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 { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js';
@@ -124,24 +71,7 @@ const errorKV = ref<{
 const url = ref<string | null>(null);
 const hash = ref<string | null>(null);
 
-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);
+const data = ref<Extension | null>(null);
 
 function goBack(): void {
 	history.back();
@@ -227,7 +157,7 @@ async function fetch() {
 				data.value = {
 					type: 'theme',
 					meta: {
-						description,
+						// description, // 使用されていない
 						...meta,
 					},
 					raw: res.data,
@@ -353,9 +283,4 @@ definePageMetadata(() => ({
 .extInstallerNormDesc {
 	text-align: center;
 }
-
-.extInstallerKVList {
-	margin-top: 0;
-	margin-bottom: 0;
-}
 </style>