From 53682f5cc6fd76ec325aff771459a5d42a1cd559 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Wed, 25 Sep 2024 12:31:04 +0900
Subject: [PATCH] :art:

---
 locales/index.d.ts                            |  12 +-
 locales/ja-JP.yml                             |   4 +-
 packages/frontend/src/components/MkMenu.vue   | 113 ++++++++++--------
 packages/frontend/src/components/MkModal.vue  |   2 +-
 .../frontend/src/pages/settings/general.vue   |  26 ++--
 .../pages/settings/preferences-backups.vue    |   4 +-
 packages/frontend/src/store.ts                |  10 +-
 7 files changed, 102 insertions(+), 69 deletions(-)

diff --git a/locales/index.d.ts b/locales/index.d.ts
index 2a27eb3e15..f379fe7c40 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -2053,9 +2053,17 @@ export interface Locale extends ILocale {
      */
     "native": string;
     /**
-     * メニューをドロワーで表示しない
+     * メニューのスタイル
      */
-    "disableDrawer": string;
+    "menuStyle": string;
+    /**
+     * ドロワー
+     */
+    "drawer": string;
+    /**
+     * ポップアップ
+     */
+    "popup": string;
     /**
      * ノートのアクションをホバー時のみ表示する
      */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 80cd8dc7cc..25af266c0b 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -509,7 +509,9 @@ uiLanguage: "UIの表示言語"
 aboutX: "{x}について"
 emojiStyle: "絵文字のスタイル"
 native: "ネイティブ"
-disableDrawer: "メニューをドロワーで表示しない"
+menuStyle: "メニューのスタイル"
+drawer: "ドロワー"
+popup: "ポップアップ"
 showNoteActionsOnlyHover: "ノートのアクションをホバー時のみ表示する"
 showReactionsCount: "ノートのリアクション数を表示する"
 noHistory: "履歴はありません"
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index 0d794d84d5..890b99fcc2 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -4,18 +4,22 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div role="menu" @focusin.passive.stop="() => {}">
+<div
+	role="menu"
+	:class="{
+		[$style.root]: true,
+		[$style.center]: align === 'center',
+		[$style.big]: big,
+		[$style.asDrawer]: asDrawer,
+	}"
+	@focusin.passive.stop="() => {}"
+>
 	<div
 		ref="itemsEl"
 		v-hotkey="keymap"
 		tabindex="0"
 		class="_popup _shadow"
-		:class="{
-			[$style.root]: true,
-			[$style.center]: align === 'center',
-			[$style.big]: big,
-			[$style.asDrawer]: asDrawer,
-		}"
+		:class="$style.menu"
 		:style="{
 			width: (width && !asDrawer) ? `${width}px` : '',
 			maxHeight: maxHeight ? `min(${maxHeight}px, calc(100dvh - 32px))` : 'calc(100dvh - 32px)',
@@ -300,6 +304,8 @@ async function showRadioOptions(item: MenuRadio, ev: Event) {
 }
 
 async function showChildren(item: MenuParent, ev: Event) {
+	ev.stopPropagation();
+
 	const children: MenuItem[] = await (async () => {
 		if (childrenCache.has(item)) {
 			return childrenCache.get(item)!;
@@ -421,6 +427,58 @@ onBeforeUnmount(() => {
 
 <style lang="scss" module>
 .root {
+	&.center {
+		> .menu {
+			> .item {
+				text-align: center;
+			}
+		}
+	}
+
+	&.big:not(.asDrawer) {
+		> .menu {
+			> .item {
+				padding: 6px 20px;
+				font-size: 1em;
+				line-height: 24px;
+			}
+		}
+	}
+
+	&.asDrawer {
+		max-width: 600px;
+		margin: auto;
+
+		> .menu {
+			padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
+			width: 100%;
+			border-radius: 24px;
+			border-bottom-right-radius: 0;
+			border-bottom-left-radius: 0;
+
+			> .item {
+				font-size: 1em;
+				padding: 12px 24px;
+
+				&::before {
+					width: calc(100% - 24px);
+					border-radius: 12px;
+				}
+
+				> .icon {
+					margin-right: 14px;
+					width: 24px;
+				}
+			}
+
+			> .divider {
+				margin: 12px 0;
+			}
+		}
+	}
+}
+
+.menu {
 	padding: 8px 0;
 	box-sizing: border-box;
 	max-width: 100vw;
@@ -431,47 +489,6 @@ onBeforeUnmount(() => {
 	&:focus-visible {
 		outline: none;
 	}
-
-	&.center {
-		> .item {
-			text-align: center;
-		}
-	}
-
-	&.big:not(.asDrawer) {
-		> .item {
-			padding: 6px 20px;
-			font-size: 1em;
-			line-height: 24px;
-		}
-	}
-
-	&.asDrawer {
-		padding: 12px 0 max(env(safe-area-inset-bottom, 0px), 12px) 0;
-		width: 100%;
-		border-radius: 24px;
-		border-bottom-right-radius: 0;
-		border-bottom-left-radius: 0;
-
-		> .item {
-			font-size: 1em;
-			padding: 12px 24px;
-
-			&::before {
-				width: calc(100% - 24px);
-				border-radius: 12px;
-			}
-
-			> .icon {
-				margin-right: 14px;
-				width: 24px;
-			}
-		}
-
-		> .divider {
-			margin: 12px 0;
-		}
-	}
 }
 
 .item {
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index f8032f9b43..c766a33823 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -106,7 +106,7 @@ const zIndex = os.claimZIndex(props.zPriority);
 const useSendAnime = ref(false);
 const type = computed<ModalTypes>(() => {
 	if (props.preferType === 'auto') {
-		if (!defaultStore.state.disableDrawer && isTouchUsing && deviceKind === 'smartphone') {
+		if ((defaultStore.state.menuStyle === 'drawer') || (defaultStore.state.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) {
 			return 'drawer';
 		} else {
 			return props.src != null ? 'popup' : 'dialog';
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 69238b0436..1bfdfd0e76 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -17,13 +17,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</template>
 	</MkSelect>
 
-	<MkRadios v-model="hemisphere">
-		<template #label>{{ i18n.ts.hemisphere }}</template>
-		<option value="N">{{ i18n.ts._hemisphere.N }}</option>
-		<option value="S">{{ i18n.ts._hemisphere.S }}</option>
-		<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
-	</MkRadios>
-
 	<MkRadios v-model="overridedDeviceKind">
 		<template #label>{{ i18n.ts.overridedDeviceKind }}</template>
 		<option :value="null">{{ i18n.ts.auto }}</option>
@@ -132,11 +125,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkSwitch v-model="squareAvatars">{{ i18n.ts.squareAvatars }}</MkSwitch>
 				<MkSwitch v-model="showAvatarDecorations">{{ i18n.ts.showAvatarDecorations }}</MkSwitch>
 				<MkSwitch v-model="useSystemFont">{{ i18n.ts.useSystemFont }}</MkSwitch>
-				<MkSwitch v-model="disableDrawer">{{ i18n.ts.disableDrawer }}</MkSwitch>
 				<MkSwitch v-model="forceShowAds">{{ i18n.ts.forceShowAds }}</MkSwitch>
 				<MkSwitch v-model="enableSeasonalScreenEffect">{{ i18n.ts.seasonalScreenEffect }}</MkSwitch>
 				<MkSwitch v-model="useNativeUIForVideoAudioPlayer">{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</MkSwitch>
 			</div>
+
+			<MkSelect v-model="menuStyle">
+				<template #label>{{ i18n.ts.menuStyle }}</template>
+				<option value="auto">{{ i18n.ts.auto }}</option>
+				<option value="popup">{{ i18n.ts.popup }}</option>
+				<option value="drawer">{{ i18n.ts.drawer }}</option>
+			</MkSelect>
+
 			<div>
 				<MkRadios v-model="emojiStyle">
 					<template #label>{{ i18n.ts.emojiStyle }}</template>
@@ -225,6 +225,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #label>{{ i18n.ts.other }}</template>
 
 		<div class="_gaps">
+			<MkRadios v-model="hemisphere">
+				<template #label>{{ i18n.ts.hemisphere }}</template>
+				<option value="N">{{ i18n.ts._hemisphere.N }}</option>
+				<option value="S">{{ i18n.ts._hemisphere.S }}</option>
+				<template #caption>{{ i18n.ts._hemisphere.caption }}</template>
+			</MkRadios>
 			<MkFolder>
 				<template #label>{{ i18n.ts.additionalEmojiDictionary }}</template>
 				<div class="_buttons">
@@ -244,6 +250,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
+import { langs } from '@@/js/config.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkRadios from '@/components/MkRadios.vue';
@@ -254,7 +261,6 @@ import FormSection from '@/components/form/section.vue';
 import FormLink from '@/components/form/link.vue';
 import MkLink from '@/components/MkLink.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { langs } from '@@/js/config.js';
 import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/scripts/misskey-api.js';
@@ -287,7 +293,7 @@ const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
 const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount'));
 const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
 const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
-const disableDrawer = computed(defaultStore.makeGetterSetter('disableDrawer'));
+const menuStyle = computed(defaultStore.makeGetterSetter('menuStyle'));
 const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
 const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
 const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index 1552a7afee..f6f3b933c6 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -39,6 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, onUnmounted, ref } from 'vue';
 import { v4 as uuid } from 'uuid';
+import { version, host } from '@@/js/config.js';
 import FormSection from '@/components/form/section.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
@@ -49,7 +50,6 @@ import { unisonReload } from '@/scripts/unison-reload.js';
 import { useStream } from '@/stream.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { version, host } from '@@/js/config.js';
 import { definePageMetadata } from '@/scripts/page-metadata.js';
 import { miLocalStorage } from '@/local-storage.js';
 
@@ -75,7 +75,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
 	'dataSaver',
 	'disableShowingAnimatedImages',
 	'emojiStyle',
-	'disableDrawer',
+	'menuStyle',
 	'useBlurEffectForModal',
 	'useBlurEffect',
 	'showFixedPostForm',
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 40615cfc7d..5b10a9a387 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -5,10 +5,12 @@
 
 import { markRaw, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { hemisphere } from '@@/js/intl-const.js';
+import lightTheme from '@@/themes/l-light.json5';
+import darkTheme from '@@/themes/d-green-lime.json5';
 import { miLocalStorage } from './local-storage.js';
 import type { SoundType } from '@/scripts/sound.js';
 import { Storage } from '@/pizzax.js';
-import { hemisphere } from '@@/js/intl-const.js';
 
 interface PostFormAction {
 	title: string,
@@ -250,9 +252,9 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: 'twemoji', // twemoji / fluentEmoji / native
 	},
-	disableDrawer: {
+	menuStyle: {
 		where: 'device',
-		default: false,
+		default: 'auto' as 'auto' | 'popup' | 'drawer',
 	},
 	useBlurEffectForModal: {
 		where: 'device',
@@ -520,8 +522,6 @@ interface Watcher {
 /**
  * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
  */
-import lightTheme from '@@/themes/l-light.json5';
-import darkTheme from '@@/themes/d-green-lime.json5';
 
 export class ColdDeviceStorage {
 	public static default = {