From 9b2af53025eda12262db3633d47940fab28239d8 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 09:02:38 +0900
Subject: [PATCH 01/16] enhance(frontend): improve pref manager

---
 packages/frontend/src/preferences/manager.ts | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts
index 3f3eba6389..f42a9be25f 100644
--- a/packages/frontend/src/preferences/manager.ts
+++ b/packages/frontend/src/preferences/manager.ts
@@ -94,6 +94,7 @@ export type PreferencesDefinition = Record<string, {
 export class ProfileManager {
 	private storageProvider: StorageProvider;
 	public profile: PreferencesProfile;
+	public cloudReady: Promise<void>;
 
 	/**
 	 * static / state の略 (static が予約語のため)
@@ -120,7 +121,7 @@ export class ProfileManager {
 			this.r[key] = ref(this.s[key]);
 		}
 
-		this.fetchCloudValues();
+		this.cloudReady = this.fetchCloudValues();
 
 		// TODO: 定期的にクラウドの値をフェッチ
 	}
@@ -226,7 +227,7 @@ export class ProfileManager {
 			const record = this.getMatchedRecordOf(key);
 			if (record[2].sync && Object.hasOwn(cloudValues, key) && cloudValues[key] !== undefined) {
 				const cloudValue = cloudValues[key];
-				if (cloudValue !== this.s[key]) {
+				if (!deepEqual(cloudValue, record[1])) {
 					this.rewriteRawState(key, cloudValue);
 					record[1] = cloudValue;
 					console.log('cloud fetched', key, cloudValue);

From ce6eba77d9eb9be11d91c982210eb7bd4293e63f Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 09:07:22 +0900
Subject: [PATCH 02/16] =?UTF-8?q?=F0=9F=8E=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/index.d.ts                                         | 2 +-
 locales/ja-JP.yml                                          | 2 +-
 packages/frontend/src/components/MkPreferenceContainer.vue | 2 +-
 3 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/locales/index.d.ts b/locales/index.d.ts
index a4233cf7c8..7efca999b9 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5368,7 +5368,7 @@ export interface Locale extends ILocale {
          */
         "accountData": string;
         /**
-         * アカウントのデータをエクスポート/インポートして管理できます。
+         * アカウントデータのアーカイブをエクスポート/インポートして管理できます。
          */
         "accountDataBanner": string;
         /**
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index c45553817d..96d2439247 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1339,7 +1339,7 @@ _settings:
   serviceConnection: "サービス連携"
   serviceConnectionBanner: "外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。"
   accountData: "アカウントのデータ"
-  accountDataBanner: "アカウントのデータをエクスポート/インポートして管理できます。"
+  accountDataBanner: "アカウントデータのアーカイブをエクスポート/インポートして管理できます。"
   muteAndBlockBanner: "非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。"
   accessibilityBanner: "クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。"
   privacyBanner: "コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。"
diff --git a/packages/frontend/src/components/MkPreferenceContainer.vue b/packages/frontend/src/components/MkPreferenceContainer.vue
index acdd2a8d3b..70b111513c 100644
--- a/packages/frontend/src/components/MkPreferenceContainer.vue
+++ b/packages/frontend/src/components/MkPreferenceContainer.vue
@@ -57,7 +57,7 @@ function showMenu(ev: MouseEvent, contextmenu?: boolean) {
 	display: flex;
 
 	&:hover {
-		&::after {
+		&::before {
 			content: '';
 			position: absolute;
 			top: -8px;

From 8593aa1418e3cbaab9b11df67c9e842b427884c6 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 09:10:09 +0900
Subject: [PATCH 03/16] refactor

---
 packages/frontend/src/preferences.ts         | 14 +++++++-------
 packages/frontend/src/preferences/manager.ts |  2 +-
 2 files changed, 8 insertions(+), 8 deletions(-)

diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
index 7d1821b72b..67ccbbde8d 100644
--- a/packages/frontend/src/preferences.ts
+++ b/packages/frontend/src/preferences.ts
@@ -7,25 +7,25 @@ import { v4 as uuid } from 'uuid';
 import type { PreferencesProfile, StorageProvider } from '@/preferences/manager.js';
 import { cloudBackup } from '@/preferences/utility.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { isSameCond, ProfileManager } from '@/preferences/manager.js';
+import { isSameCond, PreferencesManager } from '@/preferences/manager.js';
 import { store } from '@/store.js';
 import { $i } from '@/account.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 
 const TAB_ID = uuid();
 
-function createProfileManager(storageProvider: StorageProvider) {
+function createPrefManager(storageProvider: StorageProvider) {
 	let profile: PreferencesProfile;
 
 	const savedProfileRaw = miLocalStorage.getItem('preferences');
 	if (savedProfileRaw == null) {
-		profile = ProfileManager.newProfile();
+		profile = PreferencesManager.newProfile();
 		miLocalStorage.setItem('preferences', JSON.stringify(profile));
 	} else {
-		profile = ProfileManager.normalizeProfile(JSON.parse(savedProfileRaw));
+		profile = PreferencesManager.normalizeProfile(JSON.parse(savedProfileRaw));
 	}
 
-	return new ProfileManager(profile, storageProvider);
+	return new PreferencesManager(profile, storageProvider);
 }
 
 const syncGroup = 'default';
@@ -104,7 +104,7 @@ const storageProvider: StorageProvider = {
 	},
 };
 
-export const prefer = createProfileManager(storageProvider);
+export const prefer = createPrefManager(storageProvider);
 
 let latestSyncedAt = Date.now();
 
@@ -118,7 +118,7 @@ function syncBetweenTabs() {
 	if (latestTab === TAB_ID) return;
 	if (latestAt <= latestSyncedAt) return;
 
-	prefer.rewriteProfile(ProfileManager.normalizeProfile(JSON.parse(miLocalStorage.getItem('preferences')!)));
+	prefer.rewriteProfile(PreferencesManager.normalizeProfile(JSON.parse(miLocalStorage.getItem('preferences')!)));
 
 	latestSyncedAt = Date.now();
 
diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts
index f42a9be25f..c3e3ea7ed3 100644
--- a/packages/frontend/src/preferences/manager.ts
+++ b/packages/frontend/src/preferences/manager.ts
@@ -91,7 +91,7 @@ export type PreferencesDefinition = Record<string, {
 	serverDependent?: boolean;
 }>;
 
-export class ProfileManager {
+export class PreferencesManager {
 	private storageProvider: StorageProvider;
 	public profile: PreferencesProfile;
 	public cloudReady: Promise<void>;

From 6b69588c036783e1563827c3d50002971eb0bb75 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 09:24:15 +0900
Subject: [PATCH 04/16] enhance(frontend): improve deck setting page

---
 packages/frontend/src/pages/settings/deck.vue | 39 +++++++++++++++----
 .../utility/autogen/settings-search-index.ts  | 22 +++++++++++
 2 files changed, 53 insertions(+), 8 deletions(-)

diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index 2c4ec01344..03c0cb16f8 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -8,17 +8,39 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="_gaps_m">
 		<MkSwitch :modelValue="profilesSyncEnabled" @update:modelValue="changeProfilesSyncEnabled">{{ i18n.ts._deck.enableSyncBetweenDevicesForProfiles }}</MkSwitch>
 
-		<MkSwitch v-model="useSimpleUiForNonRootPages">{{ i18n.ts._deck.useSimpleUiForNonRootPages }}</MkSwitch>
+		<SearchMarker :keywords="['ui', 'root', 'page']">
+			<MkPreferenceContainer k="deck.useSimpleUiForNonRootPages">
+				<MkSwitch v-model="useSimpleUiForNonRootPages">
+					<template #label><SearchLabel>{{ i18n.ts._deck.useSimpleUiForNonRootPages }}</SearchLabel></template>
+				</MkSwitch>
+			</MkPreferenceContainer>
+		</SearchMarker>
 
-		<MkSwitch v-model="navWindow">{{ i18n.ts.defaultNavigationBehaviour }}: {{ i18n.ts.openInWindow }}</MkSwitch>
+		<SearchMarker :keywords="['default', 'navigation', 'behaviour', 'window']">
+			<MkPreferenceContainer k="deck.navWindow">
+				<MkSwitch v-model="navWindow">
+					<template #label><SearchLabel>{{ i18n.ts.defaultNavigationBehaviour }}</SearchLabel>: {{ i18n.ts.openInWindow }}</template>
+				</MkSwitch>
+			</MkPreferenceContainer>
+		</SearchMarker>
 
-		<MkSwitch v-model="alwaysShowMainColumn">{{ i18n.ts._deck.alwaysShowMainColumn }}</MkSwitch>
+		<SearchMarker :keywords="['always', 'show', 'main', 'column']">
+			<MkPreferenceContainer k="deck.alwaysShowMainColumn">
+				<MkSwitch v-model="alwaysShowMainColumn">
+					<template #label><SearchLabel>{{ i18n.ts._deck.alwaysShowMainColumn }}</SearchLabel></template>
+				</MkSwitch>
+			</MkPreferenceContainer>
+		</SearchMarker>
 
-		<MkRadios v-model="columnAlign">
-			<template #label>{{ i18n.ts._deck.columnAlign }}</template>
-			<option value="left">{{ i18n.ts.left }}</option>
-			<option value="center">{{ i18n.ts.center }}</option>
-		</MkRadios>
+		<SearchMarker :keywords="['column', 'align']">
+			<MkPreferenceContainer k="deck.columnAlign">
+				<MkRadios v-model="columnAlign">
+					<template #label>{{ i18n.ts._deck.columnAlign }}</template>
+					<option value="left">{{ i18n.ts.left }}</option>
+					<option value="center">{{ i18n.ts.center }}</option>
+				</MkRadios>
+			</MkPreferenceContainer>
+		</SearchMarker>
 	</div>
 </SearchMarker>
 </template>
@@ -30,6 +52,7 @@ import MkRadios from '@/components/MkRadios.vue';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { prefer } from '@/preferences.js';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 
 const navWindow = prefer.model('deck.navWindow');
 const useSimpleUiForNonRootPages = prefer.model('deck.useSimpleUiForNonRootPages');
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index d0bce9a522..f5a72a8d63 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -606,6 +606,28 @@ export const searchIndexes: SearchIndexItem[] = [
 	},
 	{
 		id: 'FfZdOs8y',
+		children: [
+			{
+				id: 'lVlkdP4zN',
+				label: i18n.ts._deck.useSimpleUiForNonRootPages,
+				keywords: ['ui', 'root', 'page'],
+			},
+			{
+				id: 'avgxEYgsi',
+				label: i18n.ts.defaultNavigationBehaviour,
+				keywords: ['default', 'navigation', 'behaviour', 'window'],
+			},
+			{
+				id: 'ma7OSw5JK',
+				label: i18n.ts._deck.alwaysShowMainColumn,
+				keywords: ['always', 'show', 'main', 'column'],
+			},
+			{
+				id: 'jjTlUDhJH',
+				label: 'Unnamed marker',
+				keywords: ['column', 'align'],
+			},
+		],
 		label: i18n.ts.deck,
 		keywords: ['deck', 'ui'],
 		path: '/settings/deck',

From 30005ba959e12d123c8686dc48707a53434eb353 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 09:26:04 +0900
Subject: [PATCH 05/16] enhance(frontend): tweak search index

---
 packages/frontend/src/pages/settings/account-data.vue          | 2 +-
 packages/frontend/src/utility/autogen/settings-search-index.ts | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/pages/settings/account-data.vue b/packages/frontend/src/pages/settings/account-data.vue
index 8df545a2a3..7e7036a8dc 100644
--- a/packages/frontend/src/pages/settings/account-data.vue
+++ b/packages/frontend/src/pages/settings/account-data.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<SearchMarker path="/settings/account-data" :label="i18n.ts._settings.accountData" :keywords="['import', 'export', 'data']" icon="ti ti-package">
+<SearchMarker path="/settings/account-data" :label="i18n.ts._settings.accountData" :keywords="['import', 'export', 'data', 'archive']" icon="ti ti-package">
 	<div class="_gaps_m">
 		<MkFeatureBanner icon="/client-assets/package_3d.png" color="#ff9100">
 			<SearchKeyword>{{ i18n.ts._settings.accountDataBanner }}</SearchKeyword>
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index f5a72a8d63..52100ab639 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -817,7 +817,7 @@ export const searchIndexes: SearchIndexItem[] = [
 			},
 		],
 		label: i18n.ts._settings.accountData,
-		keywords: ['import', 'export', 'data', i18n.ts._settings.accountDataBanner],
+		keywords: ['import', 'export', 'data', 'archive', i18n.ts._settings.accountDataBanner],
 		path: '/settings/account-data',
 		icon: 'ti ti-package',
 	},

From 010ec113c277252b8eb394357440106f3c06aca0 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 13:45:23 +0900
Subject: [PATCH 06/16] refactor(frontend): cond -> scope

---
 packages/frontend/src/preferences.ts         | 10 ++--
 packages/frontend/src/preferences/manager.ts | 62 ++++++++++----------
 2 files changed, 36 insertions(+), 36 deletions(-)

diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
index 67ccbbde8d..8785acd7d1 100644
--- a/packages/frontend/src/preferences.ts
+++ b/packages/frontend/src/preferences.ts
@@ -7,7 +7,7 @@ import { v4 as uuid } from 'uuid';
 import type { PreferencesProfile, StorageProvider } from '@/preferences/manager.js';
 import { cloudBackup } from '@/preferences/utility.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { isSameCond, PreferencesManager } from '@/preferences/manager.js';
+import { isSameScope, PreferencesManager } from '@/preferences/manager.js';
 import { store } from '@/store.js';
 import { $i } from '@/account.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
@@ -44,7 +44,7 @@ const storageProvider: StorageProvider = {
 				scope: ['client', 'preferences', 'sync'],
 				key: syncGroup + ':' + ctx.key,
 			}) as [any, any][];
-			const target = cloudData.find(([cond]) => isSameCond(cond, ctx.cond));
+			const target = cloudData.find(([scope]) => isSameScope(scope, ctx.scope));
 			if (target == null) return null;
 			return {
 				value: target[1],
@@ -73,12 +73,12 @@ const storageProvider: StorageProvider = {
 			}
 		}
 
-		const i = cloudData.findIndex(([cond]) => isSameCond(cond, ctx.cond));
+		const i = cloudData.findIndex(([scope]) => isSameScope(scope, ctx.scope));
 
 		if (i === -1) {
-			cloudData.push([ctx.cond, ctx.value]);
+			cloudData.push([ctx.scope, ctx.value]);
 		} else {
-			cloudData[i] = [ctx.cond, ctx.value];
+			cloudData[i] = [ctx.scope, ctx.value];
 		}
 
 		await misskeyApi('i/registry/set', {
diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts
index c3e3ea7ed3..b053cadacb 100644
--- a/packages/frontend/src/preferences/manager.ts
+++ b/packages/frontend/src/preferences/manager.ts
@@ -25,7 +25,7 @@ type PREF = typeof PREF_DEF;
 type ValueOf<K extends keyof PREF> = PREF[K]['default'];
 type Account = string; // <host>/<userId>
 
-type Cond = Partial<{
+type Scope = Partial<{
 	server: string | null; // 将来のため
 	account: Account | null;
 	device: string | null; // 将来のため
@@ -35,33 +35,33 @@ type ValueMeta = Partial<{
 	sync: boolean;
 }>;
 
-type PrefRecord<K extends keyof PREF> = [cond: Cond, value: ValueOf<K>, meta: ValueMeta];
+type PrefRecord<K extends keyof PREF> = [scope: Scope, value: ValueOf<K>, meta: ValueMeta];
 
-function parseCond(cond: Cond): {
+function parseScope(scope: Scope): {
 	server: string | null;
 	account: Account | null;
 	device: string | null;
 } {
 	return {
-		server: cond.server ?? null,
-		account: cond.account ?? null,
-		device: cond.device ?? null,
+		server: scope.server ?? null,
+		account: scope.account ?? null,
+		device: scope.device ?? null,
 	};
 }
 
-function makeCond(cond: Partial<{
+function makeScope(scope: Partial<{
 	server: string | null;
 	account: Account | null;
 	device: string | null;
-}>): Cond {
-	const c = {} as Cond;
-	if (cond.server != null) c.server = cond.server;
-	if (cond.account != null) c.account = cond.account;
-	if (cond.device != null) c.device = cond.device;
+}>): Scope {
+	const c = {} as Scope;
+	if (scope.server != null) c.server = scope.server;
+	if (scope.account != null) c.account = scope.account;
+	if (scope.device != null) c.device = scope.device;
 	return c;
 }
 
-export function isSameCond(a: Cond, b: Cond): boolean {
+export function isSameScope(a: Scope, b: Scope): boolean {
 	// null と undefined (キー無し) は区別したくないので == で比較
 	// eslint-disable-next-line eqeqeq
 	return a.server == b.server && a.account == b.account && a.device == b.device;
@@ -80,9 +80,9 @@ export type PreferencesProfile = {
 
 export type StorageProvider = {
 	save: (ctx: { profile: PreferencesProfile; }) => void;
-	cloudGets: <K extends keyof PREF>(ctx: { needs: { key: K; cond: Cond; }[] }) => Promise<Partial<Record<K, ValueOf<K>>>>;
-	cloudGet: <K extends keyof PREF>(ctx: { key: K; cond: Cond; }) => Promise<{ value: ValueOf<K>; } | null>;
-	cloudSet: <K extends keyof PREF>(ctx: { key: K; cond: Cond; value: ValueOf<K>; }) => Promise<void>;
+	cloudGets: <K extends keyof PREF>(ctx: { needs: { key: K; scope: Scope; }[] }) => Promise<Partial<Record<K, ValueOf<K>>>>;
+	cloudGet: <K extends keyof PREF>(ctx: { key: K; scope: Scope; }) => Promise<{ value: ValueOf<K>; } | null>;
+	cloudSet: <K extends keyof PREF>(ctx: { key: K; scope: Scope; value: ValueOf<K>; }) => Promise<void>;
 };
 
 export type PreferencesDefinition = Record<string, {
@@ -141,8 +141,8 @@ export class PreferencesManager {
 		this.rewriteRawState(key, value);
 
 		const record = this.getMatchedRecordOf(key);
-		if (parseCond(record[0]).account == null && this.isAccountDependentKey(key)) {
-			this.profile.preferences[key].push([makeCond({
+		if (parseScope(record[0]).account == null && this.isAccountDependentKey(key)) {
+			this.profile.preferences[key].push([makeScope({
 				account: `${host}/${$i!.id}`,
 			}), value, {}]);
 			this.save();
@@ -155,7 +155,7 @@ export class PreferencesManager {
 		if (record[2].sync) {
 			// awaitの必要なし
 			// TODO: リクエストを間引く
-			this.storageProvider.cloudSet({ key, cond: record[0], value: record[1] });
+			this.storageProvider.cloudSet({ key, scope: record[0], value: record[1] });
 		}
 	}
 
@@ -208,14 +208,14 @@ export class PreferencesManager {
 	}
 
 	private async fetchCloudValues() {
-		const needs = [] as { key: keyof PREF; cond: Cond; }[];
+		const needs = [] as { key: keyof PREF; scope: Scope; }[];
 		for (const _key in PREF_DEF) {
 			const key = _key as keyof PREF;
 			const record = this.getMatchedRecordOf(key);
 			if (record[2].sync) {
 				needs.push({
 					key,
-					cond: record[0],
+					scope: record[0],
 				});
 			}
 		}
@@ -242,7 +242,7 @@ export class PreferencesManager {
 	public static newProfile(): PreferencesProfile {
 		const data = {} as PreferencesProfile['preferences'];
 		for (const key in PREF_DEF) {
-			data[key] = [[makeCond({}), PREF_DEF[key].default, {}]];
+			data[key] = [[makeScope({}), PREF_DEF[key].default, {}]];
 		}
 		return {
 			id: uuid(),
@@ -259,7 +259,7 @@ export class PreferencesManager {
 		for (const key in PREF_DEF) {
 			const records = profileLike.preferences[key];
 			if (records == null || records.length === 0) {
-				data[key] = [[makeCond({}), PREF_DEF[key].default, {}]];
+				data[key] = [[makeScope({}), PREF_DEF[key].default, {}]];
 				continue;
 			} else {
 				data[key] = records;
@@ -289,18 +289,18 @@ export class PreferencesManager {
 	public getMatchedRecordOf<K extends keyof PREF>(key: K): PrefRecord<K> {
 		const records = this.profile.preferences[key];
 
-		if ($i == null) return records.find(([cond, v]) => parseCond(cond).account == null)!;
+		if ($i == null) return records.find(([scope, v]) => parseScope(scope).account == null)!;
 
-		const accountOverrideRecord = records.find(([cond, v]) => parseCond(cond).account === `${host}/${$i!.id}`);
+		const accountOverrideRecord = records.find(([scope, v]) => parseScope(scope).account === `${host}/${$i!.id}`);
 		if (accountOverrideRecord) return accountOverrideRecord;
 
-		const record = records.find(([cond, v]) => parseCond(cond).account == null);
+		const record = records.find(([scope, v]) => parseScope(scope).account == null);
 		return record!;
 	}
 
 	public isAccountOverrided<K extends keyof PREF>(key: K): boolean {
 		if ($i == null) return false;
-		return this.profile.preferences[key].some(([cond, v]) => parseCond(cond).account === `${host}/${$i!.id}`) ?? false;
+		return this.profile.preferences[key].some(([scope, v]) => parseScope(scope).account === `${host}/${$i!.id}`) ?? false;
 	}
 
 	public setAccountOverride<K extends keyof PREF>(key: K) {
@@ -309,7 +309,7 @@ export class PreferencesManager {
 		if (this.isAccountOverrided(key)) return;
 
 		const records = this.profile.preferences[key];
-		records.push([makeCond({
+		records.push([makeScope({
 			account: `${host}/${$i!.id}`,
 		}), this.s[key], {}]);
 
@@ -322,7 +322,7 @@ export class PreferencesManager {
 
 		const records = this.profile.preferences[key];
 
-		const index = records.findIndex(([cond, v]) => parseCond(cond).account === `${host}/${$i!.id}`);
+		const index = records.findIndex(([scope, v]) => parseScope(scope).account === `${host}/${$i!.id}`);
 		if (index === -1) return;
 
 		records.splice(index, 1);
@@ -341,7 +341,7 @@ export class PreferencesManager {
 
 		const record = this.getMatchedRecordOf(key);
 
-		const existing = await this.storageProvider.cloudGet({ key, cond: record[0] });
+		const existing = await this.storageProvider.cloudGet({ key, scope: record[0] });
 		if (existing != null && !deepEqual(existing.value, record[1])) {
 			const { canceled, result } = await os.select({
 				title: i18n.ts.preferenceSyncConflictTitle,
@@ -371,7 +371,7 @@ export class PreferencesManager {
 		this.save();
 
 		// awaitの必要性は無い
-		this.storageProvider.cloudSet({ key, cond: record[0], value: this.s[key] });
+		this.storageProvider.cloudSet({ key, scope: record[0], value: this.s[key] });
 
 		return { enabled: true };
 	}

From 3ced310f77c2511366c421314a024885810a9082 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 14:05:04 +0900
Subject: [PATCH 07/16] refactor(frontend): organize use functions

---
 packages/frontend/src/components/MkChart.vue                  | 2 +-
 packages/frontend/src/components/MkHeatmap.vue                | 2 +-
 packages/frontend/src/components/MkInstanceStats.vue          | 2 +-
 packages/frontend/src/components/MkLink.vue                   | 2 +-
 packages/frontend/src/components/MkNote.vue                   | 4 ++--
 packages/frontend/src/components/MkNoteDetailed.vue           | 4 ++--
 packages/frontend/src/components/MkReactionIcon.vue           | 2 +-
 .../frontend/src/components/MkReactionsViewer.reaction.vue    | 2 +-
 packages/frontend/src/components/MkRetentionHeatmap.vue       | 2 +-
 packages/frontend/src/components/MkRetentionLineChart.vue     | 2 +-
 .../src/components/MkVisitorDashboard.ActiveUsersChart.vue    | 2 +-
 packages/frontend/src/components/global/MkUrl.vue             | 2 +-
 packages/frontend/src/components/grid/MkDataCell.vue          | 2 +-
 packages/frontend/src/pages/admin/bot-protection.vue          | 2 +-
 packages/frontend/src/pages/admin/overview.active-users.vue   | 2 +-
 packages/frontend/src/pages/admin/overview.ap-requests.vue    | 2 +-
 packages/frontend/src/pages/admin/overview.federation.vue     | 2 +-
 packages/frontend/src/pages/admin/overview.pie.vue            | 2 +-
 packages/frontend/src/pages/admin/overview.queue.chart.vue    | 2 +-
 packages/frontend/src/pages/admin/performance.vue             | 2 +-
 packages/frontend/src/pages/admin/queue.chart.chart.vue       | 2 +-
 packages/frontend/src/pages/admin/security.vue                | 2 +-
 packages/frontend/src/pages/admin/settings.vue                | 2 +-
 packages/frontend/src/pages/theme-editor.vue                  | 2 +-
 packages/frontend/src/pages/user/activity.following.vue       | 2 +-
 packages/frontend/src/pages/user/activity.notes.vue           | 2 +-
 packages/frontend/src/pages/user/activity.pv.vue              | 2 +-
 packages/frontend/src/{utility => use}/use-chart-tooltip.ts   | 0
 packages/frontend/src/{utility => use}/use-form.ts            | 0
 packages/frontend/src/{utility => use}/use-leave-guard.ts     | 0
 packages/frontend/src/{utility => use}/use-note-capture.ts    | 0
 packages/frontend/src/{utility => use}/use-tooltip.ts         | 0
 32 files changed, 29 insertions(+), 29 deletions(-)
 rename packages/frontend/src/{utility => use}/use-chart-tooltip.ts (100%)
 rename packages/frontend/src/{utility => use}/use-form.ts (100%)
 rename packages/frontend/src/{utility => use}/use-leave-guard.ts (100%)
 rename packages/frontend/src/{utility => use}/use-note-capture.ts (100%)
 rename packages/frontend/src/{utility => use}/use-tooltip.ts (100%)

diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index c1a55906ae..d9f4558014 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -55,7 +55,7 @@ import { Chart } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { chartVLine } from '@/utility/chart-vline.js';
 import { alpha } from '@/utility/color.js';
 import date from '@/filters/date.js';
diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue
index 8339e68b07..8a84bfd541 100644
--- a/packages/frontend/src/components/MkHeatmap.vue
+++ b/packages/frontend/src/components/MkHeatmap.vue
@@ -18,7 +18,7 @@ import { Chart } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { alpha } from '@/utility/color.js';
 import { initChart } from '@/utility/init-chart.js';
 
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 9d475bc8aa..70777bb89a 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -88,7 +88,7 @@ import { onMounted, ref, computed, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkChart from '@/components/MkChart.vue';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { $i } from '@/account.js';
 import * as os from '@/os.js';
 import { misskeyApiGet } from '@/utility/misskey-api.js';
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index 140fc9816c..4bac2bcea4 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent, ref } from 'vue';
 import { url as local } from '@@/js/config.js';
-import { useTooltip } from '@/utility/use-tooltip.js';
+import { useTooltip } from '@/use/use-tooltip.js';
 import * as os from '@/os.js';
 import { isEnabledUrlPreview } from '@/instance.js';
 import type { MkABehavior } from '@/components/global/MkA.vue';
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index ca778d87de..aa352d5163 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -211,9 +211,9 @@ import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
-import { useNoteCapture } from '@/utility/use-note-capture.js';
+import { useNoteCapture } from '@/use/use-note-capture.js';
 import { deepClone } from '@/utility/clone.js';
-import { useTooltip } from '@/utility/use-tooltip.js';
+import { useTooltip } from '@/use/use-tooltip.js';
 import { claimAchievement } from '@/utility/achievements.js';
 import { getNoteSummary } from '@/utility/get-note-summary.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 3f3dbf6d83..b3f99b702a 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -241,9 +241,9 @@ import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
-import { useNoteCapture } from '@/utility/use-note-capture.js';
+import { useNoteCapture } from '@/use/use-note-capture.js';
 import { deepClone } from '@/utility/clone.js';
-import { useTooltip } from '@/utility/use-tooltip.js';
+import { useTooltip } from '@/use/use-tooltip.js';
 import { claimAchievement } from '@/utility/achievements.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { showMovedDialog } from '@/utility/show-moved-dialog.js';
diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue
index cb25580bc2..7551524246 100644
--- a/packages/frontend/src/components/MkReactionIcon.vue
+++ b/packages/frontend/src/components/MkReactionIcon.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { defineAsyncComponent, shallowRef } from 'vue';
-import { useTooltip } from '@/utility/use-tooltip.js';
+import { useTooltip } from '@/use/use-tooltip.js';
 import * as os from '@/os.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index f35c938d02..d079e68cde 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -26,7 +26,7 @@ import XDetails from '@/components/MkReactionsViewer.details.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import * as os from '@/os.js';
 import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
-import { useTooltip } from '@/utility/use-tooltip.js';
+import { useTooltip } from '@/use/use-tooltip.js';
 import { $i } from '@/account.js';
 import MkReactionEffect from '@/components/MkReactionEffect.vue';
 import { claimAchievement } from '@/utility/achievements.js';
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index c53bf98f67..a962b37e4a 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -17,7 +17,7 @@ import { onMounted, nextTick, shallowRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { alpha } from '@/utility/color.js';
 import { initChart } from '@/utility/init-chart.js';
 
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index 9e03be3e7f..6fa19efed5 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -12,7 +12,7 @@ import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
 import tinycolor from 'tinycolor2';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { chartVLine } from '@/utility/chart-vline.js';
 import { alpha } from '@/utility/color.js';
 import { initChart } from '@/utility/init-chart.js';
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index 872d4201bb..4168876f08 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -19,7 +19,7 @@ import gradient from 'chartjs-plugin-gradient';
 import tinycolor from 'tinycolor2';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { chartVLine } from '@/utility/chart-vline.js';
 import { initChart } from '@/utility/init-chart.js';
 
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index d28db0d0ca..0c248b041d 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -29,7 +29,7 @@ import { defineAsyncComponent, ref } from 'vue';
 import { toUnicode as decodePunycode } from 'punycode.js';
 import { url as local } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { useTooltip } from '@/utility/use-tooltip.js';
+import { useTooltip } from '@/use/use-tooltip.js';
 import { isEnabledUrlPreview } from '@/instance.js';
 import type { MkABehavior } from '@/components/global/MkA.vue';
 
diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue
index f813bcb73f..f7f6f5c140 100644
--- a/packages/frontend/src/components/grid/MkDataCell.vue
+++ b/packages/frontend/src/components/grid/MkDataCell.vue
@@ -90,7 +90,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script setup lang="ts">
 import { computed, defineAsyncComponent, nextTick, onMounted, onUnmounted, ref, shallowRef, toRefs, watch } from 'vue';
 import { GridEventEmitter } from '@/components/grid/grid.js';
-import { useTooltip } from '@/utility/use-tooltip.js';
+import { useTooltip } from '@/use/use-tooltip.js';
 import * as os from '@/os.js';
 import { equalCellAddress, getCellAddress } from '@/components/grid/grid-utils.js';
 import type { Size } from '@/components/grid/grid.js';
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 59b8435595..2bd734f7d3 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -163,7 +163,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { useForm } from '@/utility/use-form.js';
+import { useForm } from '@/use/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkInfo from '@/components/MkInfo.vue';
diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue
index 7cada9b5be..5c50336b2c 100644
--- a/packages/frontend/src/pages/admin/overview.active-users.vue
+++ b/packages/frontend/src/pages/admin/overview.active-users.vue
@@ -18,7 +18,7 @@ import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { chartVLine } from '@/utility/chart-vline.js';
 import { initChart } from '@/utility/init-chart.js';
 
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue
index bbfe60d205..1949201ca0 100644
--- a/packages/frontend/src/pages/admin/overview.ap-requests.vue
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue
@@ -25,7 +25,7 @@ import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
 import isChromatic from 'chromatic';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { chartVLine } from '@/utility/chart-vline.js';
 import { store } from '@/store.js';
 import { alpha } from '@/utility/color.js';
diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue
index 362f0974e0..6d6d431863 100644
--- a/packages/frontend/src/pages/admin/overview.federation.vue
+++ b/packages/frontend/src/pages/admin/overview.federation.vue
@@ -54,7 +54,7 @@ import { misskeyApiGet } from '@/utility/misskey-api.js';
 import number from '@/filters/number.js';
 import MkNumberDiff from '@/components/MkNumberDiff.vue';
 import { i18n } from '@/i18n.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 
 const topSubInstancesForPie = ref<InstanceForPie[] | null>(null);
 const topPubInstancesForPie = ref<InstanceForPie[] | null>(null);
diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue
index 792565dd96..424bcdd51f 100644
--- a/packages/frontend/src/pages/admin/overview.pie.vue
+++ b/packages/frontend/src/pages/admin/overview.pie.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { initChart } from '@/utility/init-chart.js';
 
 export type InstanceForPie = {
diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue
index 708768a1e4..34c0945ddb 100644
--- a/packages/frontend/src/pages/admin/overview.queue.chart.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { chartVLine } from '@/utility/chart-vline.js';
 import { alpha } from '@/utility/color.js';
 import { initChart } from '@/utility/init-chart.js';
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue
index 8fa2c61613..6bb0918fea 100644
--- a/packages/frontend/src/pages/admin/performance.vue
+++ b/packages/frontend/src/pages/admin/performance.vue
@@ -119,7 +119,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkLink from '@/components/MkLink.vue';
-import { useForm } from '@/utility/use-form.js';
+import { useForm } from '@/use/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 
 const meta = await misskeyApi('admin/meta');
diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue
index 071d4b2f51..9c7a83b1fb 100644
--- a/packages/frontend/src/pages/admin/queue.chart.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.chart.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { chartVLine } from '@/utility/chart-vline.js';
 import { alpha } from '@/utility/color.js';
 import { initChart } from '@/utility/init-chart.js';
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index 85dcec6b2e..13f57b8549 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -135,7 +135,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { useForm } from '@/utility/use-form.js';
+import { useForm } from '@/use/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 
 const meta = await misskeyApi('admin/meta');
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 8c89664671..c019bda32c 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -273,7 +273,7 @@ import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
-import { useForm } from '@/utility/use-form.js';
+import { useForm } from '@/use/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 import MkRadios from '@/components/MkRadios.vue';
 
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index 11971756f4..5ee42cc1ec 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -92,7 +92,7 @@ import * as os from '@/os.js';
 import { store } from '@/store.js';
 import { addTheme } from '@/theme-store.js';
 import { i18n } from '@/i18n.js';
-import { useLeaveGuard } from '@/utility/use-leave-guard.js';
+import { useLeaveGuard } from '@/use/use-leave-guard.js';
 import { definePage } from '@/page.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue
index a86387571a..3b5e9617d5 100644
--- a/packages/frontend/src/pages/user/activity.following.vue
+++ b/packages/frontend/src/pages/user/activity.following.vue
@@ -21,7 +21,7 @@ import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { chartVLine } from '@/utility/chart-vline.js';
 import { initChart } from '@/utility/init-chart.js';
 import { chartLegend } from '@/utility/chart-legend.js';
diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue
index d083fdebed..af8e4d43a6 100644
--- a/packages/frontend/src/pages/user/activity.notes.vue
+++ b/packages/frontend/src/pages/user/activity.notes.vue
@@ -21,7 +21,7 @@ import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { chartVLine } from '@/utility/chart-vline.js';
 import { initChart } from '@/utility/init-chart.js';
 import { chartLegend } from '@/utility/chart-legend.js';
diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue
index d5e8f45608..32e4e78d21 100644
--- a/packages/frontend/src/pages/user/activity.pv.vue
+++ b/packages/frontend/src/pages/user/activity.pv.vue
@@ -21,7 +21,7 @@ import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
-import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 import { chartVLine } from '@/utility/chart-vline.js';
 import { initChart } from '@/utility/init-chart.js';
 import { chartLegend } from '@/utility/chart-legend.js';
diff --git a/packages/frontend/src/utility/use-chart-tooltip.ts b/packages/frontend/src/use/use-chart-tooltip.ts
similarity index 100%
rename from packages/frontend/src/utility/use-chart-tooltip.ts
rename to packages/frontend/src/use/use-chart-tooltip.ts
diff --git a/packages/frontend/src/utility/use-form.ts b/packages/frontend/src/use/use-form.ts
similarity index 100%
rename from packages/frontend/src/utility/use-form.ts
rename to packages/frontend/src/use/use-form.ts
diff --git a/packages/frontend/src/utility/use-leave-guard.ts b/packages/frontend/src/use/use-leave-guard.ts
similarity index 100%
rename from packages/frontend/src/utility/use-leave-guard.ts
rename to packages/frontend/src/use/use-leave-guard.ts
diff --git a/packages/frontend/src/utility/use-note-capture.ts b/packages/frontend/src/use/use-note-capture.ts
similarity index 100%
rename from packages/frontend/src/utility/use-note-capture.ts
rename to packages/frontend/src/use/use-note-capture.ts
diff --git a/packages/frontend/src/utility/use-tooltip.ts b/packages/frontend/src/use/use-tooltip.ts
similarity index 100%
rename from packages/frontend/src/utility/use-tooltip.ts
rename to packages/frontend/src/use/use-tooltip.ts

From 10b67e1b3a8d03c25b8e2c008f11ce45ef3d915c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 16:56:47 +0900
Subject: [PATCH 08/16] enhance(frontend): improve emoji picker settings

---
 locales/index.d.ts                            |  26 ++
 locales/ja-JP.yml                             |   8 +
 packages/frontend/src/boot/main-boot.ts       |  11 +
 packages/frontend/src/components/MkButton.vue |  10 +-
 packages/frontend/src/pages/about-misskey.vue |   4 +-
 packages/frontend/src/pages/settings/deck.vue |   8 +-
 .../pages/settings/emoji-palette.palette.vue  | 166 ++++++++++
 .../src/pages/settings/emoji-palette.vue      | 251 +++++++++++++++
 .../src/pages/settings/emoji-picker.vue       | 288 ------------------
 .../frontend/src/pages/settings/index.vue     |  10 +-
 packages/frontend/src/preferences/def.ts      |  29 +-
 packages/frontend/src/router/definition.ts    |   6 +-
 packages/frontend/src/store.ts                |  16 +-
 .../utility/autogen/settings-search-index.ts  |  66 +++-
 packages/frontend/src/utility/emoji-picker.ts |  13 +-
 .../frontend/src/utility/reaction-picker.ts   |  13 +-
 16 files changed, 601 insertions(+), 324 deletions(-)
 create mode 100644 packages/frontend/src/pages/settings/emoji-palette.palette.vue
 create mode 100644 packages/frontend/src/pages/settings/emoji-palette.vue
 delete mode 100644 packages/frontend/src/pages/settings/emoji-picker.vue

diff --git a/locales/index.d.ts b/locales/index.d.ts
index 7efca999b9..b814bb70e1 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5334,6 +5334,32 @@ export interface Locale extends ILocale {
      * 同期の有効化をキャンセル
      */
     "preferenceSyncConflictChoiceCancel": string;
+    /**
+     * ペースト
+     */
+    "paste": string;
+    /**
+     * 絵文字パレット
+     */
+    "emojiPalette": string;
+    "_emojiPalette": {
+        /**
+         * パレット
+         */
+        "palettes": string;
+        /**
+         * パレットのデバイス間同期を有効にする
+         */
+        "enableSyncBetweenDevicesForPalettes": string;
+        /**
+         * メインで使用するパレット
+         */
+        "paletteForMain": string;
+        /**
+         * リアクションで使用するパレット
+         */
+        "paletteForReaction": string;
+    };
     "_settings": {
         /**
          * ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 96d2439247..b51a839715 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1329,6 +1329,14 @@ preferenceSyncConflictText: "同期が有効にされた設定項目は設定値
 preferenceSyncConflictChoiceServer: "サーバーの設定値"
 preferenceSyncConflictChoiceDevice: "デバイスの設定値"
 preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
+paste: "ペースト"
+emojiPalette: "絵文字パレット"
+
+_emojiPalette:
+  palettes: "パレット"
+  enableSyncBetweenDevicesForPalettes: "パレットのデバイス間同期を有効にする"
+  paletteForMain: "メインで使用するパレット"
+  paletteForReaction: "リアクションで使用するパレット"
 
 _settings:
   driveBanner: "ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。"
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 86efd48c4e..510e0509f4 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -176,6 +176,17 @@ export async function mainBoot() {
 				prefer.commit('lightTheme', ColdDeviceStorage.get('lightTheme'));
 				prefer.commit('darkTheme', ColdDeviceStorage.get('darkTheme'));
 				prefer.commit('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
+				prefer.commit('emojiPalettes', [{
+					id: 'reactions',
+					name: '',
+					emojis: store.s.reactions,
+				}, {
+					id: 'pinnedEmojis',
+					name: '',
+					emojis: store.s.pinnedEmojis,
+				}]);
+				prefer.commit('emojiPaletteForMain', 'pinnedEmojis');
+				prefer.commit('emojiPaletteForReaction', 'reactions');
 				prefer.commit('overridedDeviceKind', store.s.overridedDeviceKind);
 				prefer.commit('widgets', store.s.widgets);
 				prefer.commit('keepCw', store.s.keepCw);
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index 667e624853..5b32ec0fc8 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <button
 	v-if="!link"
 	ref="el" class="_button"
-	:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
+	:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly }]"
 	:type="type"
 	:name="name"
 	:value="value"
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </button>
 <MkA
 	v-else class="_button"
-	:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
+	:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly }]"
 	:to="to ?? '#'"
 	:behavior="linkBehavior"
 	@mousedown="onMousedown"
@@ -57,6 +57,7 @@ const props = defineProps<{
 	name?: string;
 	value?: string;
 	disabled?: boolean;
+	iconOnly?: boolean;
 }>();
 
 const emit = defineEmits<{
@@ -147,6 +148,11 @@ function onMousedown(evt: MouseEvent): void {
 		background: var(--MI_THEME-buttonHoverBg);
 	}
 
+	&.iconOnly {
+		padding: 7px;
+		min-width: auto;
+	}
+
 	&.small {
 		font-size: 90%;
 		padding: 6px 12px;
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 55d703b9ea..5395429d7f 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -143,11 +143,11 @@ import MkInfo from '@/components/MkInfo.vue';
 import { physics } from '@/utility/physics.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { store } from '@/store.js';
 import * as os from '@/os.js';
 import { definePage } from '@/page.js';
 import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
 import { $i } from '@/account.js';
+import { prefer } from '@/preferences.js';
 
 const patronsWithIcon = [{
 	name: 'カイヤン',
@@ -406,7 +406,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null);
 const containerEl = shallowRef<HTMLElement>();
 
 function iconLoaded() {
-	const emojis = store.s.reactions;
+	const emojis = prefer.s.emojiPalettes[0].emojis;
 	const containerWidth = containerEl.value.offsetWidth;
 	for (let i = 0; i < 32; i++) {
 		easterEggEmojis.value.push({
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index 03c0cb16f8..9b2b40374e 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -6,7 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <SearchMarker path="/settings/deck" :label="i18n.ts.deck" :keywords="['deck', 'ui']" icon="ti ti-columns">
 	<div class="_gaps_m">
-		<MkSwitch :modelValue="profilesSyncEnabled" @update:modelValue="changeProfilesSyncEnabled">{{ i18n.ts._deck.enableSyncBetweenDevicesForProfiles }}</MkSwitch>
+		<SearchMarker :keywords="['sync', 'profiles', 'devices']">
+			<MkSwitch :modelValue="profilesSyncEnabled" @update:modelValue="changeProfilesSyncEnabled">
+				<template #label><SearchLabel>{{ i18n.ts._deck.enableSyncBetweenDevicesForProfiles }}</SearchLabel></template>
+			</MkSwitch>
+		</SearchMarker>
 
 		<SearchMarker :keywords="['ui', 'root', 'page']">
 			<MkPreferenceContainer k="deck.useSimpleUiForNonRootPages">
@@ -35,7 +39,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<SearchMarker :keywords="['column', 'align']">
 			<MkPreferenceContainer k="deck.columnAlign">
 				<MkRadios v-model="columnAlign">
-					<template #label>{{ i18n.ts._deck.columnAlign }}</template>
+					<template #label><SearchLabel>{{ i18n.ts._deck.columnAlign }}</SearchLabel></template>
 					<option value="left">{{ i18n.ts.left }}</option>
 					<option value="center">{{ i18n.ts.center }}</option>
 				</MkRadios>
diff --git a/packages/frontend/src/pages/settings/emoji-palette.palette.vue b/packages/frontend/src/pages/settings/emoji-palette.palette.vue
new file mode 100644
index 0000000000..33d1d7c9fa
--- /dev/null
+++ b/packages/frontend/src/pages/settings/emoji-palette.palette.vue
@@ -0,0 +1,166 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<MkFolder :defaultOpen="true">
+	<template #icon><i class="ti ti-palette"></i></template>
+	<template #label>{{ palette.name === '' ? '(' + i18n.ts.noName + ')' : palette.name }}</template>
+	<template #footer>
+		<div class="_buttons">
+			<MkButton @click="rename"><i class="ti ti-pencil"></i> {{ i18n.ts.rename }}</MkButton>
+			<MkButton @click="copy"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+			<MkButton danger @click="paste"><i class="ti ti-clipboard"></i> {{ i18n.ts.paste }}</MkButton>
+			<MkButton danger iconOnly style="margin-left: auto;" @click="del"><i class="ti ti-trash"></i></MkButton>
+		</div>
+	</template>
+
+	<div>
+		<div v-panel style="border-radius: 6px;">
+			<Sortable
+				v-model="emojis"
+				:class="$style.emojis"
+				:itemKey="item => item"
+				:animation="150"
+				:delay="100"
+				:delayOnTouchOnly="true"
+				:group="{ name: 'SortableEmojiPalettes' }"
+			>
+				<template #item="{element}">
+					<button class="_button" :class="$style.emojisItem" @click="remove(element, $event)">
+						<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true" :fallbackToImage="true"/>
+						<MkEmoji v-else :emoji="element" :normal="true"/>
+					</button>
+				</template>
+				<template #footer>
+					<button class="_button" :class="$style.emojisAdd" @click="pick">
+						<i class="ti ti-plus"></i>
+					</button>
+				</template>
+			</Sortable>
+		</div>
+		<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
+	</div>
+</MkFolder>
+</template>
+
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
+import Sortable from 'vuedraggable';
+import MkButton from '@/components/MkButton.vue';
+import * as os from '@/os.js';
+import { i18n } from '@/i18n.js';
+import { deepClone } from '@/utility/clone.js';
+import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
+import MkEmoji from '@/components/global/MkEmoji.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+
+const props = defineProps<{
+	palette: {
+		id: string;
+		name: string;
+		emojis: string[];
+	};
+}>();
+
+const emit = defineEmits<{
+	(ev: 'updateEmojis', emojis: string[]): void,
+	(ev: 'updateName', name: string): void,
+	(ev: 'del'): void,
+}>();
+
+const emojis = ref<string[]>(deepClone(props.palette.emojis));
+
+watch(emojis, () => {
+	emit('updateEmojis', emojis.value);
+}, { deep: true });
+
+function remove(reaction: string, ev: MouseEvent) {
+	os.popupMenu([{
+		text: i18n.ts.remove,
+		action: () => {
+			emojis.value = emojis.value.filter(x => x !== reaction);
+		},
+	}], getHTMLElement(ev));
+}
+
+function pick(ev: MouseEvent) {
+	os.pickEmoji(getHTMLElement(ev), {
+		showPinned: false,
+	}).then(it => {
+		const emoji = it;
+		if (!emojis.value.includes(emoji)) {
+			emojis.value.push(emoji);
+		}
+	});
+}
+
+function getHTMLElement(ev: MouseEvent): HTMLElement {
+	const target = ev.currentTarget ?? ev.target;
+	return target as HTMLElement;
+}
+
+function rename() {
+	os.inputText({
+		title: i18n.ts.rename,
+		default: props.palette.name,
+	}).then(({ canceled, result: name }) => {
+		if (canceled) return;
+		if (name != null) {
+			emit('updateName', name);
+		}
+	});
+}
+
+function copy() {
+	copyToClipboard(emojis.value.join(' '));
+}
+
+function paste() {
+	// TODO: validate
+	navigator.clipboard.readText().then(text => {
+		emojis.value = text.split(' ');
+	});
+}
+
+function del(ev: MouseEvent) {
+	os.popupMenu([{
+		text: i18n.ts.delete,
+		action: () => {
+			emit('del');
+		},
+	}], ev.currentTarget ?? ev.target);
+}
+</script>
+
+<style lang="scss" module>
+.tab {
+	margin: calc(var(--MI-margin) / 2) 0;
+	padding: calc(var(--MI-margin) / 2) 0;
+	background: var(--MI_THEME-bg);
+}
+
+.emojis {
+	padding: 12px;
+	font-size: 1.1em;
+}
+
+.emojisItem {
+	display: inline-block;
+	padding: 8px;
+	cursor: move;
+}
+
+.emojisAdd {
+	display: inline-block;
+	padding: 8px;
+}
+
+.editorCaption {
+	font-size: 0.85em;
+	padding: 8px 0 0 0;
+	color: var(--MI_THEME-fgTransparentWeak);
+}
+</style>
diff --git a/packages/frontend/src/pages/settings/emoji-palette.vue b/packages/frontend/src/pages/settings/emoji-palette.vue
new file mode 100644
index 0000000000..398228e226
--- /dev/null
+++ b/packages/frontend/src/pages/settings/emoji-palette.vue
@@ -0,0 +1,251 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<SearchMarker path="/settings/emoji-palette" :label="i18n.ts.emojiPalette" :keywords="['emoji', 'palette']" icon="ti ti-mood-happy">
+	<div class="_gaps_m">
+		<FormSection first>
+			<template #label>{{ i18n.ts._emojiPalette.palettes }}</template>
+
+			<div class="_gaps_s">
+				<XPalette
+					v-for="palette in prefer.r.emojiPalettes.value"
+					:key="palette.id"
+					:palette="palette"
+					@updateEmojis="emojis => updatePaletteEmojis(palette.id, emojis)"
+					@updateName="name => updatePaletteName(palette.id, name)"
+					@del="delPalette(palette.id)"
+				/>
+				<MkButton primary rounded style="margin: auto;" @click="addPalette"><i class="ti ti-plus"></i></MkButton>
+			</div>
+		</FormSection>
+
+		<FormSection>
+			<div class="_gaps_m">
+				<SearchMarker :keywords="['sync', 'palettes', 'devices']">
+					<MkSwitch :modelValue="palettesSyncEnabled" @update:modelValue="changePalettesSyncEnabled">
+						<template #label><SearchLabel>{{ i18n.ts._emojiPalette.enableSyncBetweenDevicesForPalettes }}</SearchLabel></template>
+					</MkSwitch>
+				</SearchMarker>
+			</div>
+		</FormSection>
+
+		<FormSection>
+			<div class="_gaps_m">
+				<SearchMarker :keywords="['main', 'palette']">
+					<MkPreferenceContainer k="emojiPaletteForMain">
+						<MkSelect v-model="emojiPaletteForMain">
+							<template #label><SearchLabel>{{ i18n.ts._emojiPalette.paletteForMain }}</SearchLabel></template>
+							<option key="-" :value="null">({{ i18n.ts.auto }})</option>
+							<option v-for="palette in prefer.r.emojiPalettes.value" :key="palette.id" :value="palette.id">{{ palette.name === '' ? '(' + i18n.ts.noName + ')' : palette.name }}</option>
+						</MkSelect>
+					</MkPreferenceContainer>
+				</SearchMarker>
+
+				<SearchMarker :keywords="['reaction', 'palette']">
+					<MkPreferenceContainer k="emojiPaletteForReaction">
+						<MkSelect v-model="emojiPaletteForReaction">
+							<template #label><SearchLabel>{{ i18n.ts._emojiPalette.paletteForReaction }}</SearchLabel></template>
+							<option key="-" :value="null">({{ i18n.ts.auto }})</option>
+							<option v-for="palette in prefer.r.emojiPalettes.value" :key="palette.id" :value="palette.id">{{ palette.name === '' ? '(' + i18n.ts.noName + ')' : palette.name }}</option>
+						</MkSelect>
+					</MkPreferenceContainer>
+				</SearchMarker>
+			</div>
+		</FormSection>
+
+		<SearchMarker :keywords="['emoji', 'picker', 'display']">
+			<FormSection>
+				<template #label><SearchLabel>{{ i18n.ts.emojiPickerDisplay }}</SearchLabel></template>
+
+				<div class="_gaps_m">
+					<SearchMarker :keywords="['emoji', 'picker', 'scale', 'size']">
+						<MkPreferenceContainer k="emojiPickerScale">
+							<MkRadios v-model="emojiPickerScale">
+								<template #label><SearchLabel>{{ i18n.ts.size }}</SearchLabel></template>
+								<option :value="1">{{ i18n.ts.small }}</option>
+								<option :value="2">{{ i18n.ts.medium }}</option>
+								<option :value="3">{{ i18n.ts.large }}</option>
+							</MkRadios>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['emoji', 'picker', 'width', 'column', 'size']">
+						<MkPreferenceContainer k="emojiPickerWidth">
+							<MkRadios v-model="emojiPickerWidth">
+								<template #label><SearchLabel>{{ i18n.ts.numberOfColumn }}</SearchLabel></template>
+								<option :value="1">5</option>
+								<option :value="2">6</option>
+								<option :value="3">7</option>
+								<option :value="4">8</option>
+								<option :value="5">9</option>
+							</MkRadios>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['emoji', 'picker', 'height', 'size']">
+						<MkPreferenceContainer k="emojiPickerHeight">
+							<MkRadios v-model="emojiPickerHeight">
+								<template #label><SearchLabel>{{ i18n.ts.height }}</SearchLabel></template>
+								<option :value="1">{{ i18n.ts.small }}</option>
+								<option :value="2">{{ i18n.ts.medium }}</option>
+								<option :value="3">{{ i18n.ts.large }}</option>
+								<option :value="4">{{ i18n.ts.large }}+</option>
+							</MkRadios>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['emoji', 'picker', 'style']">
+						<MkPreferenceContainer k="emojiPickerStyle">
+							<MkSelect v-model="emojiPickerStyle">
+								<template #label><SearchLabel>{{ i18n.ts.style }}</SearchLabel></template>
+								<template #caption>{{ i18n.ts.needReloadToApply }}</template>
+								<option value="auto">{{ i18n.ts.auto }}</option>
+								<option value="popup">{{ i18n.ts.popup }}</option>
+								<option value="drawer">{{ i18n.ts.drawer }}</option>
+							</MkSelect>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<MkButton @click="previewPicker"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
+				</div>
+			</FormSection>
+		</SearchMarker>
+	</div>
+</SearchMarker>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from 'vue';
+import { v4 as uuid } from 'uuid';
+import XPalette from './emoji-palette.palette.vue';
+import MkRadios from '@/components/MkRadios.vue';
+import MkButton from '@/components/MkButton.vue';
+import FormSection from '@/components/form/section.vue';
+import MkSelect from '@/components/MkSelect.vue';
+import * as os from '@/os.js';
+import { i18n } from '@/i18n.js';
+import { definePage } from '@/page.js';
+import MkFolder from '@/components/MkFolder.vue';
+import { prefer } from '@/preferences.js';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
+import MkSwitch from '@/components/MkSwitch.vue';
+import { emojiPicker } from '@/utility/emoji-picker.js';
+
+const emojiPaletteForReaction = prefer.model('emojiPaletteForReaction');
+const emojiPaletteForMain = prefer.model('emojiPaletteForMain');
+const emojiPickerScale = prefer.model('emojiPickerScale');
+const emojiPickerWidth = prefer.model('emojiPickerWidth');
+const emojiPickerHeight = prefer.model('emojiPickerHeight');
+const emojiPickerStyle = prefer.model('emojiPickerStyle');
+
+const palettesSyncEnabled = ref(prefer.isSyncEnabled('emojiPalettes'));
+
+function changePalettesSyncEnabled(value: boolean) {
+	if (value) {
+		prefer.enableSync('emojiPalettes').then((res) => {
+			if (res == null) return;
+			if (res.enabled) palettesSyncEnabled.value = true;
+		});
+	} else {
+		prefer.disableSync('emojiPalettes');
+		palettesSyncEnabled.value = false;
+	}
+}
+
+function addPalette() {
+	prefer.commit('emojiPalettes', [
+		...prefer.s.emojiPalettes,
+		{
+			id: uuid(),
+			name: '',
+			emojis: [],
+		},
+	]);
+}
+
+function updatePaletteEmojis(id: string, emojis: string[]) {
+	prefer.commit('emojiPalettes', prefer.s.emojiPalettes.map(palette => {
+		if (palette.id === id) {
+			return {
+				...palette,
+				emojis,
+			};
+		} else {
+			return palette;
+		}
+	}));
+}
+
+function updatePaletteName(id: string, name: string) {
+	prefer.commit('emojiPalettes', prefer.s.emojiPalettes.map(palette => {
+		if (palette.id === id) {
+			return {
+				...palette,
+				name,
+			};
+		} else {
+			return palette;
+		}
+	}));
+}
+
+function delPalette(id: string) {
+	if (prefer.s.emojiPalettes.length === 1) {
+		addPalette();
+	}
+	prefer.commit('emojiPalettes', prefer.s.emojiPalettes.filter(palette => palette.id !== id));
+	if (prefer.s.emojiPaletteForMain === id) {
+		prefer.commit('emojiPaletteForMain', null);
+	}
+	if (prefer.s.emojiPaletteForReaction === id) {
+		prefer.commit('emojiPaletteForReaction', null);
+	}
+}
+
+function getHTMLElement(ev: MouseEvent): HTMLElement {
+	const target = ev.currentTarget ?? ev.target;
+	return target as HTMLElement;
+}
+
+function previewPicker(ev: MouseEvent) {
+	emojiPicker.show(getHTMLElement(ev));
+}
+
+definePage(() => ({
+	title: i18n.ts.emojiPalette,
+	icon: 'ti ti-mood-happy',
+}));
+</script>
+
+<style lang="scss" module>
+.tab {
+	margin: calc(var(--MI-margin) / 2) 0;
+	padding: calc(var(--MI-margin) / 2) 0;
+	background: var(--MI_THEME-bg);
+}
+
+.emojis {
+  padding: 12px;
+  font-size: 1.1em;
+}
+
+.emojisItem {
+  display: inline-block;
+  padding: 8px;
+  cursor: move;
+}
+
+.emojisAdd {
+  display: inline-block;
+  padding: 8px;
+}
+
+.editorCaption {
+	font-size: 0.85em;
+	padding: 8px 0 0 0;
+	color: var(--MI_THEME-fgTransparentWeak);
+}
+</style>
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
deleted file mode 100644
index d8f27078ae..0000000000
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ /dev/null
@@ -1,288 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<div class="_gaps_m">
-	<MkFolder :defaultOpen="true">
-		<template #icon><i class="ti ti-pin"></i></template>
-		<template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.reaction }})</template>
-		<template #caption>{{ i18n.ts.pinnedEmojisForReactionSettingDescription }}</template>
-
-		<div class="_gaps">
-			<div>
-				<div v-panel style="border-radius: 6px;">
-					<Sortable
-						v-model="pinnedEmojisForReaction"
-						:class="$style.emojis"
-						:itemKey="item => item"
-						:animation="150"
-						:delay="100"
-						:delayOnTouchOnly="true"
-					>
-						<template #item="{element}">
-							<button class="_button" :class="$style.emojisItem" @click="removeReaction(element, $event)">
-								<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true" :fallbackToImage="true"/>
-								<MkEmoji v-else :emoji="element" :normal="true"/>
-							</button>
-						</template>
-						<template #footer>
-							<button class="_button" :class="$style.emojisAdd" @click="chooseReaction">
-								<i class="ti ti-plus"></i>
-							</button>
-						</template>
-					</Sortable>
-				</div>
-				<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
-			</div>
-
-			<div class="_buttons">
-				<MkButton inline @click="previewReaction"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
-				<MkButton inline danger @click="setDefaultReaction"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
-				<MkButton inline danger @click="overwriteFromPinnedEmojis"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojis }}</MkButton>
-			</div>
-		</div>
-	</MkFolder>
-
-	<MkFolder>
-		<template #icon><i class="ti ti-pin"></i></template>
-		<template #label>{{ i18n.ts.pinned }} ({{ i18n.ts.general }})</template>
-		<template #caption>{{ i18n.ts.pinnedEmojisSettingDescription }}</template>
-
-		<div class="_gaps">
-			<div>
-				<div v-panel style="border-radius: 6px;">
-					<Sortable
-						v-model="pinnedEmojis"
-						:class="$style.emojis"
-						:itemKey="item => item"
-						:animation="150"
-						:delay="100"
-						:delayOnTouchOnly="true"
-					>
-						<template #item="{element}">
-							<button class="_button" :class="$style.emojisItem" @click="removeEmoji(element, $event)">
-								<MkCustomEmoji v-if="element[0] === ':'" :name="element" :normal="true" :fallbackToImage="true"/>
-								<MkEmoji v-else :emoji="element" :normal="true"/>
-							</button>
-						</template>
-						<template #footer>
-							<button class="_button" :class="$style.emojisAdd" @click="chooseEmoji">
-								<i class="ti ti-plus"></i>
-							</button>
-						</template>
-					</Sortable>
-				</div>
-				<div :class="$style.editorCaption">{{ i18n.ts.reactionSettingDescription2 }}</div>
-			</div>
-
-			<div class="_buttons">
-				<MkButton inline @click="previewEmoji"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
-				<MkButton inline danger @click="setDefaultEmoji"><i class="ti ti-reload"></i> {{ i18n.ts.default }}</MkButton>
-				<MkButton inline danger @click="overwriteFromPinnedEmojisForReaction"><i class="ti ti-copy"></i> {{ i18n.ts.overwriteFromPinnedEmojisForReaction }}</MkButton>
-			</div>
-		</div>
-	</MkFolder>
-
-	<FormSection>
-		<template #label>{{ i18n.ts.emojiPickerDisplay }}</template>
-
-		<div class="_gaps_m">
-			<MkPreferenceContainer k="emojiPickerScale">
-				<MkRadios v-model="emojiPickerScale">
-					<template #label>{{ i18n.ts.size }}</template>
-					<option :value="1">{{ i18n.ts.small }}</option>
-					<option :value="2">{{ i18n.ts.medium }}</option>
-					<option :value="3">{{ i18n.ts.large }}</option>
-				</MkRadios>
-			</MkPreferenceContainer>
-
-			<MkPreferenceContainer k="emojiPickerWidth">
-				<MkRadios v-model="emojiPickerWidth">
-					<template #label>{{ i18n.ts.numberOfColumn }}</template>
-					<option :value="1">5</option>
-					<option :value="2">6</option>
-					<option :value="3">7</option>
-					<option :value="4">8</option>
-					<option :value="5">9</option>
-				</MkRadios>
-			</MkPreferenceContainer>
-
-			<MkPreferenceContainer k="emojiPickerHeight">
-				<MkRadios v-model="emojiPickerHeight">
-					<template #label>{{ i18n.ts.height }}</template>
-					<option :value="1">{{ i18n.ts.small }}</option>
-					<option :value="2">{{ i18n.ts.medium }}</option>
-					<option :value="3">{{ i18n.ts.large }}</option>
-					<option :value="4">{{ i18n.ts.large }}+</option>
-				</MkRadios>
-			</MkPreferenceContainer>
-
-			<MkPreferenceContainer k="emojiPickerStyle">
-				<MkSelect v-model="emojiPickerStyle">
-					<template #label>{{ i18n.ts.style }}</template>
-					<template #caption>{{ i18n.ts.needReloadToApply }}</template>
-					<option value="auto">{{ i18n.ts.auto }}</option>
-					<option value="popup">{{ i18n.ts.popup }}</option>
-					<option value="drawer">{{ i18n.ts.drawer }}</option>
-				</MkSelect>
-			</MkPreferenceContainer>
-		</div>
-	</FormSection>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { computed, ref, watch } from 'vue';
-import Sortable from 'vuedraggable';
-import type { Ref } from 'vue';
-import MkRadios from '@/components/MkRadios.vue';
-import MkButton from '@/components/MkButton.vue';
-import FormSection from '@/components/form/section.vue';
-import MkSelect from '@/components/MkSelect.vue';
-import * as os from '@/os.js';
-import { store } from '@/store.js';
-import { i18n } from '@/i18n.js';
-import { definePage } from '@/page.js';
-import { deepClone } from '@/utility/clone.js';
-import { reactionPicker } from '@/utility/reaction-picker.js';
-import { emojiPicker } from '@/utility/emoji-picker.js';
-import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
-import MkEmoji from '@/components/global/MkEmoji.vue';
-import MkFolder from '@/components/MkFolder.vue';
-import { prefer } from '@/preferences.js';
-import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
-
-const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(store.s.reactions));
-const pinnedEmojis: Ref<string[]> = ref(deepClone(store.s.pinnedEmojis));
-
-const emojiPickerScale = prefer.model('emojiPickerScale');
-const emojiPickerWidth = prefer.model('emojiPickerWidth');
-const emojiPickerHeight = prefer.model('emojiPickerHeight');
-const emojiPickerStyle = prefer.model('emojiPickerStyle');
-
-const removeReaction = (reaction: string, ev: MouseEvent) => remove(pinnedEmojisForReaction, reaction, ev);
-const chooseReaction = (ev: MouseEvent) => pickEmoji(pinnedEmojisForReaction, ev);
-const setDefaultReaction = () => setDefault(pinnedEmojisForReaction);
-
-const removeEmoji = (reaction: string, ev: MouseEvent) => remove(pinnedEmojis, reaction, ev);
-const chooseEmoji = (ev: MouseEvent) => pickEmoji(pinnedEmojis, ev);
-const setDefaultEmoji = () => setDefault(pinnedEmojis);
-
-function previewReaction(ev: MouseEvent) {
-	reactionPicker.show(getHTMLElement(ev), null);
-}
-
-function previewEmoji(ev: MouseEvent) {
-	emojiPicker.show(getHTMLElement(ev));
-}
-
-async function overwriteFromPinnedEmojis() {
-	const { canceled } = await os.confirm({
-		type: 'warning',
-		text: i18n.ts.overwriteContentConfirm,
-	});
-
-	if (canceled) {
-		return;
-	}
-
-	pinnedEmojisForReaction.value = [...pinnedEmojis.value];
-}
-
-async function overwriteFromPinnedEmojisForReaction() {
-	const { canceled } = await os.confirm({
-		type: 'warning',
-		text: i18n.ts.overwriteContentConfirm,
-	});
-
-	if (canceled) {
-		return;
-	}
-
-	pinnedEmojis.value = [...pinnedEmojisForReaction.value];
-}
-
-function remove(itemsRef: Ref<string[]>, reaction: string, ev: MouseEvent) {
-	os.popupMenu([{
-		text: i18n.ts.remove,
-		action: () => {
-			itemsRef.value = itemsRef.value.filter(x => x !== reaction);
-		},
-	}], getHTMLElement(ev));
-}
-
-async function setDefault(itemsRef: Ref<string[]>) {
-	const { canceled } = await os.confirm({
-		type: 'warning',
-		text: i18n.ts.resetAreYouSure,
-	});
-	if (canceled) return;
-
-	itemsRef.value = deepClone(store.def.reactions.default);
-}
-
-async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) {
-	os.pickEmoji(getHTMLElement(ev), {
-		showPinned: false,
-	}).then(it => {
-		const emoji = it;
-		if (!itemsRef.value.includes(emoji)) {
-			itemsRef.value.push(emoji);
-		}
-	});
-}
-
-function getHTMLElement(ev: MouseEvent): HTMLElement {
-	const target = ev.currentTarget ?? ev.target;
-	return target as HTMLElement;
-}
-
-watch(pinnedEmojisForReaction, () => {
-	store.set('reactions', pinnedEmojisForReaction.value);
-}, {
-	deep: true,
-});
-
-watch(pinnedEmojis, () => {
-	store.set('pinnedEmojis', pinnedEmojis.value);
-}, {
-	deep: true,
-});
-
-definePage(() => ({
-	title: i18n.ts.emojiPicker,
-	icon: 'ti ti-mood-happy',
-}));
-</script>
-
-<style lang="scss" module>
-.tab {
-	margin: calc(var(--MI-margin) / 2) 0;
-	padding: calc(var(--MI-margin) / 2) 0;
-	background: var(--MI_THEME-bg);
-}
-
-.emojis {
-  padding: 12px;
-  font-size: 1.1em;
-}
-
-.emojisItem {
-  display: inline-block;
-  padding: 8px;
-  cursor: move;
-}
-
-.emojisAdd {
-  display: inline-block;
-  padding: 8px;
-}
-
-.editorCaption {
-	font-size: 0.85em;
-	padding: 8px 0 0 0;
-	color: var(--MI_THEME-fgTransparentWeak);
-}
-</style>
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 7bbec82757..debcd4bd3e 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -86,11 +86,6 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
 		text: i18n.ts.privacy,
 		to: '/settings/privacy',
 		active: currentPage.value?.route.name === 'privacy',
-	}, {
-		icon: 'ti ti-mood-happy',
-		text: i18n.ts.emojiPicker,
-		to: '/settings/emoji-picker',
-		active: currentPage.value?.route.name === 'emojiPicker',
 	}, {
 		icon: 'ti ti-bell',
 		text: i18n.ts.notifications,
@@ -118,6 +113,11 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
 		text: i18n.ts.theme,
 		to: '/settings/theme',
 		active: currentPage.value?.route.name === 'theme',
+	}, {
+		icon: 'ti ti-mood-happy',
+		text: i18n.ts.emojiPalette,
+		to: '/settings/emoji-palette',
+		active: currentPage.value?.route.name === 'emoji-palette',
 	}, {
 		icon: 'ti ti-device-desktop',
 		text: i18n.ts.appearance,
diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts
index 47d0ab5cbc..6a926c4b26 100644
--- a/packages/frontend/src/preferences/def.ts
+++ b/packages/frontend/src/preferences/def.ts
@@ -29,6 +29,8 @@ export type SoundStore = {
 	volume: number;
 };
 
+// NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
+
 export const PREF_DEF = {
 	pinnedUserLists: {
 		accountDependent: true,
@@ -56,6 +58,27 @@ export const PREF_DEF = {
 		default: [] as DeckProfile[],
 	},
 
+	emojiPalettes: {
+		serverDependent: true,
+		default: [{
+			id: 'a',
+			name: '',
+			emojis: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
+		}] as {
+			id: string;
+			name: string;
+			emojis: string[];
+		}[],
+	},
+	emojiPaletteForReaction: {
+		serverDependent: true,
+		default: null as string | null,
+	},
+	emojiPaletteForMain: {
+		serverDependent: true,
+		default: null as string | null,
+	},
+
 	overridedDeviceKind: {
 		default: null as DeviceKind | null,
 	},
@@ -180,13 +203,13 @@ export const PREF_DEF = {
 		default: 'remote' as 'none' | 'remote' | 'always',
 	},
 	emojiPickerScale: {
-		default: 1,
+		default: 2,
 	},
 	emojiPickerWidth: {
-		default: 1,
+		default: 2,
 	},
 	emojiPickerHeight: {
-		default: 2,
+		default: 3,
 	},
 	emojiPickerStyle: {
 		default: 'auto' as 'auto' | 'popup' | 'drawer',
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 93dd081127..752356497e 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -66,9 +66,9 @@ const routes: RouteDef[] = [{
 		name: 'privacy',
 		component: page(() => import('@/pages/settings/privacy.vue')),
 	}, {
-		path: '/emoji-picker',
-		name: 'emojiPicker',
-		component: page(() => import('@/pages/settings/emoji-picker.vue')),
+		path: '/emoji-palette',
+		name: 'emoji-palette',
+		component: page(() => import('@/pages/settings/emoji-palette.vue')),
 	}, {
 		path: '/drive',
 		name: 'drive',
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 738a57d233..6eebcd1ead 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -39,14 +39,6 @@ export const store = markRaw(new Storage('base', {
 		where: 'account',
 		default: null,
 	},
-	reactions: {
-		where: 'account',
-		default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
-	},
-	pinnedEmojis: {
-		where: 'account',
-		default: [],
-	},
 	reactionAcceptance: {
 		where: 'account',
 		default: 'nonSensitiveOnly' as 'likeOnly' | 'likeOnlyForRemote' | 'nonSensitiveOnly' | 'nonSensitiveOnlyForLocalLikeOnlyForRemote' | null,
@@ -127,6 +119,14 @@ export const store = markRaw(new Storage('base', {
 	},
 
 	//#region TODO: そのうち消す (preferに移行済み)
+	reactions: {
+		where: 'account',
+		default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
+	},
+	pinnedEmojis: {
+		where: 'account',
+		default: [],
+	},
 	widgets: {
 		where: 'account',
 		default: [] as {
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index 52100ab639..4f1a94f266 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -536,6 +536,57 @@ export const searchIndexes: SearchIndexItem[] = [
 		path: '/settings/mute-block',
 		icon: 'ti ti-ban',
 	},
+	{
+		id: 'yR1OSyLiT',
+		children: [
+			{
+				id: 'yMJzyzOUk',
+				label: i18n.ts._emojiPalette.enableSyncBetweenDevicesForPalettes,
+				keywords: ['sync', 'palettes', 'devices'],
+			},
+			{
+				id: 'wCE09vgZr',
+				label: i18n.ts._emojiPalette.paletteForMain,
+				keywords: ['main', 'palette'],
+			},
+			{
+				id: 'uCzRPrSNx',
+				label: i18n.ts._emojiPalette.paletteForReaction,
+				keywords: ['reaction', 'palette'],
+			},
+			{
+				id: 'hgQr28WUk',
+				children: [
+					{
+						id: 'fY04NIHSQ',
+						label: i18n.ts.size,
+						keywords: ['emoji', 'picker', 'scale', 'size'],
+					},
+					{
+						id: '3j7vlaL7t',
+						label: i18n.ts.numberOfColumn,
+						keywords: ['emoji', 'picker', 'width', 'column', 'size'],
+					},
+					{
+						id: 'zPX8z1Bcy',
+						label: i18n.ts.height,
+						keywords: ['emoji', 'picker', 'height', 'size'],
+					},
+					{
+						id: '2CSkZa4tl',
+						label: i18n.ts.style,
+						keywords: ['emoji', 'picker', 'style'],
+					},
+				],
+				label: i18n.ts.emojiPickerDisplay,
+				keywords: ['emoji', 'picker', 'display'],
+			},
+		],
+		label: i18n.ts.emojiPalette,
+		keywords: ['emoji', 'palette'],
+		path: '/settings/emoji-palette',
+		icon: 'ti ti-mood-happy',
+	},
 	{
 		id: '3Tcxw4Fwl',
 		children: [
@@ -608,23 +659,28 @@ export const searchIndexes: SearchIndexItem[] = [
 		id: 'FfZdOs8y',
 		children: [
 			{
-				id: 'lVlkdP4zN',
+				id: 'B1ZU6Ur54',
+				label: i18n.ts._deck.enableSyncBetweenDevicesForProfiles,
+				keywords: ['sync', 'profiles', 'devices'],
+			},
+			{
+				id: 'iEF0gqNAo',
 				label: i18n.ts._deck.useSimpleUiForNonRootPages,
 				keywords: ['ui', 'root', 'page'],
 			},
 			{
-				id: 'avgxEYgsi',
+				id: 'BNdSeWxZn',
 				label: i18n.ts.defaultNavigationBehaviour,
 				keywords: ['default', 'navigation', 'behaviour', 'window'],
 			},
 			{
-				id: 'ma7OSw5JK',
+				id: 'zT9pGm8DF',
 				label: i18n.ts._deck.alwaysShowMainColumn,
 				keywords: ['always', 'show', 'main', 'column'],
 			},
 			{
-				id: 'jjTlUDhJH',
-				label: 'Unnamed marker',
+				id: '5dk2xv1vc',
+				label: i18n.ts._deck.columnAlign,
 				keywords: ['column', 'align'],
 			},
 		],
diff --git a/packages/frontend/src/utility/emoji-picker.ts b/packages/frontend/src/utility/emoji-picker.ts
index e7275b86f2..6279786b2d 100644
--- a/packages/frontend/src/utility/emoji-picker.ts
+++ b/packages/frontend/src/utility/emoji-picker.ts
@@ -3,10 +3,10 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { defineAsyncComponent, ref } from 'vue';
+import { defineAsyncComponent, ref, watch } from 'vue';
 import type { Ref } from 'vue';
 import { popup } from '@/os.js';
-import { store } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 /**
  * 絵文字ピッカーを表示する。
@@ -25,7 +25,14 @@ class EmojiPicker {
 	}
 
 	public async init() {
-		const emojisRef = store.r.pinnedEmojis;
+		const emojisRef = ref<string[]>([]);
+
+		watch([prefer.r.emojiPaletteForMain, prefer.r.emojiPalettes], () => {
+			emojisRef.value = prefer.s.emojiPaletteForMain == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForMain)?.emojis ?? [];
+		}, {
+			immediate: true,
+		});
+
 		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
 			src: this.src,
 			pinnedEmojis: emojisRef,
diff --git a/packages/frontend/src/utility/reaction-picker.ts b/packages/frontend/src/utility/reaction-picker.ts
index 200fb0b686..7c159fa2da 100644
--- a/packages/frontend/src/utility/reaction-picker.ts
+++ b/packages/frontend/src/utility/reaction-picker.ts
@@ -4,10 +4,10 @@
  */
 
 import * as Misskey from 'misskey-js';
-import { defineAsyncComponent, ref } from 'vue';
+import { defineAsyncComponent, ref, watch } from 'vue';
 import type { Ref } from 'vue';
 import { popup } from '@/os.js';
-import { store } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 class ReactionPicker {
 	private src: Ref<HTMLElement | null> = ref(null);
@@ -21,7 +21,14 @@ class ReactionPicker {
 	}
 
 	public async init() {
-		const reactionsRef = store.r.reactions;
+		const reactionsRef = ref<string[]>([]);
+
+		watch([prefer.r.emojiPaletteForReaction, prefer.r.emojiPalettes], () => {
+			reactionsRef.value = prefer.s.emojiPaletteForReaction == null ? prefer.s.emojiPalettes[0].emojis : prefer.s.emojiPalettes.find(palette => palette.id === prefer.s.emojiPaletteForReaction)?.emojis ?? [];
+		}, {
+			immediate: true,
+		});
+
 		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
 			src: this.src,
 			pinnedEmojis: reactionsRef,

From 5d228fb0f32aca9337c8b8a9ea9544f28d981f34 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 17:39:53 +0900
Subject: [PATCH 09/16] enhance(frontend): re-organize settings page

---
 locales/index.d.ts                            |   4 +
 locales/ja-JP.yml                             |   1 +
 .../src/pages/settings/accessibility.vue      |  51 ++
 .../src/pages/settings/appearance.vue         | 325 ---------
 .../frontend/src/pages/settings/index.vue     |   5 -
 .../src/pages/settings/preferences.vue        | 668 ++++++++++++------
 packages/frontend/src/router/definition.ts    |   4 -
 .../utility/autogen/settings-search-index.ts  | 423 ++++++-----
 8 files changed, 702 insertions(+), 779 deletions(-)
 delete mode 100644 packages/frontend/src/pages/settings/appearance.vue

diff --git a/locales/index.d.ts b/locales/index.d.ts
index b814bb70e1..f579aadb5d 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5342,6 +5342,10 @@ export interface Locale extends ILocale {
      * 絵文字パレット
      */
     "emojiPalette": string;
+    /**
+     * 投稿フォーム
+     */
+    "postForm": string;
     "_emojiPalette": {
         /**
          * パレット
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index b51a839715..2151a06611 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1331,6 +1331,7 @@ preferenceSyncConflictChoiceDevice: "デバイスの設定値"
 preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
 paste: "ペースト"
 emojiPalette: "絵文字パレット"
+postForm: "投稿フォーム"
 
 _emojiPalette:
   palettes: "パレット"
diff --git a/packages/frontend/src/pages/settings/accessibility.vue b/packages/frontend/src/pages/settings/accessibility.vue
index 3dbb039a17..f7b1e7d2a0 100644
--- a/packages/frontend/src/pages/settings/accessibility.vue
+++ b/packages/frontend/src/pages/settings/accessibility.vue
@@ -60,6 +60,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</SearchMarker>
 		</div>
 
+		<SearchMarker :keywords="['menu', 'style', 'popup', 'drawer']">
+			<MkPreferenceContainer k="menuStyle">
+				<MkSelect v-model="menuStyle">
+					<template #label><SearchLabel>{{ i18n.ts.menuStyle }}</SearchLabel></template>
+					<option value="auto">{{ i18n.ts.auto }}</option>
+					<option value="popup">{{ i18n.ts.popup }}</option>
+					<option value="drawer">{{ i18n.ts.drawer }}</option>
+				</MkSelect>
+			</MkPreferenceContainer>
+		</SearchMarker>
+
 		<SearchMarker :keywords="['contextmenu', 'system', 'native']">
 			<MkPreferenceContainer k="contextMenu">
 				<MkSelect v-model="contextMenu">
@@ -70,6 +81,22 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkSelect>
 			</MkPreferenceContainer>
 		</SearchMarker>
+
+		<SearchMarker :keywords="['font', 'size']">
+			<MkRadios v-model="fontSize">
+				<template #label><SearchLabel>{{ i18n.ts.fontSize }}</SearchLabel></template>
+				<option :value="null"><span style="font-size: 14px;">Aa</span></option>
+				<option value="1"><span style="font-size: 15px;">Aa</span></option>
+				<option value="2"><span style="font-size: 16px;">Aa</span></option>
+				<option value="3"><span style="font-size: 17px;">Aa</span></option>
+			</MkRadios>
+		</SearchMarker>
+
+		<SearchMarker :keywords="['font', 'system', 'native']">
+			<MkSwitch v-model="useSystemFont">
+				<template #label><SearchLabel>{{ i18n.ts.useSystemFont }}</SearchLabel></template>
+			</MkSwitch>
+		</SearchMarker>
 	</div>
 </SearchMarker>
 </template>
@@ -84,6 +111,8 @@ import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
+import { miLocalStorage } from '@/local-storage.js';
+import MkRadios from '@/components/MkRadios.vue';
 
 const reduceAnimation = prefer.model('animation', v => !v, v => !v);
 const animatedMfm = prefer.model('animatedMfm');
@@ -92,10 +121,32 @@ const keepScreenOn = prefer.model('keepScreenOn');
 const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe');
 const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer');
 const contextMenu = prefer.model('contextMenu');
+const menuStyle = prefer.model('menuStyle');
+
+const fontSize = ref(miLocalStorage.getItem('fontSize'));
+const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
+
+watch(fontSize, () => {
+	if (fontSize.value == null) {
+		miLocalStorage.removeItem('fontSize');
+	} else {
+		miLocalStorage.setItem('fontSize', fontSize.value);
+	}
+});
+
+watch(useSystemFont, () => {
+	if (useSystemFont.value) {
+		miLocalStorage.setItem('useSystemFont', 't');
+	} else {
+		miLocalStorage.removeItem('useSystemFont');
+	}
+});
 
 watch([
 	keepScreenOn,
 	contextMenu,
+	fontSize,
+	useSystemFont,
 ], async () => {
 	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
 });
diff --git a/packages/frontend/src/pages/settings/appearance.vue b/packages/frontend/src/pages/settings/appearance.vue
deleted file mode 100644
index 3fda5bc4c8..0000000000
--- a/packages/frontend/src/pages/settings/appearance.vue
+++ /dev/null
@@ -1,325 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<SearchMarker path="/settings/appearance" :label="i18n.ts.appearance" :keywords="['appearance']" icon="ti ti-device-desktop">
-	<div class="_gaps_m">
-		<MkFeatureBanner icon="/client-assets/desktop_computer_3d.png" color="#eaff00">
-			<SearchKeyword>{{ i18n.ts._settings.appearanceBanner }}</SearchKeyword>
-		</MkFeatureBanner>
-
-		<FormSection first>
-			<div class="_gaps_m">
-				<div class="_gaps_s">
-					<SearchMarker :keywords="['blur']">
-						<MkPreferenceContainer k="useBlurEffect">
-							<MkSwitch v-model="useBlurEffect">
-								<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['blur', 'modal']">
-						<MkPreferenceContainer k="useBlurEffectForModal">
-							<MkSwitch v-model="useBlurEffectForModal">
-								<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail']">
-						<MkPreferenceContainer k="highlightSensitiveMedia">
-							<MkSwitch v-model="highlightSensitiveMedia">
-								<template #label><SearchLabel>{{ i18n.ts.highlightSensitiveMedia }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['avatar', 'icon', 'square']">
-						<MkPreferenceContainer k="squareAvatars">
-							<MkSwitch v-model="squareAvatars">
-								<template #label><SearchLabel>{{ i18n.ts.squareAvatars }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['avatar', 'icon', 'decoration', 'show']">
-						<MkPreferenceContainer k="showAvatarDecorations">
-							<MkSwitch v-model="showAvatarDecorations">
-								<template #label><SearchLabel>{{ i18n.ts.showAvatarDecorations }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['note', 'timeline', 'gap']">
-						<MkPreferenceContainer k="showGapBetweenNotesInTimeline">
-							<MkSwitch v-model="showGapBetweenNotesInTimeline">
-								<template #label><SearchLabel>{{ i18n.ts.showGapBetweenNotesInTimeline }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['effect', 'show']">
-						<MkPreferenceContainer k="enableSeasonalScreenEffect">
-							<MkSwitch v-model="enableSeasonalScreenEffect">
-								<template #label><SearchLabel>{{ i18n.ts.seasonalScreenEffect }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-				</div>
-
-				<SearchMarker :keywords="['menu', 'style', 'popup', 'drawer']">
-					<MkPreferenceContainer k="menuStyle">
-						<MkSelect v-model="menuStyle">
-							<template #label><SearchLabel>{{ i18n.ts.menuStyle }}</SearchLabel></template>
-							<option value="auto">{{ i18n.ts.auto }}</option>
-							<option value="popup">{{ i18n.ts.popup }}</option>
-							<option value="drawer">{{ i18n.ts.drawer }}</option>
-						</MkSelect>
-					</MkPreferenceContainer>
-				</SearchMarker>
-
-				<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">
-					<MkPreferenceContainer k="emojiStyle">
-						<div>
-							<MkRadios v-model="emojiStyle">
-								<template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template>
-								<option value="native">{{ i18n.ts.native }}</option>
-								<option value="fluentEmoji">Fluent Emoji</option>
-								<option value="twemoji">Twemoji</option>
-							</MkRadios>
-							<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
-						</div>
-					</MkPreferenceContainer>
-				</SearchMarker>
-
-				<SearchMarker :keywords="['font', 'size']">
-					<MkRadios v-model="fontSize">
-						<template #label><SearchLabel>{{ i18n.ts.fontSize }}</SearchLabel></template>
-						<option :value="null"><span style="font-size: 14px;">Aa</span></option>
-						<option value="1"><span style="font-size: 15px;">Aa</span></option>
-						<option value="2"><span style="font-size: 16px;">Aa</span></option>
-						<option value="3"><span style="font-size: 17px;">Aa</span></option>
-					</MkRadios>
-				</SearchMarker>
-
-				<SearchMarker :keywords="['font', 'system', 'native']">
-					<MkSwitch v-model="useSystemFont">
-						<template #label><SearchLabel>{{ i18n.ts.useSystemFont }}</SearchLabel></template>
-					</MkSwitch>
-				</SearchMarker>
-			</div>
-		</FormSection>
-
-		<SearchMarker :keywords="['note', 'display']">
-			<FormSection>
-				<template #label><SearchLabel>{{ i18n.ts.displayOfNote }}</SearchLabel></template>
-
-				<div class="_gaps_m">
-					<SearchMarker :keywords="['reaction', 'size', 'scale', 'display']">
-						<MkPreferenceContainer k="reactionsDisplaySize">
-							<MkRadios v-model="reactionsDisplaySize">
-								<template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template>
-								<option value="small">{{ i18n.ts.small }}</option>
-								<option value="medium">{{ i18n.ts.medium }}</option>
-								<option value="large">{{ i18n.ts.large }}</option>
-							</MkRadios>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['reaction', 'size', 'scale', 'display', 'width', 'limit']">
-						<MkPreferenceContainer k="limitWidthOfReaction">
-							<MkSwitch v-model="limitWidthOfReaction">
-								<template #label><SearchLabel>{{ i18n.ts.limitWidthOfReaction }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height']">
-						<MkPreferenceContainer k="mediaListWithOneImageAppearance">
-							<MkRadios v-model="mediaListWithOneImageAppearance">
-								<template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template>
-								<option value="expand">{{ i18n.ts.default }}</option>
-								<option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
-								<option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
-								<option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
-							</MkRadios>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation']">
-						<MkPreferenceContainer k="instanceTicker">
-							<MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker">
-								<template #label><SearchLabel>{{ i18n.ts.instanceTicker }}</SearchLabel></template>
-								<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
-								<option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
-								<option value="always">{{ i18n.ts._instanceTicker.always }}</option>
-							</MkSelect>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility']">
-						<MkPreferenceContainer k="nsfw">
-							<MkSelect v-model="nsfw">
-								<template #label><SearchLabel>{{ i18n.ts.displayOfSensitiveMedia }}</SearchLabel></template>
-								<option value="respect">{{ i18n.ts._displayOfSensitiveMedia.respect }}</option>
-								<option value="ignore">{{ i18n.ts._displayOfSensitiveMedia.ignore }}</option>
-								<option value="force">{{ i18n.ts._displayOfSensitiveMedia.force }}</option>
-							</MkSelect>
-						</MkPreferenceContainer>
-					</SearchMarker>
-				</div>
-			</FormSection>
-		</SearchMarker>
-
-		<SearchMarker :keywords="['notification', 'display']">
-			<FormSection>
-				<template #label><SearchLabel>{{ i18n.ts.notificationDisplay }}</SearchLabel></template>
-
-				<div class="_gaps_m">
-					<SearchMarker :keywords="['position']">
-						<MkPreferenceContainer k="notificationPosition">
-							<MkRadios v-model="notificationPosition">
-								<template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template>
-								<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
-								<option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option>
-								<option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option>
-								<option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option>
-							</MkRadios>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['stack', 'axis', 'direction']">
-						<MkPreferenceContainer k="notificationStackAxis">
-							<MkRadios v-model="notificationStackAxis">
-								<template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template>
-								<option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
-								<option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
-							</MkRadios>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton>
-				</div>
-			</FormSection>
-		</SearchMarker>
-
-		<FormSection>
-			<FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
-		</FormSection>
-	</div>
-</SearchMarker>
-</template>
-
-<script lang="ts" setup>
-import { computed, ref, watch } from 'vue';
-import * as Misskey from 'misskey-js';
-import MkSwitch from '@/components/MkSwitch.vue';
-import MkSelect from '@/components/MkSelect.vue';
-import MkRadios from '@/components/MkRadios.vue';
-import { prefer } from '@/preferences.js';
-import { reloadAsk } from '@/utility/reload-ask.js';
-import { i18n } from '@/i18n.js';
-import { definePage } from '@/page.js';
-import { miLocalStorage } from '@/local-storage.js';
-import FormLink from '@/components/form/link.vue';
-import { globalEvents } from '@/events.js';
-import { claimAchievement } from '@/utility/achievements.js';
-import MkButton from '@/components/MkButton.vue';
-import FormSection from '@/components/form/section.vue';
-import { instance } from '@/instance.js';
-import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
-import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
-
-const fontSize = ref(miLocalStorage.getItem('fontSize'));
-const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
-
-const showAvatarDecorations = prefer.model('showAvatarDecorations');
-const emojiStyle = prefer.model('emojiStyle');
-const menuStyle = prefer.model('menuStyle');
-const useBlurEffectForModal = prefer.model('useBlurEffectForModal');
-const useBlurEffect = prefer.model('useBlurEffect');
-const highlightSensitiveMedia = prefer.model('highlightSensitiveMedia');
-const squareAvatars = prefer.model('squareAvatars');
-const enableSeasonalScreenEffect = prefer.model('enableSeasonalScreenEffect');
-const showGapBetweenNotesInTimeline = prefer.model('showGapBetweenNotesInTimeline');
-const mediaListWithOneImageAppearance = prefer.model('mediaListWithOneImageAppearance');
-const reactionsDisplaySize = prefer.model('reactionsDisplaySize');
-const limitWidthOfReaction = prefer.model('limitWidthOfReaction');
-const notificationPosition = prefer.model('notificationPosition');
-const notificationStackAxis = prefer.model('notificationStackAxis');
-const nsfw = prefer.model('nsfw');
-const instanceTicker = prefer.model('instanceTicker');
-
-watch(fontSize, () => {
-	if (fontSize.value == null) {
-		miLocalStorage.removeItem('fontSize');
-	} else {
-		miLocalStorage.setItem('fontSize', fontSize.value);
-	}
-});
-
-watch(useSystemFont, () => {
-	if (useSystemFont.value) {
-		miLocalStorage.setItem('useSystemFont', 't');
-	} else {
-		miLocalStorage.removeItem('useSystemFont');
-	}
-});
-
-watch([
-	fontSize,
-	useSystemFont,
-	squareAvatars,
-	highlightSensitiveMedia,
-	enableSeasonalScreenEffect,
-	showGapBetweenNotesInTimeline,
-	mediaListWithOneImageAppearance,
-	reactionsDisplaySize,
-	limitWidthOfReaction,
-	mediaListWithOneImageAppearance,
-	reactionsDisplaySize,
-	limitWidthOfReaction,
-	instanceTicker,
-], async () => {
-	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
-});
-
-let smashCount = 0;
-let smashTimer: number | null = null;
-
-function testNotification(): void {
-	const notification: Misskey.entities.Notification = {
-		id: Math.random().toString(),
-		createdAt: new Date().toUTCString(),
-		isRead: false,
-		type: 'test',
-	};
-
-	globalEvents.emit('clientNotification', notification);
-
-	// セルフ通知破壊 実績関連
-	smashCount++;
-	if (smashCount >= 10) {
-		claimAchievement('smashTestNotificationButton');
-		smashCount = 0;
-	}
-	if (smashTimer) {
-		clearTimeout(smashTimer);
-	}
-	smashTimer = window.setTimeout(() => {
-		smashCount = 0;
-	}, 300);
-}
-
-const headerActions = computed(() => []);
-
-const headerTabs = computed(() => []);
-
-definePage(() => ({
-	title: i18n.ts.appearance,
-	icon: 'ti ti-device-desktop',
-}));
-</script>
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index debcd4bd3e..3b7c44fbfe 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -118,11 +118,6 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
 		text: i18n.ts.emojiPalette,
 		to: '/settings/emoji-palette',
 		active: currentPage.value?.route.name === 'emoji-palette',
-	}, {
-		icon: 'ti ti-device-desktop',
-		text: i18n.ts.appearance,
-		to: '/settings/appearance',
-		active: currentPage.value?.route.name === 'appearance',
 	}, {
 		icon: 'ti ti-music',
 		text: i18n.ts.sounds,
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 374477c510..b9a596067c 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -10,121 +10,174 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<SearchKeyword>{{ i18n.ts._settings.preferencesBanner }}</SearchKeyword>
 		</MkFeatureBanner>
 
-		<SearchMarker :keywords="['language']">
-			<MkSelect v-model="lang">
-				<template #label><SearchLabel>{{ i18n.ts.uiLanguage }}</SearchLabel></template>
-				<option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
-				<template #caption>
-					<I18n :src="i18n.ts.i18nInfo" tag="span">
-						<template #link>
-							<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
-						</template>
-					</I18n>
-				</template>
-			</MkSelect>
-		</SearchMarker>
-
-		<SearchMarker :keywords="['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop']">
-			<MkRadios v-model="overridedDeviceKind">
-				<template #label><SearchLabel>{{ i18n.ts.overridedDeviceKind }}</SearchLabel></template>
-				<option :value="null">{{ i18n.ts.auto }}</option>
-				<option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
-				<option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option>
-				<option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
-			</MkRadios>
-		</SearchMarker>
-
-		<FormSection>
-			<div class="_gaps_s">
-				<SearchMarker :keywords="['post', 'form', 'timeline']">
-					<MkPreferenceContainer k="showFixedPostForm">
-						<MkSwitch v-model="showFixedPostForm">
-							<template #label><SearchLabel>{{ i18n.ts.showFixedPostForm }}</SearchLabel></template>
-						</MkSwitch>
-					</MkPreferenceContainer>
-				</SearchMarker>
-
-				<SearchMarker :keywords="['post', 'form', 'timeline', 'channel']">
-					<MkPreferenceContainer k="showFixedPostFormInChannel">
-						<MkSwitch v-model="showFixedPostFormInChannel">
-							<template #label><SearchLabel>{{ i18n.ts.showFixedPostFormInChannel }}</SearchLabel></template>
-						</MkSwitch>
-					</MkPreferenceContainer>
-				</SearchMarker>
-
-				<SearchMarker :keywords="['pinned', 'list']">
-					<MkFolder>
-						<template #label><SearchLabel>{{ i18n.ts.pinnedList }}</SearchLabel></template>
-						<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
-						<MkButton v-if="prefer.r.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
-						<MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
-					</MkFolder>
-				</SearchMarker>
-
-				<SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn']">
-					<MkPreferenceContainer k="enableQuickAddMfmFunction">
-						<MkSwitch v-model="enableQuickAddMfmFunction">
-							<template #label><SearchLabel>{{ i18n.ts.enableQuickAddMfmFunction }}</SearchLabel></template>
-						</MkSwitch>
-					</MkPreferenceContainer>
-				</SearchMarker>
-			</div>
-		</FormSection>
-
-		<FormSection>
-			<div class="_gaps_m">
-				<SearchMarker :keywords="['remember', 'keep', 'note', 'visibility']">
-					<MkPreferenceContainer k="rememberNoteVisibility">
-						<MkSwitch v-model="rememberNoteVisibility">
-							<template #label><SearchLabel>{{ i18n.ts.rememberNoteVisibility }}</SearchLabel></template>
-						</MkSwitch>
-					</MkPreferenceContainer>
-				</SearchMarker>
-
-				<SearchMarker :keywords="['default', 'note', 'visibility']">
-					<MkDisableSection :disabled="rememberNoteVisibility">
-						<MkFolder>
-							<template #label><SearchLabel>{{ i18n.ts.defaultNoteVisibility }}</SearchLabel></template>
-							<template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
-							<template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template>
-							<template v-else-if="defaultNoteVisibility === 'followers'" #suffix>{{ i18n.ts._visibility.followers }}</template>
-							<template v-else-if="defaultNoteVisibility === 'specified'" #suffix>{{ i18n.ts._visibility.specified }}</template>
-
-							<div class="_gaps_m">
-								<MkPreferenceContainer k="defaultNoteVisibility">
-									<MkSelect v-model="defaultNoteVisibility">
-										<option value="public">{{ i18n.ts._visibility.public }}</option>
-										<option value="home">{{ i18n.ts._visibility.home }}</option>
-										<option value="followers">{{ i18n.ts._visibility.followers }}</option>
-										<option value="specified">{{ i18n.ts._visibility.specified }}</option>
-									</MkSelect>
-								</MkPreferenceContainer>
-
-								<MkPreferenceContainer k="defaultNoteLocalOnly">
-									<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch>
-								</MkPreferenceContainer>
-							</div>
-						</MkFolder>
-					</MkDisableSection>
-				</SearchMarker>
-			</div>
-		</FormSection>
-
-		<SearchMarker :keywords="['note']">
-			<FormSection>
-				<template #label><SearchLabel>{{ i18n.ts.note }}</SearchLabel></template>
+		<SearchMarker :keywords="['general']">
+			<MkFolder :defaultOpen="true">
+				<template #label><SearchLabel>{{ i18n.ts.general }}</SearchLabel></template>
 
 				<div class="_gaps_m">
+					<SearchMarker :keywords="['language']">
+						<MkSelect v-model="lang">
+							<template #label><SearchLabel>{{ i18n.ts.uiLanguage }}</SearchLabel></template>
+							<option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
+							<template #caption>
+								<I18n :src="i18n.ts.i18nInfo" tag="span">
+									<template #link>
+										<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
+									</template>
+								</I18n>
+							</template>
+						</MkSelect>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop']">
+						<MkRadios v-model="overridedDeviceKind">
+							<template #label><SearchLabel>{{ i18n.ts.overridedDeviceKind }}</SearchLabel></template>
+							<option :value="null">{{ i18n.ts.auto }}</option>
+							<option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
+							<option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option>
+							<option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
+						</MkRadios>
+					</SearchMarker>
+
 					<div class="_gaps_s">
-						<SearchMarker :keywords="['renote']">
-							<MkPreferenceContainer k="collapseRenotes">
-								<MkSwitch v-model="collapseRenotes">
-									<template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template>
-									<template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template>
+						<SearchMarker :keywords="['blur']">
+							<MkPreferenceContainer k="useBlurEffect">
+								<MkSwitch v-model="useBlurEffect">
+									<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
 								</MkSwitch>
 							</MkPreferenceContainer>
 						</SearchMarker>
 
+						<SearchMarker :keywords="['blur', 'modal']">
+							<MkPreferenceContainer k="useBlurEffectForModal">
+								<MkSwitch v-model="useBlurEffectForModal">
+									<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['avatar', 'icon', 'decoration', 'show']">
+							<MkPreferenceContainer k="showAvatarDecorations">
+								<MkSwitch v-model="showAvatarDecorations">
+									<template #label><SearchLabel>{{ i18n.ts.showAvatarDecorations }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['follow', 'confirm', 'always']">
+							<MkPreferenceContainer k="alwaysConfirmFollow">
+								<MkSwitch v-model="alwaysConfirmFollow">
+									<template #label><SearchLabel>{{ i18n.ts.alwaysConfirmFollow }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail']">
+							<MkPreferenceContainer k="highlightSensitiveMedia">
+								<MkSwitch v-model="highlightSensitiveMedia">
+									<template #label><SearchLabel>{{ i18n.ts.highlightSensitiveMedia }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm']">
+							<MkPreferenceContainer k="confirmWhenRevealingSensitiveMedia">
+								<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">
+									<template #label><SearchLabel>{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+					</div>
+
+					<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">
+						<MkPreferenceContainer k="emojiStyle">
+							<div>
+								<MkRadios v-model="emojiStyle">
+									<template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template>
+									<option value="native">{{ i18n.ts.native }}</option>
+									<option value="fluentEmoji">Fluent Emoji</option>
+									<option value="twemoji">Twemoji</option>
+								</MkRadios>
+								<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
+							</div>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['pinned', 'list']">
+						<MkFolder>
+							<template #label><SearchLabel>{{ i18n.ts.pinnedList }}</SearchLabel></template>
+							<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
+							<MkButton v-if="prefer.r.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
+							<MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
+						</MkFolder>
+					</SearchMarker>
+				</div>
+			</MkFolder>
+		</SearchMarker>
+
+		<SearchMarker :keywords="['timeline']">
+			<MkFolder :defaultOpen="true">
+				<template #label><SearchLabel>{{ i18n.ts.timeline }}</SearchLabel></template>
+
+				<div class="_gaps_s">
+					<SearchMarker :keywords="['post', 'form', 'timeline']">
+						<MkPreferenceContainer k="showFixedPostForm">
+							<MkSwitch v-model="showFixedPostForm">
+								<template #label><SearchLabel>{{ i18n.ts.showFixedPostForm }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['post', 'form', 'timeline', 'channel']">
+						<MkPreferenceContainer k="showFixedPostFormInChannel">
+							<MkSwitch v-model="showFixedPostFormInChannel">
+								<template #label><SearchLabel>{{ i18n.ts.showFixedPostFormInChannel }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['renote']">
+						<MkPreferenceContainer k="collapseRenotes">
+							<MkSwitch v-model="collapseRenotes">
+								<template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template>
+								<template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['note', 'timeline', 'gap']">
+						<MkPreferenceContainer k="showGapBetweenNotesInTimeline">
+							<MkSwitch v-model="showGapBetweenNotesInTimeline">
+								<template #label><SearchLabel>{{ i18n.ts.showGapBetweenNotesInTimeline }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['load', 'auto', 'more']">
+						<MkPreferenceContainer k="enableInfiniteScroll">
+							<MkSwitch v-model="enableInfiniteScroll">
+								<template #label><SearchLabel>{{ i18n.ts.enableInfiniteScroll }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['disable', 'streaming', 'timeline']">
+						<MkPreferenceContainer k="disableStreamingTimeline">
+							<MkSwitch v-model="disableStreamingTimeline">
+								<template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
+					</SearchMarker>
+				</div>
+			</MkFolder>
+		</SearchMarker>
+
+		<SearchMarker :keywords="['note']">
+			<MkFolder :defaultOpen="true">
+				<template #label><SearchLabel>{{ i18n.ts.note }}</SearchLabel></template>
+
+				<div class="_gaps_m">
+					<div class="_gaps_s">
 						<SearchMarker :keywords="['hover', 'show', 'footer', 'action']">
 							<MkPreferenceContainer k="showNoteActionsOnlyHover">
 								<MkSwitch v-model="showNoteActionsOnlyHover">
@@ -157,6 +210,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</MkPreferenceContainer>
 						</SearchMarker>
 
+						<SearchMarker :keywords="['reaction', 'confirm']">
+							<MkPreferenceContainer k="confirmOnReact">
+								<MkSwitch v-model="confirmOnReact">
+									<template #label><SearchLabel>{{ i18n.ts.confirmOnReact }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
 						<SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment']">
 							<MkPreferenceContainer k="loadRawImages">
 								<MkSwitch v-model="loadRawImages">
@@ -164,40 +225,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 								</MkSwitch>
 							</MkPreferenceContainer>
 						</SearchMarker>
-					</div>
-				</div>
-			</FormSection>
-		</SearchMarker>
-
-		<SearchMarker :keywords="['notification']">
-			<FormSection>
-				<template #label><SearchLabel>{{ i18n.ts.notifications }}</SearchLabel></template>
-
-				<div class="_gaps_m">
-					<SearchMarker :keywords="['group']">
-						<MkPreferenceContainer k="useGroupedNotifications">
-							<MkSwitch v-model="useGroupedNotifications">
-								<template #label><SearchLabel>{{ i18n.ts.useGroupedNotifications }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-				</div>
-			</FormSection>
-		</SearchMarker>
-
-		<SearchMarker :keywords="['behavior']">
-			<FormSection>
-				<template #label><SearchLabel>{{ i18n.ts.behavior }}</SearchLabel></template>
-
-				<div class="_gaps_m">
-					<div class="_gaps_s">
-						<SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab']">
-							<MkPreferenceContainer k="imageNewTab">
-								<MkSwitch v-model="imageNewTab">
-									<template #label><SearchLabel>{{ i18n.ts.openImageInNewTab }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
 
 						<SearchMarker :keywords="['reaction', 'picker', 'contextmenu', 'open']">
 							<MkPreferenceContainer k="useReactionPickerForContextMenu">
@@ -206,47 +233,70 @@ SPDX-License-Identifier: AGPL-3.0-only
 								</MkSwitch>
 							</MkPreferenceContainer>
 						</SearchMarker>
+					</div>
 
-						<SearchMarker :keywords="['load', 'auto', 'more']">
-							<MkPreferenceContainer k="enableInfiniteScroll">
-								<MkSwitch v-model="enableInfiniteScroll">
-									<template #label><SearchLabel>{{ i18n.ts.enableInfiniteScroll }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
+					<SearchMarker :keywords="['reaction', 'size', 'scale', 'display']">
+						<MkPreferenceContainer k="reactionsDisplaySize">
+							<MkRadios v-model="reactionsDisplaySize">
+								<template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template>
+								<option value="small">{{ i18n.ts.small }}</option>
+								<option value="medium">{{ i18n.ts.medium }}</option>
+								<option value="large">{{ i18n.ts.large }}</option>
+							</MkRadios>
+						</MkPreferenceContainer>
+					</SearchMarker>
 
-						<SearchMarker :keywords="['disable', 'streaming', 'timeline']">
-							<MkPreferenceContainer k="disableStreamingTimeline">
-								<MkSwitch v-model="disableStreamingTimeline">
-									<template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
+					<SearchMarker :keywords="['reaction', 'size', 'scale', 'display', 'width', 'limit']">
+						<MkPreferenceContainer k="limitWidthOfReaction">
+							<MkSwitch v-model="limitWidthOfReaction">
+								<template #label><SearchLabel>{{ i18n.ts.limitWidthOfReaction }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
+					</SearchMarker>
 
-						<SearchMarker :keywords="['follow', 'confirm', 'always']">
-							<MkPreferenceContainer k="alwaysConfirmFollow">
-								<MkSwitch v-model="alwaysConfirmFollow">
-									<template #label><SearchLabel>{{ i18n.ts.alwaysConfirmFollow }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
+					<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height']">
+						<MkPreferenceContainer k="mediaListWithOneImageAppearance">
+							<MkRadios v-model="mediaListWithOneImageAppearance">
+								<template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template>
+								<option value="expand">{{ i18n.ts.default }}</option>
+								<option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
+								<option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
+								<option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
+							</MkRadios>
+						</MkPreferenceContainer>
+					</SearchMarker>
 
-						<SearchMarker :keywords="['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm']">
-							<MkPreferenceContainer k="confirmWhenRevealingSensitiveMedia">
-								<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">
-									<template #label><SearchLabel>{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
+					<SearchMarker :keywords="['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation']">
+						<MkPreferenceContainer k="instanceTicker">
+							<MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker">
+								<template #label><SearchLabel>{{ i18n.ts.instanceTicker }}</SearchLabel></template>
+								<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
+								<option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
+								<option value="always">{{ i18n.ts._instanceTicker.always }}</option>
+							</MkSelect>
+						</MkPreferenceContainer>
+					</SearchMarker>
 
-						<SearchMarker :keywords="['reaction', 'confirm']">
-							<MkPreferenceContainer k="confirmOnReact">
-								<MkSwitch v-model="confirmOnReact">
-									<template #label><SearchLabel>{{ i18n.ts.confirmOnReact }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
+					<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility']">
+						<MkPreferenceContainer k="nsfw">
+							<MkSelect v-model="nsfw">
+								<template #label><SearchLabel>{{ i18n.ts.displayOfSensitiveMedia }}</SearchLabel></template>
+								<option value="respect">{{ i18n.ts._displayOfSensitiveMedia.respect }}</option>
+								<option value="ignore">{{ i18n.ts._displayOfSensitiveMedia.ignore }}</option>
+								<option value="force">{{ i18n.ts._displayOfSensitiveMedia.force }}</option>
+							</MkSelect>
+						</MkPreferenceContainer>
+					</SearchMarker>
+				</div>
+			</MkFolder>
+		</SearchMarker>
 
+		<SearchMarker :keywords="['post', 'form']">
+			<MkFolder :defaultOpen="true">
+				<template #label><SearchLabel>{{ i18n.ts.postForm }}</SearchLabel></template>
+
+				<div class="_gaps_m">
+					<div class="_gaps_s">
 						<SearchMarker :keywords="['remember', 'keep', 'note', 'cw']">
 							<MkPreferenceContainer k="keepCw">
 								<MkSwitch v-model="keepCw">
@@ -254,6 +304,123 @@ SPDX-License-Identifier: AGPL-3.0-only
 								</MkSwitch>
 							</MkPreferenceContainer>
 						</SearchMarker>
+
+						<SearchMarker :keywords="['remember', 'keep', 'note', 'visibility']">
+							<MkPreferenceContainer k="rememberNoteVisibility">
+								<MkSwitch v-model="rememberNoteVisibility">
+									<template #label><SearchLabel>{{ i18n.ts.rememberNoteVisibility }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn']">
+							<MkPreferenceContainer k="enableQuickAddMfmFunction">
+								<MkSwitch v-model="enableQuickAddMfmFunction">
+									<template #label><SearchLabel>{{ i18n.ts.enableQuickAddMfmFunction }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+					</div>
+
+					<SearchMarker :keywords="['default', 'note', 'visibility']">
+						<MkDisableSection :disabled="rememberNoteVisibility">
+							<MkFolder>
+								<template #label><SearchLabel>{{ i18n.ts.defaultNoteVisibility }}</SearchLabel></template>
+								<template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
+								<template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template>
+								<template v-else-if="defaultNoteVisibility === 'followers'" #suffix>{{ i18n.ts._visibility.followers }}</template>
+								<template v-else-if="defaultNoteVisibility === 'specified'" #suffix>{{ i18n.ts._visibility.specified }}</template>
+
+								<div class="_gaps_m">
+									<MkPreferenceContainer k="defaultNoteVisibility">
+										<MkSelect v-model="defaultNoteVisibility">
+											<option value="public">{{ i18n.ts._visibility.public }}</option>
+											<option value="home">{{ i18n.ts._visibility.home }}</option>
+											<option value="followers">{{ i18n.ts._visibility.followers }}</option>
+											<option value="specified">{{ i18n.ts._visibility.specified }}</option>
+										</MkSelect>
+									</MkPreferenceContainer>
+
+									<MkPreferenceContainer k="defaultNoteLocalOnly">
+										<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch>
+									</MkPreferenceContainer>
+								</div>
+							</MkFolder>
+						</MkDisableSection>
+					</SearchMarker>
+				</div>
+			</MkFolder>
+		</SearchMarker>
+
+		<SearchMarker :keywords="['notification']">
+			<MkFolder :defaultOpen="true">
+				<template #label><SearchLabel>{{ i18n.ts.notifications }}</SearchLabel></template>
+
+				<div class="_gaps_m">
+					<SearchMarker :keywords="['group']">
+						<MkPreferenceContainer k="useGroupedNotifications">
+							<MkSwitch v-model="useGroupedNotifications">
+								<template #label><SearchLabel>{{ i18n.ts.useGroupedNotifications }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['position']">
+						<MkPreferenceContainer k="notificationPosition">
+							<MkRadios v-model="notificationPosition">
+								<template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template>
+								<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
+								<option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option>
+								<option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option>
+								<option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option>
+							</MkRadios>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<SearchMarker :keywords="['stack', 'axis', 'direction']">
+						<MkPreferenceContainer k="notificationStackAxis">
+							<MkRadios v-model="notificationStackAxis">
+								<template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template>
+								<option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
+								<option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
+							</MkRadios>
+						</MkPreferenceContainer>
+					</SearchMarker>
+
+					<MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton>
+				</div>
+			</MkFolder>
+		</SearchMarker>
+
+		<SearchMarker :keywords="['other']">
+			<MkFolder :defaultOpen="true">
+				<template #label><SearchLabel>{{ i18n.ts.other }}</SearchLabel></template>
+
+				<div class="_gaps_m">
+					<div class="_gaps_s">
+						<SearchMarker :keywords="['avatar', 'icon', 'square']">
+							<MkPreferenceContainer k="squareAvatars">
+								<MkSwitch v-model="squareAvatars">
+									<template #label><SearchLabel>{{ i18n.ts.squareAvatars }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['effect', 'show']">
+							<MkPreferenceContainer k="enableSeasonalScreenEffect">
+								<MkSwitch v-model="enableSeasonalScreenEffect">
+									<template #label><SearchLabel>{{ i18n.ts.seasonalScreenEffect }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab']">
+							<MkPreferenceContainer k="imageNewTab">
+								<MkSwitch v-model="imageNewTab">
+									<template #label><SearchLabel>{{ i18n.ts.openImageInNewTab }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
 					</div>
 
 					<SearchMarker :keywords="['server', 'disconnect', 'reconnect', 'reload', 'streaming']">
@@ -276,47 +443,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 						</MkPreferenceContainer>
 					</SearchMarker>
 
-					<SearchMarker :label="i18n.ts.dataSaver" :keywords="['datasaver']">
-						<MkFolder>
-							<template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template>
-
-							<div class="_gaps_m">
-								<MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
-
-								<div class="_buttons">
-									<MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
-									<MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
-								</div>
-								<div class="_gaps_m">
-									<MkSwitch v-model="dataSaver.media">
-										{{ i18n.ts._dataSaver._media.title }}
-										<template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
-									</MkSwitch>
-									<MkSwitch v-model="dataSaver.avatar">
-										{{ i18n.ts._dataSaver._avatar.title }}
-										<template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
-									</MkSwitch>
-									<MkSwitch v-model="dataSaver.urlPreview">
-										{{ i18n.ts._dataSaver._urlPreview.title }}
-										<template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
-									</MkSwitch>
-									<MkSwitch v-model="dataSaver.code">
-										{{ i18n.ts._dataSaver._code.title }}
-										<template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
-									</MkSwitch>
-								</div>
-							</div>
-						</MkFolder>
-					</SearchMarker>
-				</div>
-			</FormSection>
-		</SearchMarker>
-
-		<SearchMarker>
-			<FormSection>
-				<template #label><SearchLabel>{{ i18n.ts.other }}</SearchLabel></template>
-
-				<div class="_gaps">
 					<SearchMarker :keywords="['ad', 'show']">
 						<MkPreferenceContainer k="forceShowAds">
 							<MkSwitch v-model="forceShowAds">
@@ -347,18 +473,47 @@ SPDX-License-Identifier: AGPL-3.0-only
 							</div>
 						</MkFolder>
 					</SearchMarker>
-
-					<FormLink to="/settings/navbar">{{ i18n.ts.navbar }}</FormLink>
-					<FormLink to="/settings/statusbar">{{ i18n.ts.statusbar }}</FormLink>
 				</div>
-			</FormSection>
+			</MkFolder>
 		</SearchMarker>
 
-		<FormSection>
-			<div class="_gaps">
-				<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
-			</div>
-		</FormSection>
+		<SearchMarker :keywords="['datasaver']">
+			<MkFolder>
+				<template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template>
+
+				<div class="_gaps_m">
+					<MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
+
+					<div class="_buttons">
+						<MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
+						<MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
+					</div>
+					<div class="_gaps_m">
+						<MkSwitch v-model="dataSaver.media">
+							{{ i18n.ts._dataSaver._media.title }}
+							<template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
+						</MkSwitch>
+						<MkSwitch v-model="dataSaver.avatar">
+							{{ i18n.ts._dataSaver._avatar.title }}
+							<template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
+						</MkSwitch>
+						<MkSwitch v-model="dataSaver.urlPreview">
+							{{ i18n.ts._dataSaver._urlPreview.title }}
+							<template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
+						</MkSwitch>
+						<MkSwitch v-model="dataSaver.code">
+							{{ i18n.ts._dataSaver._code.title }}
+							<template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
+						</MkSwitch>
+					</div>
+				</div>
+			</MkFolder>
+		</SearchMarker>
+
+		<FormLink to="/settings/navbar">{{ i18n.ts.navbar }}</FormLink>
+		<FormLink to="/settings/statusbar">{{ i18n.ts.statusbar }}</FormLink>
+		<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
+		<FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
 	</div>
 </SearchMarker>
 </template>
@@ -366,6 +521,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue';
 import { langs } from '@@/js/config.js';
+import * as Misskey from 'misskey-js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkRadios from '@/components/MkRadios.vue';
@@ -386,6 +542,9 @@ import { miLocalStorage } from '@/local-storage.js';
 import { prefer } from '@/preferences.js';
 import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
+import { globalEvents } from '@/events.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { instance } from '@/instance.js';
 
 const lang = ref(miLocalStorage.getItem('lang'));
 const dataSaver = ref(prefer.s.dataSaver);
@@ -413,10 +572,24 @@ const useGroupedNotifications = prefer.model('useGroupedNotifications');
 const alwaysConfirmFollow = prefer.model('alwaysConfirmFollow');
 const confirmWhenRevealingSensitiveMedia = prefer.model('confirmWhenRevealingSensitiveMedia');
 const confirmOnReact = prefer.model('confirmOnReact');
-const contextMenu = prefer.model('contextMenu');
 const defaultNoteVisibility = prefer.model('defaultNoteVisibility');
 const defaultNoteLocalOnly = prefer.model('defaultNoteLocalOnly');
 const rememberNoteVisibility = prefer.model('rememberNoteVisibility');
+const showGapBetweenNotesInTimeline = prefer.model('showGapBetweenNotesInTimeline');
+const notificationPosition = prefer.model('notificationPosition');
+const notificationStackAxis = prefer.model('notificationStackAxis');
+const instanceTicker = prefer.model('instanceTicker');
+const highlightSensitiveMedia = prefer.model('highlightSensitiveMedia');
+const mediaListWithOneImageAppearance = prefer.model('mediaListWithOneImageAppearance');
+const reactionsDisplaySize = prefer.model('reactionsDisplaySize');
+const limitWidthOfReaction = prefer.model('limitWidthOfReaction');
+const squareAvatars = prefer.model('squareAvatars');
+const enableSeasonalScreenEffect = prefer.model('enableSeasonalScreenEffect');
+const showAvatarDecorations = prefer.model('showAvatarDecorations');
+const nsfw = prefer.model('nsfw');
+const emojiStyle = prefer.model('emojiStyle');
+const useBlurEffectForModal = prefer.model('useBlurEffectForModal');
+const useBlurEffect = prefer.model('useBlurEffect');
 
 watch(lang, () => {
 	miLocalStorage.setItem('lang', lang.value as string);
@@ -433,7 +606,17 @@ watch([
 	disableStreamingTimeline,
 	alwaysConfirmFollow,
 	confirmWhenRevealingSensitiveMedia,
-	contextMenu,
+	showGapBetweenNotesInTimeline,
+	mediaListWithOneImageAppearance,
+	reactionsDisplaySize,
+	limitWidthOfReaction,
+	mediaListWithOneImageAppearance,
+	reactionsDisplaySize,
+	limitWidthOfReaction,
+	instanceTicker,
+	squareAvatars,
+	highlightSensitiveMedia,
+	enableSeasonalScreenEffect,
 ], async () => {
 	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
 });
@@ -522,6 +705,33 @@ watch(dataSaver, (to) => {
 	deep: true,
 });
 
+let smashCount = 0;
+let smashTimer: number | null = null;
+
+function testNotification(): void {
+	const notification: Misskey.entities.Notification = {
+		id: Math.random().toString(),
+		createdAt: new Date().toUTCString(),
+		isRead: false,
+		type: 'test',
+	};
+
+	globalEvents.emit('clientNotification', notification);
+
+	// セルフ通知破壊 実績関連
+	smashCount++;
+	if (smashCount >= 10) {
+		claimAchievement('smashTestNotificationButton');
+		smashCount = 0;
+	}
+	if (smashTimer) {
+		clearTimeout(smashTimer);
+	}
+	smashTimer = window.setTimeout(() => {
+		smashCount = 0;
+	}, 300);
+}
+
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 752356497e..62b13d22be 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -105,10 +105,6 @@ const routes: RouteDef[] = [{
 		path: '/theme',
 		name: 'theme',
 		component: page(() => import('@/pages/settings/theme.vue')),
-	}, {
-		path: '/appearance',
-		name: 'appearance',
-		component: page(() => import('@/pages/settings/appearance.vue')),
 	}, {
 		path: '/navbar',
 		name: 'navbar',
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index 4f1a94f266..734dc0c99c 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -272,176 +272,265 @@ export const searchIndexes: SearchIndexItem[] = [
 		children: [
 			{
 				id: 'kMJ5laK3n',
-				label: i18n.ts.uiLanguage,
-				keywords: ['language'],
-			},
-			{
-				id: 'dlKebHH6k',
-				label: i18n.ts.overridedDeviceKind,
-				keywords: ['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop'],
-			},
-			{
-				id: 'nxvMUir3T',
-				label: i18n.ts.showFixedPostForm,
-				keywords: ['post', 'form', 'timeline'],
-			},
-			{
-				id: '84MdeDWL1',
-				label: i18n.ts.showFixedPostFormInChannel,
-				keywords: ['post', 'form', 'timeline', 'channel'],
-			},
-			{
-				id: 'dOig3ye4Z',
-				label: i18n.ts.pinnedList,
-				keywords: ['pinned', 'list'],
-			},
-			{
-				id: '4huRldNp5',
-				label: i18n.ts.enableQuickAddMfmFunction,
-				keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
-			},
-			{
-				id: '1x3JNXj8N',
-				label: i18n.ts.rememberNoteVisibility,
-				keywords: ['remember', 'keep', 'note', 'visibility'],
-			},
-			{
-				id: 'CfAg0Qekq',
-				label: i18n.ts.defaultNoteVisibility,
-				keywords: ['default', 'note', 'visibility'],
-			},
-			{
-				id: 'tMm9kH9gy',
 				children: [
 					{
-						id: 'hDdVkBFJP',
+						id: 'EC8J177N8',
+						label: i18n.ts.uiLanguage,
+						keywords: ['language'],
+					},
+					{
+						id: 'CHKy9gnrh',
+						label: i18n.ts.overridedDeviceKind,
+						keywords: ['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop'],
+					},
+					{
+						id: 'snyCQ5oKE',
+						label: i18n.ts.useBlurEffect,
+						keywords: ['blur'],
+					},
+					{
+						id: '8j36S4Ev6',
+						label: i18n.ts.useBlurEffectForModal,
+						keywords: ['blur', 'modal'],
+					},
+					{
+						id: 'cytWLyF1V',
+						label: i18n.ts.showAvatarDecorations,
+						keywords: ['avatar', 'icon', 'decoration', 'show'],
+					},
+					{
+						id: 'odi1d2SWy',
+						label: i18n.ts.alwaysConfirmFollow,
+						keywords: ['follow', 'confirm', 'always'],
+					},
+					{
+						id: 'm43Eu3Ypg',
+						label: i18n.ts.highlightSensitiveMedia,
+						keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
+					},
+					{
+						id: 'cjfAtxMzP',
+						label: i18n.ts.confirmWhenRevealingSensitiveMedia,
+						keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
+					},
+					{
+						id: 'aefexW9fD',
+						label: i18n.ts.emojiStyle,
+						keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
+					},
+					{
+						id: 'p7aiLj6A0',
+						label: i18n.ts.pinnedList,
+						keywords: ['pinned', 'list'],
+					},
+				],
+				label: i18n.ts.general,
+				keywords: ['general'],
+			},
+			{
+				id: 'khT3n6byY',
+				children: [
+					{
+						id: 'DftdlLbNu',
+						label: i18n.ts.showFixedPostForm,
+						keywords: ['post', 'form', 'timeline'],
+					},
+					{
+						id: 'FbhoeuRAD',
+						label: i18n.ts.showFixedPostFormInChannel,
+						keywords: ['post', 'form', 'timeline', 'channel'],
+					},
+					{
+						id: 'rq69GTeB4',
 						label: i18n.ts.collapseRenotes,
 						keywords: ['renote', i18n.ts.collapseRenotesDescription],
 					},
 					{
-						id: 'uJJyDABGu',
+						id: 'omxZk3eET',
+						label: i18n.ts.showGapBetweenNotesInTimeline,
+						keywords: ['note', 'timeline', 'gap'],
+					},
+					{
+						id: 'epvi2Nv2G',
+						label: i18n.ts.enableInfiniteScroll,
+						keywords: ['load', 'auto', 'more'],
+					},
+					{
+						id: 'v26JSj9mH',
+						label: i18n.ts.disableStreamingTimeline,
+						keywords: ['disable', 'streaming', 'timeline'],
+					},
+				],
+				label: i18n.ts.timeline,
+				keywords: ['timeline'],
+			},
+			{
+				id: '7Uf8ksn3q',
+				children: [
+					{
+						id: 'tLGyaQagB',
 						label: i18n.ts.showNoteActionsOnlyHover,
 						keywords: ['hover', 'show', 'footer', 'action'],
 					},
 					{
-						id: 'ufc2X9voy',
+						id: '7W6g8Dcqz',
 						label: i18n.ts.showClipButtonInNoteFooter,
 						keywords: ['footer', 'action', 'clip', 'show'],
 					},
 					{
-						id: '7Jwvu8bK6',
+						id: 'uAOoH3LFF',
 						label: i18n.ts.enableAdvancedMfm,
 						keywords: ['mfm', 'enable', 'show', 'advanced'],
 					},
 					{
-						id: 'yb11lSY1G',
+						id: 'eCiyZLC8n',
 						label: i18n.ts.showReactionsCount,
 						keywords: ['reaction', 'count', 'show'],
 					},
 					{
-						id: 'fL49Zxe9i',
+						id: '68u9uRmFP',
+						label: i18n.ts.confirmOnReact,
+						keywords: ['reaction', 'confirm'],
+					},
+					{
+						id: 'rHWm4sXIe',
 						label: i18n.ts.loadRawImages,
 						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'],
 					},
+					{
+						id: '9L2XGJw7e',
+						label: i18n.ts.useReactionPickerForContextMenu,
+						keywords: ['reaction', 'picker', 'contextmenu', 'open'],
+					},
+					{
+						id: 'uIMCIK7kG',
+						label: i18n.ts.reactionsDisplaySize,
+						keywords: ['reaction', 'size', 'scale', 'display'],
+					},
+					{
+						id: 'uMckjO9bz',
+						label: i18n.ts.limitWidthOfReaction,
+						keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
+					},
+					{
+						id: 'yeghU4qiH',
+						label: i18n.ts.mediaListWithOneImageAppearance,
+						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
+					},
+					{
+						id: 'yYSOPoAKE',
+						label: i18n.ts.instanceTicker,
+						keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
+					},
+					{
+						id: 'iOHiIu32L',
+						label: i18n.ts.displayOfSensitiveMedia,
+						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
+					},
 				],
 				label: i18n.ts.note,
 				keywords: ['note'],
 			},
 			{
-				id: 'bUOs2UKY4',
+				id: 'zrJicawH9',
 				children: [
 					{
-						id: 'c8gA9Xj2a',
+						id: 'iuEuPe6pa',
+						label: i18n.ts.keepCw,
+						keywords: ['remember', 'keep', 'note', 'cw'],
+					},
+					{
+						id: '9WrGgANqN',
+						label: i18n.ts.rememberNoteVisibility,
+						keywords: ['remember', 'keep', 'note', 'visibility'],
+					},
+					{
+						id: 'Cu7ErCM7C',
+						label: i18n.ts.enableQuickAddMfmFunction,
+						keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
+					},
+					{
+						id: 'oQl8xwiyI',
+						label: i18n.ts.defaultNoteVisibility,
+						keywords: ['default', 'note', 'visibility'],
+					},
+				],
+				label: i18n.ts.postForm,
+				keywords: ['post', 'form'],
+			},
+			{
+				id: 'xFmAg2tDe',
+				children: [
+					{
+						id: 'mepqKL5Ow',
 						label: i18n.ts.useGroupedNotifications,
 						keywords: ['group'],
 					},
+					{
+						id: 'wUuUOEO1g',
+						label: i18n.ts.position,
+						keywords: ['position'],
+					},
+					{
+						id: '27em8eC8R',
+						label: i18n.ts.stackAxis,
+						keywords: ['stack', 'axis', 'direction'],
+					},
 				],
 				label: i18n.ts.notifications,
 				keywords: ['notification'],
 			},
 			{
-				id: 'tjGzqy3qa',
+				id: 'AzymHsnrp',
 				children: [
 					{
-						id: '3OeHscv45',
+						id: 'DFUrEO2DI',
+						label: i18n.ts.squareAvatars,
+						keywords: ['avatar', 'icon', 'square'],
+					},
+					{
+						id: 'r9DX60AxL',
+						label: i18n.ts.seasonalScreenEffect,
+						keywords: ['effect', 'show'],
+					},
+					{
+						id: 'sJ3fqncSD',
 						label: i18n.ts.openImageInNewTab,
 						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
 					},
 					{
-						id: 'bFsNusspF',
-						label: i18n.ts.useReactionPickerForContextMenu,
-						keywords: ['reaction', 'picker', 'contextmenu', 'open'],
-					},
-					{
-						id: '2h3rY1izt',
-						label: i18n.ts.enableInfiniteScroll,
-						keywords: ['load', 'auto', 'more'],
-					},
-					{
-						id: 'pkK3eeFKm',
-						label: i18n.ts.disableStreamingTimeline,
-						keywords: ['disable', 'streaming', 'timeline'],
-					},
-					{
-						id: 'y2v7CV9zs',
-						label: i18n.ts.alwaysConfirmFollow,
-						keywords: ['follow', 'confirm', 'always'],
-					},
-					{
-						id: 'A8a5hcLce',
-						label: i18n.ts.confirmWhenRevealingSensitiveMedia,
-						keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
-					},
-					{
-						id: 'utFrfuW7X',
-						label: i18n.ts.confirmOnReact,
-						keywords: ['reaction', 'confirm'],
-					},
-					{
-						id: 'kmdsnVIQX',
-						label: i18n.ts.keepCw,
-						keywords: ['remember', 'keep', 'note', 'cw'],
-					},
-					{
-						id: 'mNRK0pt8L',
+						id: 'p7s0hwZ8A',
 						label: i18n.ts.whenServerDisconnected,
 						keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
 					},
 					{
-						id: 'vE7KeV4U4',
+						id: 'yCleENWNf',
 						label: i18n.ts.numberOfPageCache,
 						keywords: ['cache', 'page'],
 					},
 					{
-						id: 'eJ2jme16W',
-						label: i18n.ts.dataSaver,
-						keywords: ['datasaver'],
-					},
-				],
-				label: i18n.ts.behavior,
-				keywords: ['behavior'],
-			},
-			{
-				id: 'F3kpUNvSQ',
-				children: [
-					{
-						id: '4bfFRM0UD',
+						id: 'omEy5Q3Ev',
 						label: i18n.ts.forceShowAds,
 						keywords: ['ad', 'show'],
 					},
 					{
-						id: '2pB0jWBHo',
+						id: 'aWitQSBtD',
 						label: i18n.ts.hemisphere,
 						keywords: [],
 					},
 					{
-						id: 'eIvnR6Xxo',
+						id: 'hUQAXl1H4',
 						label: i18n.ts.additionalEmojiDictionary,
 						keywords: ['emoji', 'dictionary', 'additional', 'extra'],
 					},
 				],
 				label: i18n.ts.other,
-				keywords: [],
+				keywords: ['other'],
+			},
+			{
+				id: 'aSbKFHbOy',
+				label: i18n.ts.dataSaver,
+				keywords: ['datasaver'],
 			},
 		],
 		label: i18n.ts.preferences,
@@ -715,119 +804,6 @@ export const searchIndexes: SearchIndexItem[] = [
 		path: '/settings/avatar-decoration',
 		icon: 'ti ti-sparkles',
 	},
-	{
-		id: 'AqPvMgn3A',
-		children: [
-			{
-				id: '1wtOIwAdm',
-				label: i18n.ts.useBlurEffect,
-				keywords: ['blur'],
-			},
-			{
-				id: '6fLNMTwNt',
-				label: i18n.ts.useBlurEffectForModal,
-				keywords: ['blur', 'modal'],
-			},
-			{
-				id: 'E0WXhhRB1',
-				label: i18n.ts.highlightSensitiveMedia,
-				keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
-			},
-			{
-				id: '7iZsGkplG',
-				label: i18n.ts.squareAvatars,
-				keywords: ['avatar', 'icon', 'square'],
-			},
-			{
-				id: 'AfRMcC6IM',
-				label: i18n.ts.showAvatarDecorations,
-				keywords: ['avatar', 'icon', 'decoration', 'show'],
-			},
-			{
-				id: 'i7aSaEWaT',
-				label: i18n.ts.showGapBetweenNotesInTimeline,
-				keywords: ['note', 'timeline', 'gap'],
-			},
-			{
-				id: 'knj98Mx84',
-				label: i18n.ts.seasonalScreenEffect,
-				keywords: ['effect', 'show'],
-			},
-			{
-				id: 'Bzg77rYNd',
-				label: i18n.ts.menuStyle,
-				keywords: ['menu', 'style', 'popup', 'drawer'],
-			},
-			{
-				id: '7AOZ1ZgDv',
-				label: i18n.ts.emojiStyle,
-				keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
-			},
-			{
-				id: 'fDelHUrBi',
-				label: i18n.ts.fontSize,
-				keywords: ['font', 'size'],
-			},
-			{
-				id: 'siOW5aSwp',
-				label: i18n.ts.useSystemFont,
-				keywords: ['font', 'system', 'native'],
-			},
-			{
-				id: 's05dHQ1dW',
-				children: [
-					{
-						id: 'zoMbYCvP0',
-						label: i18n.ts.reactionsDisplaySize,
-						keywords: ['reaction', 'size', 'scale', 'display'],
-					},
-					{
-						id: 'lGFzLnWfB',
-						label: i18n.ts.limitWidthOfReaction,
-						keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
-					},
-					{
-						id: '9E0v8VKIY',
-						label: i18n.ts.mediaListWithOneImageAppearance,
-						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
-					},
-					{
-						id: 'xB7MPEF4Q',
-						label: i18n.ts.instanceTicker,
-						keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
-					},
-					{
-						id: '7siYCSodm',
-						label: i18n.ts.displayOfSensitiveMedia,
-						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
-					},
-				],
-				label: i18n.ts.displayOfNote,
-				keywords: ['note', 'display'],
-			},
-			{
-				id: 'uQfyiHMSs',
-				children: [
-					{
-						id: 'y3uTXsSQ6',
-						label: i18n.ts.position,
-						keywords: ['position'],
-					},
-					{
-						id: 'PILAdkVM',
-						label: i18n.ts.stackAxis,
-						keywords: ['stack', 'axis', 'direction'],
-					},
-				],
-				label: i18n.ts.notificationDisplay,
-				keywords: ['notification', 'display'],
-			},
-		],
-		label: i18n.ts.appearance,
-		keywords: ['appearance', i18n.ts._settings.appearanceBanner],
-		path: '/settings/appearance',
-		icon: 'ti ti-device-desktop',
-	},
 	{
 		id: '330Q4mf8E',
 		children: [
@@ -912,9 +888,24 @@ export const searchIndexes: SearchIndexItem[] = [
 			},
 			{
 				id: '1fV9WINCQ',
+				label: i18n.ts.menuStyle,
+				keywords: ['menu', 'style', 'popup', 'drawer'],
+			},
+			{
+				id: 'mLQzlKUNu',
 				label: i18n.ts._contextMenu.title,
 				keywords: ['contextmenu', 'system', 'native'],
 			},
+			{
+				id: 'yP96aA3j9',
+				label: i18n.ts.fontSize,
+				keywords: ['font', 'size'],
+			},
+			{
+				id: 'jQeiMopFE',
+				label: i18n.ts.useSystemFont,
+				keywords: ['font', 'system', 'native'],
+			},
 		],
 		label: i18n.ts.accessibility,
 		keywords: ['accessibility', i18n.ts._settings.accessibilityBanner],

From 59169a6450247c5e1c0694b38de30f8b77d26ed1 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 17:42:35 +0900
Subject: [PATCH 10/16] =?UTF-8?q?=F0=9F=8E=A8?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../frontend/src/pages/settings/index.vue     | 26 +------------------
 1 file changed, 1 insertion(+), 25 deletions(-)

diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 3b7c44fbfe..e8ba03005a 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div ref="el" class="vvcocwet" :class="{ wide: !narrow }">
 			<div class="body">
 				<div v-if="!narrow || currentPage?.route.name == null" class="nav">
-					<div class="baaadecd">
+					<div class="_gaps_s">
 						<MkInfo v-if="emailNotConfigured" warn class="info">{{ i18n.ts.emailNotConfiguredWarning }} <MkA to="/settings/email" class="_link">{{ i18n.ts.configure }}</MkA></MkInfo>
 						<MkInfo v-if="!store.r.enablePreferencesAutoCloudBackup.value && store.r.showPreferencesAutoCloudBackupSuggestion.value" class="info">
 							<div>{{ i18n.ts._preferencesBackup.autoPreferencesBackupIsNotEnabledForThisDevice }}</div>
@@ -249,30 +249,6 @@ definePage(() => INFO.value);
 
 <style lang="scss" scoped>
 .vvcocwet {
-	> .body {
-		> .nav {
-			.baaadecd {
-				> .info {
-					margin: 16px 0;
-				}
-
-				> .accounts {
-					> .avatar {
-						display: block;
-						width: 50px;
-						height: 50px;
-						margin: 8px auto 16px auto;
-					}
-				}
-			}
-		}
-
-		> .main {
-			.bkzroven {
-			}
-		}
-	}
-
 	&.wide {
 		> .body {
 			display: flex;

From bdf80c49d8e5d62363f930a3fbca29d40f868714 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 18:05:44 +0900
Subject: [PATCH 11/16] fix(frontend): better migration detection

Fix #15656
---
 packages/frontend/src/boot/common.ts    |   1 +
 packages/frontend/src/boot/main-boot.ts | 149 +++++++++++++-----------
 2 files changed, 80 insertions(+), 70 deletions(-)

diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index d597e8a20a..122aa50ac0 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -326,6 +326,7 @@ export async function common(createVue: () => App<Element>) {
 
 	return {
 		isClientUpdated,
+		lastVersion,
 		app,
 	};
 }
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 510e0509f4..0ad333b203 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -7,6 +7,7 @@ import { createApp, defineAsyncComponent, markRaw } from 'vue';
 import { ui } from '@@/js/config.js';
 import * as Misskey from 'misskey-js';
 import { v4 as uuid } from 'uuid';
+import { compareVersions } from 'compare-versions';
 import { common } from './common.js';
 import type { Component } from 'vue';
 import type { Keymap } from '@/utility/hotkey.js';
@@ -30,9 +31,10 @@ import { prefer } from '@/preferences.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { launchPlugins } from '@/plugin.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 
 export async function mainBoot() {
-	const { isClientUpdated } = await common(() => {
+	const { isClientUpdated, lastVersion } = await common(() => {
 		let uiStyle = ui;
 		const searchParams = new URLSearchParams(window.location.search);
 
@@ -72,75 +74,13 @@ export async function mainBoot() {
 		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {
 			closed: () => dispose(),
 		});
-	}
 
-	const stream = useStream();
+		// prefereces migration
+		// TODO: そのうち消す
+		if (lastVersion && (compareVersions('2025.3.2-alpha.0', lastVersion) === 1)) {
+			console.log('Preferences migration');
 
-	let reloadDialogShowing = false;
-	stream.on('_disconnected_', async () => {
-		if (prefer.s.serverDisconnectedBehavior === 'reload') {
-			location.reload();
-		} else if (prefer.s.serverDisconnectedBehavior === 'dialog') {
-			if (reloadDialogShowing) return;
-			reloadDialogShowing = true;
-			const { canceled } = await confirm({
-				type: 'warning',
-				title: i18n.ts.disconnectedFromServer,
-				text: i18n.ts.reloadConfirm,
-			});
-			reloadDialogShowing = false;
-			if (!canceled) {
-				location.reload();
-			}
-		}
-	});
-
-	stream.on('emojiAdded', emojiData => {
-		addCustomEmoji(emojiData.emoji);
-	});
-
-	stream.on('emojiUpdated', emojiData => {
-		updateCustomEmojis(emojiData.emojis);
-	});
-
-	stream.on('emojiDeleted', emojiData => {
-		removeCustomEmojis(emojiData.emojis);
-	});
-
-	launchPlugins();
-
-	try {
-		if (prefer.s.enableSeasonalScreenEffect) {
-			const month = new Date().getMonth() + 1;
-			if (prefer.s.hemisphere === 'S') {
-				// ▼南半球
-				if (month === 7 || month === 8) {
-					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
-					new SnowfallEffect({}).render();
-				}
-			} else {
-				// ▼北半球
-				if (month === 12 || month === 1) {
-					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
-					new SnowfallEffect({}).render();
-				} else if (month === 3 || month === 4) {
-					const SakuraEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
-					new SakuraEffect({
-						sakura: true,
-					}).render();
-				}
-			}
-		}
-	} catch (error) {
-		// console.error(error);
-		console.error('Failed to initialise the seasonal screen effect canvas context:', error);
-	}
-
-	if ($i) {
-		store.loaded.then(async () => {
-			// prefereces migration
-			// TODO: そのうち消す
-			if (store.s.menu.length > 0) {
+			store.loaded.then(async () => {
 				const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []);
 				if (themes.length > 0) {
 					prefer.commit('themes', themes);
@@ -258,9 +198,78 @@ export async function mainBoot() {
 				prefer.commit('sound.on.noteMy', store.s.sound_noteMy as any);
 				prefer.commit('sound.on.notification', store.s.sound_notification as any);
 				prefer.commit('sound.on.reaction', store.s.sound_reaction as any);
-				store.set('menu', []);
-			}
 
+				window.setTimeout(() => {
+					unisonReload();
+				}, 5000);
+			});
+		}
+	}
+
+	const stream = useStream();
+
+	let reloadDialogShowing = false;
+	stream.on('_disconnected_', async () => {
+		if (prefer.s.serverDisconnectedBehavior === 'reload') {
+			location.reload();
+		} else if (prefer.s.serverDisconnectedBehavior === 'dialog') {
+			if (reloadDialogShowing) return;
+			reloadDialogShowing = true;
+			const { canceled } = await confirm({
+				type: 'warning',
+				title: i18n.ts.disconnectedFromServer,
+				text: i18n.ts.reloadConfirm,
+			});
+			reloadDialogShowing = false;
+			if (!canceled) {
+				location.reload();
+			}
+		}
+	});
+
+	stream.on('emojiAdded', emojiData => {
+		addCustomEmoji(emojiData.emoji);
+	});
+
+	stream.on('emojiUpdated', emojiData => {
+		updateCustomEmojis(emojiData.emojis);
+	});
+
+	stream.on('emojiDeleted', emojiData => {
+		removeCustomEmojis(emojiData.emojis);
+	});
+
+	launchPlugins();
+
+	try {
+		if (prefer.s.enableSeasonalScreenEffect) {
+			const month = new Date().getMonth() + 1;
+			if (prefer.s.hemisphere === 'S') {
+				// ▼南半球
+				if (month === 7 || month === 8) {
+					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
+					new SnowfallEffect({}).render();
+				}
+			} else {
+				// ▼北半球
+				if (month === 12 || month === 1) {
+					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
+					new SnowfallEffect({}).render();
+				} else if (month === 3 || month === 4) {
+					const SakuraEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
+					new SakuraEffect({
+						sakura: true,
+					}).render();
+				}
+			}
+		}
+	} catch (error) {
+		// console.error(error);
+		console.error('Failed to initialise the seasonal screen effect canvas context:', error);
+	}
+
+	if ($i) {
+		store.loaded.then(async () => {
 			if (store.s.accountSetupWizard !== -1) {
 				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
 					closed: () => dispose(),

From 3280a3d661a9ca8841e3b2db853ac99284cfbcac Mon Sep 17 00:00:00 2001
From: "github-actions[bot]" <github-actions[bot]@users.noreply.github.com>
Date: Thu, 13 Mar 2025 09:07:37 +0000
Subject: [PATCH 12/16] Bump version to 2025.3.2-alpha.10

---
 package.json                     | 2 +-
 packages/misskey-js/package.json | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/package.json b/package.json
index 3367c475cc..dfbfe88bc4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2025.3.2-alpha.9",
+	"version": "2025.3.2-alpha.10",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 945ea588b4..5b7a4ca610 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
 {
 	"type": "module",
 	"name": "misskey-js",
-	"version": "2025.3.2-alpha.9",
+	"version": "2025.3.2-alpha.10",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",

From 0126dba475c2dea706c6157c7baecc154a013772 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 19:30:35 +0900
Subject: [PATCH 13/16] enhance(frontend): re-organize settings page

---
 .../frontend/src/pages/settings/index.vue     |   5 -
 .../frontend/src/pages/settings/other.vue     |  21 +
 .../src/pages/settings/preferences.vue        | 972 +++++++++---------
 .../frontend/src/pages/settings/roles.vue     |  48 -
 packages/frontend/src/router/definition.ts    |   4 -
 .../utility/autogen/settings-search-index.ts  | 115 ++-
 6 files changed, 568 insertions(+), 597 deletions(-)
 delete mode 100644 packages/frontend/src/pages/settings/roles.vue

diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index e8ba03005a..0579b6d14a 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -140,11 +140,6 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
 		text: i18n.ts.drive,
 		to: '/settings/drive',
 		active: currentPage.value?.route.name === 'drive',
-	}, {
-		icon: 'ti ti-badges',
-		text: i18n.ts.roles,
-		to: '/settings/roles',
-		active: currentPage.value?.route.name === 'roles',
 	}, {
 		icon: 'ti ti-ban',
 		text: i18n.ts.muteAndBlock,
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 835739a6c6..b60db78071 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -33,10 +33,30 @@ SPDX-License-Identifier: AGPL-3.0-only
 								<template #key>{{ i18n.ts.registeredDate }}</template>
 								<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
 							</MkKeyValue>
+
+							<MkFolder>
+								<template #icon><i class="ti ti-badges"></i></template>
+								<template #label><SearchLabel>{{ i18n.ts._role.policies }}</SearchLabel></template>
+
+								<div class="_gaps_s">
+									<div v-for="policy in Object.keys($i.policies)" :key="policy">
+										{{ policy }} ... {{ $i.policies[policy] }}
+									</div>
+								</div>
+							</MkFolder>
 						</div>
 					</MkFolder>
 				</SearchMarker>
 
+				<SearchMarker :keywords="['roles']">
+					<MkFolder>
+						<template #icon><i class="ti ti-badges"></i></template>
+						<template #label><SearchLabel>{{ i18n.ts.rolesAssignedToMe }}</SearchLabel></template>
+
+						<MkRolePreview v-for="role in $i.roles" :key="role.id" :role="role" :forModeration="false"/>
+					</MkFolder>
+				</SearchMarker>
+
 				<SearchMarker :keywords="['account', 'move', 'migration']">
 					<MkFolder>
 						<template #icon><i class="ti ti-plane"></i></template>
@@ -124,6 +144,7 @@ import { definePage } from '@/page.js';
 import { reloadAsk } from '@/utility/reload-ask.js';
 import FormSection from '@/components/form/section.vue';
 import { prefer } from '@/preferences.js';
+import MkRolePreview from '@/components/MkRolePreview.vue';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index b9a596067c..94d154e9c7 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -10,510 +10,512 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<SearchKeyword>{{ i18n.ts._settings.preferencesBanner }}</SearchKeyword>
 		</MkFeatureBanner>
 
-		<SearchMarker :keywords="['general']">
-			<MkFolder :defaultOpen="true">
-				<template #label><SearchLabel>{{ i18n.ts.general }}</SearchLabel></template>
+		<div class="_gaps_s">
+			<SearchMarker :keywords="['general']">
+				<MkFolder>
+					<template #label><SearchLabel>{{ i18n.ts.general }}</SearchLabel></template>
 
-				<div class="_gaps_m">
-					<SearchMarker :keywords="['language']">
-						<MkSelect v-model="lang">
-							<template #label><SearchLabel>{{ i18n.ts.uiLanguage }}</SearchLabel></template>
-							<option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
-							<template #caption>
-								<I18n :src="i18n.ts.i18nInfo" tag="span">
-									<template #link>
-										<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
-									</template>
-								</I18n>
-							</template>
-						</MkSelect>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop']">
-						<MkRadios v-model="overridedDeviceKind">
-							<template #label><SearchLabel>{{ i18n.ts.overridedDeviceKind }}</SearchLabel></template>
-							<option :value="null">{{ i18n.ts.auto }}</option>
-							<option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
-							<option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option>
-							<option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
-						</MkRadios>
-					</SearchMarker>
-
-					<div class="_gaps_s">
-						<SearchMarker :keywords="['blur']">
-							<MkPreferenceContainer k="useBlurEffect">
-								<MkSwitch v-model="useBlurEffect">
-									<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['blur', 'modal']">
-							<MkPreferenceContainer k="useBlurEffectForModal">
-								<MkSwitch v-model="useBlurEffectForModal">
-									<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['avatar', 'icon', 'decoration', 'show']">
-							<MkPreferenceContainer k="showAvatarDecorations">
-								<MkSwitch v-model="showAvatarDecorations">
-									<template #label><SearchLabel>{{ i18n.ts.showAvatarDecorations }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['follow', 'confirm', 'always']">
-							<MkPreferenceContainer k="alwaysConfirmFollow">
-								<MkSwitch v-model="alwaysConfirmFollow">
-									<template #label><SearchLabel>{{ i18n.ts.alwaysConfirmFollow }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail']">
-							<MkPreferenceContainer k="highlightSensitiveMedia">
-								<MkSwitch v-model="highlightSensitiveMedia">
-									<template #label><SearchLabel>{{ i18n.ts.highlightSensitiveMedia }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm']">
-							<MkPreferenceContainer k="confirmWhenRevealingSensitiveMedia">
-								<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">
-									<template #label><SearchLabel>{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-					</div>
-
-					<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">
-						<MkPreferenceContainer k="emojiStyle">
-							<div>
-								<MkRadios v-model="emojiStyle">
-									<template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template>
-									<option value="native">{{ i18n.ts.native }}</option>
-									<option value="fluentEmoji">Fluent Emoji</option>
-									<option value="twemoji">Twemoji</option>
-								</MkRadios>
-								<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
-							</div>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['pinned', 'list']">
-						<MkFolder>
-							<template #label><SearchLabel>{{ i18n.ts.pinnedList }}</SearchLabel></template>
-							<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
-							<MkButton v-if="prefer.r.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
-							<MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
-						</MkFolder>
-					</SearchMarker>
-				</div>
-			</MkFolder>
-		</SearchMarker>
-
-		<SearchMarker :keywords="['timeline']">
-			<MkFolder :defaultOpen="true">
-				<template #label><SearchLabel>{{ i18n.ts.timeline }}</SearchLabel></template>
-
-				<div class="_gaps_s">
-					<SearchMarker :keywords="['post', 'form', 'timeline']">
-						<MkPreferenceContainer k="showFixedPostForm">
-							<MkSwitch v-model="showFixedPostForm">
-								<template #label><SearchLabel>{{ i18n.ts.showFixedPostForm }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['post', 'form', 'timeline', 'channel']">
-						<MkPreferenceContainer k="showFixedPostFormInChannel">
-							<MkSwitch v-model="showFixedPostFormInChannel">
-								<template #label><SearchLabel>{{ i18n.ts.showFixedPostFormInChannel }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['renote']">
-						<MkPreferenceContainer k="collapseRenotes">
-							<MkSwitch v-model="collapseRenotes">
-								<template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template>
-								<template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['note', 'timeline', 'gap']">
-						<MkPreferenceContainer k="showGapBetweenNotesInTimeline">
-							<MkSwitch v-model="showGapBetweenNotesInTimeline">
-								<template #label><SearchLabel>{{ i18n.ts.showGapBetweenNotesInTimeline }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['load', 'auto', 'more']">
-						<MkPreferenceContainer k="enableInfiniteScroll">
-							<MkSwitch v-model="enableInfiniteScroll">
-								<template #label><SearchLabel>{{ i18n.ts.enableInfiniteScroll }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['disable', 'streaming', 'timeline']">
-						<MkPreferenceContainer k="disableStreamingTimeline">
-							<MkSwitch v-model="disableStreamingTimeline">
-								<template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-				</div>
-			</MkFolder>
-		</SearchMarker>
-
-		<SearchMarker :keywords="['note']">
-			<MkFolder :defaultOpen="true">
-				<template #label><SearchLabel>{{ i18n.ts.note }}</SearchLabel></template>
-
-				<div class="_gaps_m">
-					<div class="_gaps_s">
-						<SearchMarker :keywords="['hover', 'show', 'footer', 'action']">
-							<MkPreferenceContainer k="showNoteActionsOnlyHover">
-								<MkSwitch v-model="showNoteActionsOnlyHover">
-									<template #label><SearchLabel>{{ i18n.ts.showNoteActionsOnlyHover }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['footer', 'action', 'clip', 'show']">
-							<MkPreferenceContainer k="showClipButtonInNoteFooter">
-								<MkSwitch v-model="showClipButtonInNoteFooter">
-									<template #label><SearchLabel>{{ i18n.ts.showClipButtonInNoteFooter }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced']">
-							<MkPreferenceContainer k="advancedMfm">
-								<MkSwitch v-model="advancedMfm">
-									<template #label><SearchLabel>{{ i18n.ts.enableAdvancedMfm }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['reaction', 'count', 'show']">
-							<MkPreferenceContainer k="showReactionsCount">
-								<MkSwitch v-model="showReactionsCount">
-									<template #label><SearchLabel>{{ i18n.ts.showReactionsCount }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['reaction', 'confirm']">
-							<MkPreferenceContainer k="confirmOnReact">
-								<MkSwitch v-model="confirmOnReact">
-									<template #label><SearchLabel>{{ i18n.ts.confirmOnReact }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment']">
-							<MkPreferenceContainer k="loadRawImages">
-								<MkSwitch v-model="loadRawImages">
-									<template #label><SearchLabel>{{ i18n.ts.loadRawImages }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['reaction', 'picker', 'contextmenu', 'open']">
-							<MkPreferenceContainer k="useReactionPickerForContextMenu">
-								<MkSwitch v-model="useReactionPickerForContextMenu">
-									<template #label><SearchLabel>{{ i18n.ts.useReactionPickerForContextMenu }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-					</div>
-
-					<SearchMarker :keywords="['reaction', 'size', 'scale', 'display']">
-						<MkPreferenceContainer k="reactionsDisplaySize">
-							<MkRadios v-model="reactionsDisplaySize">
-								<template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template>
-								<option value="small">{{ i18n.ts.small }}</option>
-								<option value="medium">{{ i18n.ts.medium }}</option>
-								<option value="large">{{ i18n.ts.large }}</option>
-							</MkRadios>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['reaction', 'size', 'scale', 'display', 'width', 'limit']">
-						<MkPreferenceContainer k="limitWidthOfReaction">
-							<MkSwitch v-model="limitWidthOfReaction">
-								<template #label><SearchLabel>{{ i18n.ts.limitWidthOfReaction }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height']">
-						<MkPreferenceContainer k="mediaListWithOneImageAppearance">
-							<MkRadios v-model="mediaListWithOneImageAppearance">
-								<template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template>
-								<option value="expand">{{ i18n.ts.default }}</option>
-								<option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
-								<option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
-								<option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
-							</MkRadios>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation']">
-						<MkPreferenceContainer k="instanceTicker">
-							<MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker">
-								<template #label><SearchLabel>{{ i18n.ts.instanceTicker }}</SearchLabel></template>
-								<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
-								<option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
-								<option value="always">{{ i18n.ts._instanceTicker.always }}</option>
+					<div class="_gaps_m">
+						<SearchMarker :keywords="['language']">
+							<MkSelect v-model="lang">
+								<template #label><SearchLabel>{{ i18n.ts.uiLanguage }}</SearchLabel></template>
+								<option v-for="x in langs" :key="x[0]" :value="x[0]">{{ x[1] }}</option>
+								<template #caption>
+									<I18n :src="i18n.ts.i18nInfo" tag="span">
+										<template #link>
+											<MkLink url="https://crowdin.com/project/misskey">Crowdin</MkLink>
+										</template>
+									</I18n>
+								</template>
 							</MkSelect>
-						</MkPreferenceContainer>
-					</SearchMarker>
+						</SearchMarker>
 
-					<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility']">
-						<MkPreferenceContainer k="nsfw">
-							<MkSelect v-model="nsfw">
-								<template #label><SearchLabel>{{ i18n.ts.displayOfSensitiveMedia }}</SearchLabel></template>
-								<option value="respect">{{ i18n.ts._displayOfSensitiveMedia.respect }}</option>
-								<option value="ignore">{{ i18n.ts._displayOfSensitiveMedia.ignore }}</option>
-								<option value="force">{{ i18n.ts._displayOfSensitiveMedia.force }}</option>
-							</MkSelect>
-						</MkPreferenceContainer>
-					</SearchMarker>
-				</div>
-			</MkFolder>
-		</SearchMarker>
+						<SearchMarker :keywords="['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop']">
+							<MkRadios v-model="overridedDeviceKind">
+								<template #label><SearchLabel>{{ i18n.ts.overridedDeviceKind }}</SearchLabel></template>
+								<option :value="null">{{ i18n.ts.auto }}</option>
+								<option value="smartphone"><i class="ti ti-device-mobile"/> {{ i18n.ts.smartphone }}</option>
+								<option value="tablet"><i class="ti ti-device-tablet"/> {{ i18n.ts.tablet }}</option>
+								<option value="desktop"><i class="ti ti-device-desktop"/> {{ i18n.ts.desktop }}</option>
+							</MkRadios>
+						</SearchMarker>
 
-		<SearchMarker :keywords="['post', 'form']">
-			<MkFolder :defaultOpen="true">
-				<template #label><SearchLabel>{{ i18n.ts.postForm }}</SearchLabel></template>
+						<div class="_gaps_s">
+							<SearchMarker :keywords="['blur']">
+								<MkPreferenceContainer k="useBlurEffect">
+									<MkSwitch v-model="useBlurEffect">
+										<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
 
-				<div class="_gaps_m">
-					<div class="_gaps_s">
-						<SearchMarker :keywords="['remember', 'keep', 'note', 'cw']">
-							<MkPreferenceContainer k="keepCw">
-								<MkSwitch v-model="keepCw">
-									<template #label><SearchLabel>{{ i18n.ts.keepCw }}</SearchLabel></template>
-								</MkSwitch>
+							<SearchMarker :keywords="['blur', 'modal']">
+								<MkPreferenceContainer k="useBlurEffectForModal">
+									<MkSwitch v-model="useBlurEffectForModal">
+										<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['avatar', 'icon', 'decoration', 'show']">
+								<MkPreferenceContainer k="showAvatarDecorations">
+									<MkSwitch v-model="showAvatarDecorations">
+										<template #label><SearchLabel>{{ i18n.ts.showAvatarDecorations }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['follow', 'confirm', 'always']">
+								<MkPreferenceContainer k="alwaysConfirmFollow">
+									<MkSwitch v-model="alwaysConfirmFollow">
+										<template #label><SearchLabel>{{ i18n.ts.alwaysConfirmFollow }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail']">
+								<MkPreferenceContainer k="highlightSensitiveMedia">
+									<MkSwitch v-model="highlightSensitiveMedia">
+										<template #label><SearchLabel>{{ i18n.ts.highlightSensitiveMedia }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm']">
+								<MkPreferenceContainer k="confirmWhenRevealingSensitiveMedia">
+									<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">
+										<template #label><SearchLabel>{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+						</div>
+
+						<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">
+							<MkPreferenceContainer k="emojiStyle">
+								<div>
+									<MkRadios v-model="emojiStyle">
+										<template #label><SearchLabel>{{ i18n.ts.emojiStyle }}</SearchLabel></template>
+										<option value="native">{{ i18n.ts.native }}</option>
+										<option value="fluentEmoji">Fluent Emoji</option>
+										<option value="twemoji">Twemoji</option>
+									</MkRadios>
+									<div style="margin: 8px 0 0 0; font-size: 1.5em;"><Mfm :key="emojiStyle" text="🍮🍦🍭🍩🍰🍫🍬🥞🍪"/></div>
+								</div>
 							</MkPreferenceContainer>
 						</SearchMarker>
 
-						<SearchMarker :keywords="['remember', 'keep', 'note', 'visibility']">
-							<MkPreferenceContainer k="rememberNoteVisibility">
-								<MkSwitch v-model="rememberNoteVisibility">
-									<template #label><SearchLabel>{{ i18n.ts.rememberNoteVisibility }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn']">
-							<MkPreferenceContainer k="enableQuickAddMfmFunction">
-								<MkSwitch v-model="enableQuickAddMfmFunction">
-									<template #label><SearchLabel>{{ i18n.ts.enableQuickAddMfmFunction }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-					</div>
-
-					<SearchMarker :keywords="['default', 'note', 'visibility']">
-						<MkDisableSection :disabled="rememberNoteVisibility">
+						<SearchMarker :keywords="['pinned', 'list']">
 							<MkFolder>
-								<template #label><SearchLabel>{{ i18n.ts.defaultNoteVisibility }}</SearchLabel></template>
-								<template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
-								<template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template>
-								<template v-else-if="defaultNoteVisibility === 'followers'" #suffix>{{ i18n.ts._visibility.followers }}</template>
-								<template v-else-if="defaultNoteVisibility === 'specified'" #suffix>{{ i18n.ts._visibility.specified }}</template>
+								<template #label><SearchLabel>{{ i18n.ts.pinnedList }}</SearchLabel></template>
+								<!-- 複数ピン止め管理できるようにしたいけどめんどいので一旦ひとつのみ -->
+								<MkButton v-if="prefer.r.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
+								<MkButton v-else danger @click="removePinnedList()"><i class="ti ti-trash"></i> {{ i18n.ts.remove }}</MkButton>
+							</MkFolder>
+						</SearchMarker>
+					</div>
+				</MkFolder>
+			</SearchMarker>
 
-								<div class="_gaps_m">
-									<MkPreferenceContainer k="defaultNoteVisibility">
-										<MkSelect v-model="defaultNoteVisibility">
-											<option value="public">{{ i18n.ts._visibility.public }}</option>
-											<option value="home">{{ i18n.ts._visibility.home }}</option>
-											<option value="followers">{{ i18n.ts._visibility.followers }}</option>
-											<option value="specified">{{ i18n.ts._visibility.specified }}</option>
-										</MkSelect>
-									</MkPreferenceContainer>
+			<SearchMarker :keywords="['timeline']">
+				<MkFolder>
+					<template #label><SearchLabel>{{ i18n.ts.timeline }}</SearchLabel></template>
 
-									<MkPreferenceContainer k="defaultNoteLocalOnly">
-										<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch>
-									</MkPreferenceContainer>
+					<div class="_gaps_s">
+						<SearchMarker :keywords="['post', 'form', 'timeline']">
+							<MkPreferenceContainer k="showFixedPostForm">
+								<MkSwitch v-model="showFixedPostForm">
+									<template #label><SearchLabel>{{ i18n.ts.showFixedPostForm }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['post', 'form', 'timeline', 'channel']">
+							<MkPreferenceContainer k="showFixedPostFormInChannel">
+								<MkSwitch v-model="showFixedPostFormInChannel">
+									<template #label><SearchLabel>{{ i18n.ts.showFixedPostFormInChannel }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['renote']">
+							<MkPreferenceContainer k="collapseRenotes">
+								<MkSwitch v-model="collapseRenotes">
+									<template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template>
+									<template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['note', 'timeline', 'gap']">
+							<MkPreferenceContainer k="showGapBetweenNotesInTimeline">
+								<MkSwitch v-model="showGapBetweenNotesInTimeline">
+									<template #label><SearchLabel>{{ i18n.ts.showGapBetweenNotesInTimeline }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['load', 'auto', 'more']">
+							<MkPreferenceContainer k="enableInfiniteScroll">
+								<MkSwitch v-model="enableInfiniteScroll">
+									<template #label><SearchLabel>{{ i18n.ts.enableInfiniteScroll }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['disable', 'streaming', 'timeline']">
+							<MkPreferenceContainer k="disableStreamingTimeline">
+								<MkSwitch v-model="disableStreamingTimeline">
+									<template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+					</div>
+				</MkFolder>
+			</SearchMarker>
+
+			<SearchMarker :keywords="['note']">
+				<MkFolder>
+					<template #label><SearchLabel>{{ i18n.ts.note }}</SearchLabel></template>
+
+					<div class="_gaps_m">
+						<div class="_gaps_s">
+							<SearchMarker :keywords="['hover', 'show', 'footer', 'action']">
+								<MkPreferenceContainer k="showNoteActionsOnlyHover">
+									<MkSwitch v-model="showNoteActionsOnlyHover">
+										<template #label><SearchLabel>{{ i18n.ts.showNoteActionsOnlyHover }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['footer', 'action', 'clip', 'show']">
+								<MkPreferenceContainer k="showClipButtonInNoteFooter">
+									<MkSwitch v-model="showClipButtonInNoteFooter">
+										<template #label><SearchLabel>{{ i18n.ts.showClipButtonInNoteFooter }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced']">
+								<MkPreferenceContainer k="advancedMfm">
+									<MkSwitch v-model="advancedMfm">
+										<template #label><SearchLabel>{{ i18n.ts.enableAdvancedMfm }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['reaction', 'count', 'show']">
+								<MkPreferenceContainer k="showReactionsCount">
+									<MkSwitch v-model="showReactionsCount">
+										<template #label><SearchLabel>{{ i18n.ts.showReactionsCount }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['reaction', 'confirm']">
+								<MkPreferenceContainer k="confirmOnReact">
+									<MkSwitch v-model="confirmOnReact">
+										<template #label><SearchLabel>{{ i18n.ts.confirmOnReact }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment']">
+								<MkPreferenceContainer k="loadRawImages">
+									<MkSwitch v-model="loadRawImages">
+										<template #label><SearchLabel>{{ i18n.ts.loadRawImages }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['reaction', 'picker', 'contextmenu', 'open']">
+								<MkPreferenceContainer k="useReactionPickerForContextMenu">
+									<MkSwitch v-model="useReactionPickerForContextMenu">
+										<template #label><SearchLabel>{{ i18n.ts.useReactionPickerForContextMenu }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+						</div>
+
+						<SearchMarker :keywords="['reaction', 'size', 'scale', 'display']">
+							<MkPreferenceContainer k="reactionsDisplaySize">
+								<MkRadios v-model="reactionsDisplaySize">
+									<template #label><SearchLabel>{{ i18n.ts.reactionsDisplaySize }}</SearchLabel></template>
+									<option value="small">{{ i18n.ts.small }}</option>
+									<option value="medium">{{ i18n.ts.medium }}</option>
+									<option value="large">{{ i18n.ts.large }}</option>
+								</MkRadios>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['reaction', 'size', 'scale', 'display', 'width', 'limit']">
+							<MkPreferenceContainer k="limitWidthOfReaction">
+								<MkSwitch v-model="limitWidthOfReaction">
+									<template #label><SearchLabel>{{ i18n.ts.limitWidthOfReaction }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height']">
+							<MkPreferenceContainer k="mediaListWithOneImageAppearance">
+								<MkRadios v-model="mediaListWithOneImageAppearance">
+									<template #label><SearchLabel>{{ i18n.ts.mediaListWithOneImageAppearance }}</SearchLabel></template>
+									<option value="expand">{{ i18n.ts.default }}</option>
+									<option value="16_9">{{ i18n.tsx.limitTo({ x: '16:9' }) }}</option>
+									<option value="1_1">{{ i18n.tsx.limitTo({ x: '1:1' }) }}</option>
+									<option value="2_3">{{ i18n.tsx.limitTo({ x: '2:3' }) }}</option>
+								</MkRadios>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation']">
+							<MkPreferenceContainer k="instanceTicker">
+								<MkSelect v-if="instance.federation !== 'none'" v-model="instanceTicker">
+									<template #label><SearchLabel>{{ i18n.ts.instanceTicker }}</SearchLabel></template>
+									<option value="none">{{ i18n.ts._instanceTicker.none }}</option>
+									<option value="remote">{{ i18n.ts._instanceTicker.remote }}</option>
+									<option value="always">{{ i18n.ts._instanceTicker.always }}</option>
+								</MkSelect>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility']">
+							<MkPreferenceContainer k="nsfw">
+								<MkSelect v-model="nsfw">
+									<template #label><SearchLabel>{{ i18n.ts.displayOfSensitiveMedia }}</SearchLabel></template>
+									<option value="respect">{{ i18n.ts._displayOfSensitiveMedia.respect }}</option>
+									<option value="ignore">{{ i18n.ts._displayOfSensitiveMedia.ignore }}</option>
+									<option value="force">{{ i18n.ts._displayOfSensitiveMedia.force }}</option>
+								</MkSelect>
+							</MkPreferenceContainer>
+						</SearchMarker>
+					</div>
+				</MkFolder>
+			</SearchMarker>
+
+			<SearchMarker :keywords="['post', 'form']">
+				<MkFolder>
+					<template #label><SearchLabel>{{ i18n.ts.postForm }}</SearchLabel></template>
+
+					<div class="_gaps_m">
+						<div class="_gaps_s">
+							<SearchMarker :keywords="['remember', 'keep', 'note', 'cw']">
+								<MkPreferenceContainer k="keepCw">
+									<MkSwitch v-model="keepCw">
+										<template #label><SearchLabel>{{ i18n.ts.keepCw }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['remember', 'keep', 'note', 'visibility']">
+								<MkPreferenceContainer k="rememberNoteVisibility">
+									<MkSwitch v-model="rememberNoteVisibility">
+										<template #label><SearchLabel>{{ i18n.ts.rememberNoteVisibility }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn']">
+								<MkPreferenceContainer k="enableQuickAddMfmFunction">
+									<MkSwitch v-model="enableQuickAddMfmFunction">
+										<template #label><SearchLabel>{{ i18n.ts.enableQuickAddMfmFunction }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+						</div>
+
+						<SearchMarker :keywords="['default', 'note', 'visibility']">
+							<MkDisableSection :disabled="rememberNoteVisibility">
+								<MkFolder>
+									<template #label><SearchLabel>{{ i18n.ts.defaultNoteVisibility }}</SearchLabel></template>
+									<template v-if="defaultNoteVisibility === 'public'" #suffix>{{ i18n.ts._visibility.public }}</template>
+									<template v-else-if="defaultNoteVisibility === 'home'" #suffix>{{ i18n.ts._visibility.home }}</template>
+									<template v-else-if="defaultNoteVisibility === 'followers'" #suffix>{{ i18n.ts._visibility.followers }}</template>
+									<template v-else-if="defaultNoteVisibility === 'specified'" #suffix>{{ i18n.ts._visibility.specified }}</template>
+
+									<div class="_gaps_m">
+										<MkPreferenceContainer k="defaultNoteVisibility">
+											<MkSelect v-model="defaultNoteVisibility">
+												<option value="public">{{ i18n.ts._visibility.public }}</option>
+												<option value="home">{{ i18n.ts._visibility.home }}</option>
+												<option value="followers">{{ i18n.ts._visibility.followers }}</option>
+												<option value="specified">{{ i18n.ts._visibility.specified }}</option>
+											</MkSelect>
+										</MkPreferenceContainer>
+
+										<MkPreferenceContainer k="defaultNoteLocalOnly">
+											<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch>
+										</MkPreferenceContainer>
+									</div>
+								</MkFolder>
+							</MkDisableSection>
+						</SearchMarker>
+					</div>
+				</MkFolder>
+			</SearchMarker>
+
+			<SearchMarker :keywords="['notification']">
+				<MkFolder>
+					<template #label><SearchLabel>{{ i18n.ts.notifications }}</SearchLabel></template>
+
+					<div class="_gaps_m">
+						<SearchMarker :keywords="['group']">
+							<MkPreferenceContainer k="useGroupedNotifications">
+								<MkSwitch v-model="useGroupedNotifications">
+									<template #label><SearchLabel>{{ i18n.ts.useGroupedNotifications }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['position']">
+							<MkPreferenceContainer k="notificationPosition">
+								<MkRadios v-model="notificationPosition">
+									<template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template>
+									<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
+									<option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option>
+									<option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option>
+									<option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option>
+								</MkRadios>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['stack', 'axis', 'direction']">
+							<MkPreferenceContainer k="notificationStackAxis">
+								<MkRadios v-model="notificationStackAxis">
+									<template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template>
+									<option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
+									<option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
+								</MkRadios>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton>
+					</div>
+				</MkFolder>
+			</SearchMarker>
+
+			<SearchMarker :keywords="['other']">
+				<MkFolder>
+					<template #label><SearchLabel>{{ i18n.ts.other }}</SearchLabel></template>
+
+					<div class="_gaps_m">
+						<div class="_gaps_s">
+							<SearchMarker :keywords="['avatar', 'icon', 'square']">
+								<MkPreferenceContainer k="squareAvatars">
+									<MkSwitch v-model="squareAvatars">
+										<template #label><SearchLabel>{{ i18n.ts.squareAvatars }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['effect', 'show']">
+								<MkPreferenceContainer k="enableSeasonalScreenEffect">
+									<MkSwitch v-model="enableSeasonalScreenEffect">
+										<template #label><SearchLabel>{{ i18n.ts.seasonalScreenEffect }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+
+							<SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab']">
+								<MkPreferenceContainer k="imageNewTab">
+									<MkSwitch v-model="imageNewTab">
+										<template #label><SearchLabel>{{ i18n.ts.openImageInNewTab }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
+						</div>
+
+						<SearchMarker :keywords="['server', 'disconnect', 'reconnect', 'reload', 'streaming']">
+							<MkPreferenceContainer k="serverDisconnectedBehavior">
+								<MkSelect v-model="serverDisconnectedBehavior">
+									<template #label><SearchLabel>{{ i18n.ts.whenServerDisconnected }}</SearchLabel></template>
+									<option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
+									<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
+									<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
+								</MkSelect>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['cache', 'page']">
+							<MkPreferenceContainer k="numberOfPageCache">
+								<MkRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing>
+									<template #label><SearchLabel>{{ i18n.ts.numberOfPageCache }}</SearchLabel></template>
+									<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
+								</MkRange>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['ad', 'show']">
+							<MkPreferenceContainer k="forceShowAds">
+								<MkSwitch v-model="forceShowAds">
+									<template #label><SearchLabel>{{ i18n.ts.forceShowAds }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker>
+							<MkPreferenceContainer k="hemisphere">
+								<MkRadios v-model="hemisphere">
+									<template #label><SearchLabel>{{ i18n.ts.hemisphere }}</SearchLabel></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>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['emoji', 'dictionary', 'additional', 'extra']">
+							<MkFolder>
+								<template #label><SearchLabel>{{ i18n.ts.additionalEmojiDictionary }}</SearchLabel></template>
+								<div class="_buttons">
+									<template v-for="lang in emojiIndexLangs" :key="lang">
+										<MkButton v-if="store.r.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
+										<MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ store.r.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
+									</template>
 								</div>
 							</MkFolder>
-						</MkDisableSection>
-					</SearchMarker>
-				</div>
-			</MkFolder>
-		</SearchMarker>
-
-		<SearchMarker :keywords="['notification']">
-			<MkFolder :defaultOpen="true">
-				<template #label><SearchLabel>{{ i18n.ts.notifications }}</SearchLabel></template>
-
-				<div class="_gaps_m">
-					<SearchMarker :keywords="['group']">
-						<MkPreferenceContainer k="useGroupedNotifications">
-							<MkSwitch v-model="useGroupedNotifications">
-								<template #label><SearchLabel>{{ i18n.ts.useGroupedNotifications }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['position']">
-						<MkPreferenceContainer k="notificationPosition">
-							<MkRadios v-model="notificationPosition">
-								<template #label><SearchLabel>{{ i18n.ts.position }}</SearchLabel></template>
-								<option value="leftTop"><i class="ti ti-align-box-left-top"></i> {{ i18n.ts.leftTop }}</option>
-								<option value="rightTop"><i class="ti ti-align-box-right-top"></i> {{ i18n.ts.rightTop }}</option>
-								<option value="leftBottom"><i class="ti ti-align-box-left-bottom"></i> {{ i18n.ts.leftBottom }}</option>
-								<option value="rightBottom"><i class="ti ti-align-box-right-bottom"></i> {{ i18n.ts.rightBottom }}</option>
-							</MkRadios>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['stack', 'axis', 'direction']">
-						<MkPreferenceContainer k="notificationStackAxis">
-							<MkRadios v-model="notificationStackAxis">
-								<template #label><SearchLabel>{{ i18n.ts.stackAxis }}</SearchLabel></template>
-								<option value="vertical"><i class="ti ti-carousel-vertical"></i> {{ i18n.ts.vertical }}</option>
-								<option value="horizontal"><i class="ti ti-carousel-horizontal"></i> {{ i18n.ts.horizontal }}</option>
-							</MkRadios>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<MkButton @click="testNotification">{{ i18n.ts._notification.checkNotificationBehavior }}</MkButton>
-				</div>
-			</MkFolder>
-		</SearchMarker>
-
-		<SearchMarker :keywords="['other']">
-			<MkFolder :defaultOpen="true">
-				<template #label><SearchLabel>{{ i18n.ts.other }}</SearchLabel></template>
-
-				<div class="_gaps_m">
-					<div class="_gaps_s">
-						<SearchMarker :keywords="['avatar', 'icon', 'square']">
-							<MkPreferenceContainer k="squareAvatars">
-								<MkSwitch v-model="squareAvatars">
-									<template #label><SearchLabel>{{ i18n.ts.squareAvatars }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['effect', 'show']">
-							<MkPreferenceContainer k="enableSeasonalScreenEffect">
-								<MkSwitch v-model="enableSeasonalScreenEffect">
-									<template #label><SearchLabel>{{ i18n.ts.seasonalScreenEffect }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
-						</SearchMarker>
-
-						<SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab']">
-							<MkPreferenceContainer k="imageNewTab">
-								<MkSwitch v-model="imageNewTab">
-									<template #label><SearchLabel>{{ i18n.ts.openImageInNewTab }}</SearchLabel></template>
-								</MkSwitch>
-							</MkPreferenceContainer>
 						</SearchMarker>
 					</div>
+				</MkFolder>
+			</SearchMarker>
 
-					<SearchMarker :keywords="['server', 'disconnect', 'reconnect', 'reload', 'streaming']">
-						<MkPreferenceContainer k="serverDisconnectedBehavior">
-							<MkSelect v-model="serverDisconnectedBehavior">
-								<template #label><SearchLabel>{{ i18n.ts.whenServerDisconnected }}</SearchLabel></template>
-								<option value="reload">{{ i18n.ts._serverDisconnectedBehavior.reload }}</option>
-								<option value="dialog">{{ i18n.ts._serverDisconnectedBehavior.dialog }}</option>
-								<option value="quiet">{{ i18n.ts._serverDisconnectedBehavior.quiet }}</option>
-							</MkSelect>
-						</MkPreferenceContainer>
-					</SearchMarker>
+			<SearchMarker :keywords="['datasaver']">
+				<MkFolder>
+					<template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template>
 
-					<SearchMarker :keywords="['cache', 'page']">
-						<MkPreferenceContainer k="numberOfPageCache">
-							<MkRange v-model="numberOfPageCache" :min="1" :max="10" :step="1" easing>
-								<template #label><SearchLabel>{{ i18n.ts.numberOfPageCache }}</SearchLabel></template>
-								<template #caption>{{ i18n.ts.numberOfPageCacheDescription }}</template>
-							</MkRange>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['ad', 'show']">
-						<MkPreferenceContainer k="forceShowAds">
-							<MkSwitch v-model="forceShowAds">
-								<template #label><SearchLabel>{{ i18n.ts.forceShowAds }}</SearchLabel></template>
-							</MkSwitch>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker>
-						<MkPreferenceContainer k="hemisphere">
-							<MkRadios v-model="hemisphere">
-								<template #label><SearchLabel>{{ i18n.ts.hemisphere }}</SearchLabel></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>
-						</MkPreferenceContainer>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['emoji', 'dictionary', 'additional', 'extra']">
-						<MkFolder>
-							<template #label><SearchLabel>{{ i18n.ts.additionalEmojiDictionary }}</SearchLabel></template>
-							<div class="_buttons">
-								<template v-for="lang in emojiIndexLangs" :key="lang">
-									<MkButton v-if="store.r.additionalUnicodeEmojiIndexes.value[lang]" danger @click="removeEmojiIndex(lang)"><i class="ti ti-trash"></i> {{ i18n.ts.remove }} ({{ getEmojiIndexLangName(lang) }})</MkButton>
-									<MkButton v-else @click="downloadEmojiIndex(lang)"><i class="ti ti-download"></i> {{ getEmojiIndexLangName(lang) }}{{ store.r.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
-								</template>
-							</div>
-						</MkFolder>
-					</SearchMarker>
-				</div>
-			</MkFolder>
-		</SearchMarker>
-
-		<SearchMarker :keywords="['datasaver']">
-			<MkFolder>
-				<template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template>
-
-				<div class="_gaps_m">
-					<MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
-
-					<div class="_buttons">
-						<MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
-						<MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
-					</div>
 					<div class="_gaps_m">
-						<MkSwitch v-model="dataSaver.media">
-							{{ i18n.ts._dataSaver._media.title }}
-							<template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
-						</MkSwitch>
-						<MkSwitch v-model="dataSaver.avatar">
-							{{ i18n.ts._dataSaver._avatar.title }}
-							<template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
-						</MkSwitch>
-						<MkSwitch v-model="dataSaver.urlPreview">
-							{{ i18n.ts._dataSaver._urlPreview.title }}
-							<template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
-						</MkSwitch>
-						<MkSwitch v-model="dataSaver.code">
-							{{ i18n.ts._dataSaver._code.title }}
-							<template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
-						</MkSwitch>
-					</div>
-				</div>
-			</MkFolder>
-		</SearchMarker>
+						<MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
 
-		<FormLink to="/settings/navbar">{{ i18n.ts.navbar }}</FormLink>
-		<FormLink to="/settings/statusbar">{{ i18n.ts.statusbar }}</FormLink>
-		<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
-		<FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
+						<div class="_buttons">
+							<MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
+							<MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
+						</div>
+						<div class="_gaps_m">
+							<MkSwitch v-model="dataSaver.media">
+								{{ i18n.ts._dataSaver._media.title }}
+								<template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
+							</MkSwitch>
+							<MkSwitch v-model="dataSaver.avatar">
+								{{ i18n.ts._dataSaver._avatar.title }}
+								<template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
+							</MkSwitch>
+							<MkSwitch v-model="dataSaver.urlPreview">
+								{{ i18n.ts._dataSaver._urlPreview.title }}
+								<template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
+							</MkSwitch>
+							<MkSwitch v-model="dataSaver.code">
+								{{ i18n.ts._dataSaver._code.title }}
+								<template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
+							</MkSwitch>
+						</div>
+					</div>
+				</MkFolder>
+			</SearchMarker>
+
+			<FormLink to="/settings/navbar">{{ i18n.ts.navbar }}</FormLink>
+			<FormLink to="/settings/statusbar">{{ i18n.ts.statusbar }}</FormLink>
+			<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
+			<FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
+		</div>
 	</div>
 </SearchMarker>
 </template>
diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue
deleted file mode 100644
index c1cabad2c3..0000000000
--- a/packages/frontend/src/pages/settings/roles.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<div class="_gaps_m">
-	<FormSection first>
-		<template #label>{{ i18n.ts.rolesAssignedToMe }}</template>
-		<div class="_gaps_s">
-			<MkRolePreview v-for="role in $i.roles" :key="role.id" :role="role" :forModeration="false"/>
-		</div>
-	</FormSection>
-	<FormSection>
-		<template #label>{{ i18n.ts._role.policies }}</template>
-		<div class="_gaps_s">
-			<div v-for="policy in Object.keys($i.policies)" :key="policy">
-				{{ policy }} ... {{ $i.policies[policy] }}
-			</div>
-		</div>
-	</FormSection>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { computed } from 'vue';
-import FormSection from '@/components/form/section.vue';
-import * as os from '@/os.js';
-import { i18n } from '@/i18n.js';
-import { signinRequired } from '@/account.js';
-import { definePage } from '@/page.js';
-import MkRolePreview from '@/components/MkRolePreview.vue';
-
-const $i = signinRequired();
-
-const headerActions = computed(() => []);
-
-const headerTabs = computed(() => []);
-
-definePage(() => ({
-	title: i18n.ts.roles,
-	icon: 'ti ti-badges',
-}));
-</script>
-
-<style lang="scss" module>
-
-</style>
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 62b13d22be..73920766d7 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -57,10 +57,6 @@ const routes: RouteDef[] = [{
 		path: '/avatar-decoration',
 		name: 'avatarDecoration',
 		component: page(() => import('@/pages/settings/avatar-decoration.vue')),
-	}, {
-		path: '/roles',
-		name: 'roles',
-		component: page(() => import('@/pages/settings/roles.vue')),
 	}, {
 		path: '/privacy',
 		name: 'privacy',
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index 734dc0c99c..e44910e850 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -271,55 +271,55 @@ export const searchIndexes: SearchIndexItem[] = [
 		id: '3yCAv0IsZ',
 		children: [
 			{
-				id: 'kMJ5laK3n',
+				id: 'AKvDrxSj5',
 				children: [
 					{
-						id: 'EC8J177N8',
+						id: 'cAszhShB0',
 						label: i18n.ts.uiLanguage,
 						keywords: ['language'],
 					},
 					{
-						id: 'CHKy9gnrh',
+						id: 'apz9AutPm',
 						label: i18n.ts.overridedDeviceKind,
 						keywords: ['device', 'type', 'kind', 'smartphone', 'tablet', 'desktop'],
 					},
 					{
-						id: 'snyCQ5oKE',
+						id: 'nqRVtw1xw',
 						label: i18n.ts.useBlurEffect,
 						keywords: ['blur'],
 					},
 					{
-						id: '8j36S4Ev6',
+						id: 'EO5WHBeG8',
 						label: i18n.ts.useBlurEffectForModal,
 						keywords: ['blur', 'modal'],
 					},
 					{
-						id: 'cytWLyF1V',
+						id: 'CWpyT9vLK',
 						label: i18n.ts.showAvatarDecorations,
 						keywords: ['avatar', 'icon', 'decoration', 'show'],
 					},
 					{
-						id: 'odi1d2SWy',
+						id: '1wwACqQz1',
 						label: i18n.ts.alwaysConfirmFollow,
 						keywords: ['follow', 'confirm', 'always'],
 					},
 					{
-						id: 'm43Eu3Ypg',
+						id: '1x3JNXj8N',
 						label: i18n.ts.highlightSensitiveMedia,
 						keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
 					},
 					{
-						id: 'cjfAtxMzP',
+						id: 'CfAg0Qekq',
 						label: i18n.ts.confirmWhenRevealingSensitiveMedia,
 						keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
 					},
 					{
-						id: 'aefexW9fD',
+						id: '4LxdiOMNh',
 						label: i18n.ts.emojiStyle,
 						keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
 					},
 					{
-						id: 'p7aiLj6A0',
+						id: '67knC3FWp',
 						label: i18n.ts.pinnedList,
 						keywords: ['pinned', 'list'],
 					},
@@ -328,35 +328,35 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['general'],
 			},
 			{
-				id: 'khT3n6byY',
+				id: 'hDdVkBFJP',
 				children: [
 					{
-						id: 'DftdlLbNu',
+						id: 'igFN7RIUa',
 						label: i18n.ts.showFixedPostForm,
 						keywords: ['post', 'form', 'timeline'],
 					},
 					{
-						id: 'FbhoeuRAD',
+						id: '9uxocbLO0',
 						label: i18n.ts.showFixedPostFormInChannel,
 						keywords: ['post', 'form', 'timeline', 'channel'],
 					},
 					{
-						id: 'rq69GTeB4',
+						id: 'eaT1O1Fao',
 						label: i18n.ts.collapseRenotes,
 						keywords: ['renote', i18n.ts.collapseRenotesDescription],
 					},
 					{
-						id: 'omxZk3eET',
+						id: 'jC7LtTnmc',
 						label: i18n.ts.showGapBetweenNotesInTimeline,
 						keywords: ['note', 'timeline', 'gap'],
 					},
 					{
-						id: 'epvi2Nv2G',
+						id: 'p2wlrnwLo',
 						label: i18n.ts.enableInfiniteScroll,
 						keywords: ['load', 'auto', 'more'],
 					},
 					{
-						id: 'v26JSj9mH',
+						id: 'eqMBMY6LU',
 						label: i18n.ts.disableStreamingTimeline,
 						keywords: ['disable', 'streaming', 'timeline'],
 					},
@@ -365,65 +365,65 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['timeline'],
 			},
 			{
-				id: '7Uf8ksn3q',
+				id: '2LNjwv1cr',
 				children: [
 					{
-						id: 'tLGyaQagB',
+						id: '6ylW3eIcD',
 						label: i18n.ts.showNoteActionsOnlyHover,
 						keywords: ['hover', 'show', 'footer', 'action'],
 					},
 					{
-						id: '7W6g8Dcqz',
+						id: 'lBbtAg0Hm',
 						label: i18n.ts.showClipButtonInNoteFooter,
 						keywords: ['footer', 'action', 'clip', 'show'],
 					},
 					{
-						id: 'uAOoH3LFF',
+						id: 'E9whefUtX',
 						label: i18n.ts.enableAdvancedMfm,
 						keywords: ['mfm', 'enable', 'show', 'advanced'],
 					},
 					{
-						id: 'eCiyZLC8n',
+						id: 'iQaBbJBva',
 						label: i18n.ts.showReactionsCount,
 						keywords: ['reaction', 'count', 'show'],
 					},
 					{
-						id: '68u9uRmFP',
+						id: 'hgEVGgJa1',
 						label: i18n.ts.confirmOnReact,
 						keywords: ['reaction', 'confirm'],
 					},
 					{
-						id: 'rHWm4sXIe',
+						id: 'yxehrHZ6x',
 						label: i18n.ts.loadRawImages,
 						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'],
 					},
 					{
-						id: '9L2XGJw7e',
+						id: 'DdoFLaSG8',
 						label: i18n.ts.useReactionPickerForContextMenu,
 						keywords: ['reaction', 'picker', 'contextmenu', 'open'],
 					},
 					{
-						id: 'uIMCIK7kG',
+						id: 'fyod6U3QX',
 						label: i18n.ts.reactionsDisplaySize,
 						keywords: ['reaction', 'size', 'scale', 'display'],
 					},
 					{
-						id: 'uMckjO9bz',
+						id: 'kmdsnVIQX',
 						label: i18n.ts.limitWidthOfReaction,
 						keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
 					},
 					{
-						id: 'yeghU4qiH',
+						id: 'hacQ9br20',
 						label: i18n.ts.mediaListWithOneImageAppearance,
 						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
 					},
 					{
-						id: 'yYSOPoAKE',
+						id: 'vE7KeV4U4',
 						label: i18n.ts.instanceTicker,
 						keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
 					},
 					{
-						id: 'iOHiIu32L',
+						id: '3reoOxO26',
 						label: i18n.ts.displayOfSensitiveMedia,
 						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
 					},
@@ -432,25 +432,25 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['note'],
 			},
 			{
-				id: 'zrJicawH9',
+				id: 'eROFRMtXv',
 				children: [
 					{
-						id: 'iuEuPe6pa',
+						id: 'bezWaWd6M',
 						label: i18n.ts.keepCw,
 						keywords: ['remember', 'keep', 'note', 'cw'],
 					},
 					{
-						id: '9WrGgANqN',
+						id: '90ngq28Nx',
 						label: i18n.ts.rememberNoteVisibility,
 						keywords: ['remember', 'keep', 'note', 'visibility'],
 					},
 					{
-						id: 'Cu7ErCM7C',
+						id: 'ERGQVw6ml',
 						label: i18n.ts.enableQuickAddMfmFunction,
 						keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
 					},
 					{
-						id: 'oQl8xwiyI',
+						id: 'g0otcvWv3',
 						label: i18n.ts.defaultNoteVisibility,
 						keywords: ['default', 'note', 'visibility'],
 					},
@@ -459,20 +459,20 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['post', 'form'],
 			},
 			{
-				id: 'xFmAg2tDe',
+				id: 'AWLIP02IT',
 				children: [
 					{
-						id: 'mepqKL5Ow',
+						id: 'rDLJRu99',
 						label: i18n.ts.useGroupedNotifications,
 						keywords: ['group'],
 					},
 					{
-						id: 'wUuUOEO1g',
+						id: '70WDijfPH',
 						label: i18n.ts.position,
 						keywords: ['position'],
 					},
 					{
-						id: '27em8eC8R',
+						id: 'xKUzsSrKy',
 						label: i18n.ts.stackAxis,
 						keywords: ['stack', 'axis', 'direction'],
 					},
@@ -481,45 +481,45 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['notification'],
 			},
 			{
-				id: 'AzymHsnrp',
+				id: '2E7vdIUQd',
 				children: [
 					{
-						id: 'DFUrEO2DI',
+						id: 'C2iXtZKb3',
 						label: i18n.ts.squareAvatars,
 						keywords: ['avatar', 'icon', 'square'],
 					},
 					{
-						id: 'r9DX60AxL',
+						id: 'DCfJg0bva',
 						label: i18n.ts.seasonalScreenEffect,
 						keywords: ['effect', 'show'],
 					},
 					{
-						id: 'sJ3fqncSD',
+						id: 'AV0iGW0vg',
 						label: i18n.ts.openImageInNewTab,
 						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
 					},
 					{
-						id: 'p7s0hwZ8A',
+						id: '5h8vhCX1S',
 						label: i18n.ts.whenServerDisconnected,
 						keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
 					},
 					{
-						id: 'yCleENWNf',
+						id: 'zZxyXHk3A',
 						label: i18n.ts.numberOfPageCache,
 						keywords: ['cache', 'page'],
 					},
 					{
-						id: 'omEy5Q3Ev',
+						id: '7ix3kvMyU',
 						label: i18n.ts.forceShowAds,
 						keywords: ['ad', 'show'],
 					},
 					{
-						id: 'aWitQSBtD',
+						id: '6RxgjmMLN',
 						label: i18n.ts.hemisphere,
 						keywords: [],
 					},
 					{
-						id: 'hUQAXl1H4',
+						id: '5iMpm5rES',
 						label: i18n.ts.additionalEmojiDictionary,
 						keywords: ['emoji', 'dictionary', 'additional', 'extra'],
 					},
@@ -528,7 +528,7 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['other'],
 			},
 			{
-				id: 'aSbKFHbOy',
+				id: 'fnR7PRww5',
 				label: i18n.ts.dataSaver,
 				keywords: ['datasaver'],
 			},
@@ -550,26 +550,31 @@ export const searchIndexes: SearchIndexItem[] = [
 		children: [
 			{
 				id: 'msAcN6u3S',
-				label: i18n.ts.accountInfo,
+				label: i18n.ts._role.policies,
 				keywords: ['account', 'info'],
 			},
 			{
-				id: 'ts8DgdnZV',
+				id: 'pbTLsgRO7',
+				label: i18n.ts.rolesAssignedToMe,
+				keywords: ['roles'],
+			},
+			{
+				id: 'fQpvZyfLK',
 				label: i18n.ts.accountMigration,
 				keywords: ['account', 'move', 'migration'],
 			},
 			{
-				id: '4BG7nBECm',
+				id: 'xhfur5m2z',
 				label: i18n.ts.closeAccount,
 				keywords: ['account', 'close', 'delete', i18n.ts._accountDelete.requestAccountDelete],
 			},
 			{
-				id: '2qI6ruPgi',
+				id: 'oAXB8zm2U',
 				label: i18n.ts.experimentalFeatures,
 				keywords: ['experimental', 'feature', 'flags'],
 			},
 			{
-				id: 'cIeaax47o',
+				id: '95OjjGSo7',
 				label: i18n.ts.developer,
 				keywords: ['developer', 'mode', 'debug'],
 			},

From 44073736debda0210b77a53d0f61a68783d28e7c Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 19:44:23 +0900
Subject: [PATCH 14/16] enhance(frontend): improve preferences

---
 packages/frontend/src/boot/main-boot.ts       |   1 +
 .../src/components/MkFollowButton.vue         |   5 +-
 .../src/pages/settings/account-data.vue       |   4 +-
 .../frontend/src/pages/settings/other.vue     | 180 ++++++++----------
 .../src/pages/settings/preferences.vue        |  83 ++++----
 packages/frontend/src/preferences/def.ts      |   3 +
 packages/frontend/src/store.ts                |   8 +-
 packages/frontend/src/style.scss              |   2 +-
 .../utility/autogen/settings-search-index.ts  |  43 +++--
 9 files changed, 164 insertions(+), 165 deletions(-)

diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 0ad333b203..be72eeb9e1 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -191,6 +191,7 @@ export async function mainBoot() {
 				prefer.commit('skipNoteRender', store.s.skipNoteRender);
 				prefer.commit('showSoftWordMutedWord', store.s.showSoftWordMutedWord);
 				prefer.commit('confirmOnReact', store.s.confirmOnReact);
+				prefer.commit('defaultFollowWithReplies', store.s.defaultWithReplies);
 				prefer.commit('sound.masterVolume', store.s.sound_masterVolume);
 				prefer.commit('sound.notUseSound', store.s.sound_notUseSound);
 				prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive);
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index 3d5d0ec5ab..a063854520 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -45,7 +45,6 @@ import { i18n } from '@/i18n.js';
 import { claimAchievement } from '@/utility/achievements.js';
 import { pleaseLogin } from '@/utility/please-login.js';
 import { $i } from '@/account.js';
-import { store } from '@/store.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
@@ -121,11 +120,11 @@ async function onClick() {
 			} else {
 				await misskeyApi('following/create', {
 					userId: props.user.id,
-					withReplies: store.s.defaultWithReplies,
+					withReplies: prefer.s.defaultFollowWithReplies,
 				});
 				emit('update:user', {
 					...props.user,
-					withReplies: store.s.defaultWithReplies,
+					withReplies: prefer.s.defaultFollowWithReplies,
 				});
 				hasPendingFollowRequestFromYou.value = true;
 
diff --git a/packages/frontend/src/pages/settings/account-data.vue b/packages/frontend/src/pages/settings/account-data.vue
index 7e7036a8dc..ed5fe48821 100644
--- a/packages/frontend/src/pages/settings/account-data.vue
+++ b/packages/frontend/src/pages/settings/account-data.vue
@@ -168,12 +168,12 @@ import { selectFile } from '@/utility/select-file.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { $i } from '@/account.js';
-import { store } from '@/store.js';
 import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
+import { prefer } from '@/preferences.js';
 
 const excludeMutingUsers = ref(false);
 const excludeInactiveUsers = ref(false);
-const withReplies = ref(store.s.defaultWithReplies);
+const withReplies = ref(prefer.s.defaultFollowWithReplies);
 
 const onExportSuccess = () => {
 	os.alert({
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index b60db78071..62b0f5c941 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -16,112 +16,102 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkSwitch v-model="reportError">{{ i18n.ts.sendErrorReports }}<template #caption>{{ i18n.ts.sendErrorReportsDescription }}</template></MkSwitch>
 		-->
 
-		<FormSection first>
-			<div class="_gaps_s">
-				<SearchMarker :keywords="['account', 'info']">
-					<MkFolder>
-						<template #icon><i class="ti ti-info-circle"></i></template>
-						<template #label><SearchLabel>{{ i18n.ts.accountInfo }}</SearchLabel></template>
+		<div class="_gaps_s">
+			<SearchMarker :keywords="['account', 'info']">
+				<MkFolder>
+					<template #icon><i class="ti ti-info-circle"></i></template>
+					<template #label><SearchLabel>{{ i18n.ts.accountInfo }}</SearchLabel></template>
 
-						<div class="_gaps_m">
-							<MkKeyValue>
-								<template #key>ID</template>
-								<template #value><span class="_monospace">{{ $i.id }}</span></template>
-							</MkKeyValue>
+					<div class="_gaps_m">
+						<MkKeyValue>
+							<template #key>ID</template>
+							<template #value><span class="_monospace">{{ $i.id }}</span></template>
+						</MkKeyValue>
 
-							<MkKeyValue>
-								<template #key>{{ i18n.ts.registeredDate }}</template>
-								<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
-							</MkKeyValue>
+						<MkKeyValue>
+							<template #key>{{ i18n.ts.registeredDate }}</template>
+							<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
+						</MkKeyValue>
 
-							<MkFolder>
-								<template #icon><i class="ti ti-badges"></i></template>
-								<template #label><SearchLabel>{{ i18n.ts._role.policies }}</SearchLabel></template>
+						<MkFolder>
+							<template #icon><i class="ti ti-badges"></i></template>
+							<template #label><SearchLabel>{{ i18n.ts._role.policies }}</SearchLabel></template>
 
-								<div class="_gaps_s">
-									<div v-for="policy in Object.keys($i.policies)" :key="policy">
-										{{ policy }} ... {{ $i.policies[policy] }}
-									</div>
+							<div class="_gaps_s">
+								<div v-for="policy in Object.keys($i.policies)" :key="policy">
+									{{ policy }} ... {{ $i.policies[policy] }}
 								</div>
-							</MkFolder>
-						</div>
-					</MkFolder>
-				</SearchMarker>
+							</div>
+						</MkFolder>
+					</div>
+				</MkFolder>
+			</SearchMarker>
 
-				<SearchMarker :keywords="['roles']">
-					<MkFolder>
-						<template #icon><i class="ti ti-badges"></i></template>
-						<template #label><SearchLabel>{{ i18n.ts.rolesAssignedToMe }}</SearchLabel></template>
+			<SearchMarker :keywords="['roles']">
+				<MkFolder>
+					<template #icon><i class="ti ti-badges"></i></template>
+					<template #label><SearchLabel>{{ i18n.ts.rolesAssignedToMe }}</SearchLabel></template>
 
-						<MkRolePreview v-for="role in $i.roles" :key="role.id" :role="role" :forModeration="false"/>
-					</MkFolder>
-				</SearchMarker>
+					<MkRolePreview v-for="role in $i.roles" :key="role.id" :role="role" :forModeration="false"/>
+				</MkFolder>
+			</SearchMarker>
 
-				<SearchMarker :keywords="['account', 'move', 'migration']">
-					<MkFolder>
-						<template #icon><i class="ti ti-plane"></i></template>
-						<template #label><SearchLabel>{{ i18n.ts.accountMigration }}</SearchLabel></template>
+			<SearchMarker :keywords="['account', 'move', 'migration']">
+				<MkFolder>
+					<template #icon><i class="ti ti-plane"></i></template>
+					<template #label><SearchLabel>{{ i18n.ts.accountMigration }}</SearchLabel></template>
 
-						<XMigration/>
-					</MkFolder>
-				</SearchMarker>
+					<XMigration/>
+				</MkFolder>
+			</SearchMarker>
 
-				<SearchMarker :keywords="['account', 'close', 'delete']">
-					<MkFolder>
-						<template #icon><i class="ti ti-alert-triangle"></i></template>
-						<template #label><SearchLabel>{{ i18n.ts.closeAccount }}</SearchLabel></template>
+			<SearchMarker :keywords="['account', 'close', 'delete']">
+				<MkFolder>
+					<template #icon><i class="ti ti-alert-triangle"></i></template>
+					<template #label><SearchLabel>{{ i18n.ts.closeAccount }}</SearchLabel></template>
 
-						<div class="_gaps_m">
-							<FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
-							<FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
-							<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount"><SearchKeyword>{{ i18n.ts._accountDelete.requestAccountDelete }}</SearchKeyword></MkButton>
-							<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
-						</div>
-					</MkFolder>
-				</SearchMarker>
+					<div class="_gaps_m">
+						<FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
+						<FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
+						<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount"><SearchKeyword>{{ i18n.ts._accountDelete.requestAccountDelete }}</SearchKeyword></MkButton>
+						<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
+					</div>
+				</MkFolder>
+			</SearchMarker>
 
-				<SearchMarker :keywords="['experimental', 'feature', 'flags']">
-					<MkFolder>
-						<template #icon><i class="ti ti-flask"></i></template>
-						<template #label><SearchLabel>{{ i18n.ts.experimentalFeatures }}</SearchLabel></template>
+			<SearchMarker :keywords="['experimental', 'feature', 'flags']">
+				<MkFolder>
+					<template #icon><i class="ti ti-flask"></i></template>
+					<template #label><SearchLabel>{{ i18n.ts.experimentalFeatures }}</SearchLabel></template>
 
-						<div class="_gaps_m">
-							<MkSwitch v-model="enableCondensedLine">
-								<template #label>Enable condensed line</template>
-							</MkSwitch>
-							<MkSwitch v-model="skipNoteRender">
-								<template #label>Enable note render skipping</template>
-							</MkSwitch>
-						</div>
-					</MkFolder>
-				</SearchMarker>
+					<div class="_gaps_m">
+						<MkSwitch v-model="enableCondensedLine">
+							<template #label>Enable condensed line</template>
+						</MkSwitch>
+						<MkSwitch v-model="skipNoteRender">
+							<template #label>Enable note render skipping</template>
+						</MkSwitch>
+					</div>
+				</MkFolder>
+			</SearchMarker>
 
-				<SearchMarker :keywords="['developer', 'mode', 'debug']">
-					<MkFolder>
-						<template #icon><i class="ti ti-code"></i></template>
-						<template #label><SearchLabel>{{ i18n.ts.developer }}</SearchLabel></template>
+			<SearchMarker :keywords="['developer', 'mode', 'debug']">
+				<MkFolder>
+					<template #icon><i class="ti ti-code"></i></template>
+					<template #label><SearchLabel>{{ i18n.ts.developer }}</SearchLabel></template>
 
-						<div class="_gaps_m">
-							<MkSwitch v-model="devMode">
-								<template #label>{{ i18n.ts.devMode }}</template>
-							</MkSwitch>
-						</div>
-					</MkFolder>
-				</SearchMarker>
-			</div>
-		</FormSection>
+					<div class="_gaps_m">
+						<MkSwitch v-model="devMode">
+							<template #label>{{ i18n.ts.devMode }}</template>
+						</MkSwitch>
+					</div>
+				</MkFolder>
+			</SearchMarker>
+		</div>
 
-		<FormSection>
-			<FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
-		</FormSection>
+		<hr>
 
-		<FormSection>
-			<div class="_gaps_s">
-				<MkSwitch v-model="defaultWithReplies">{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</MkSwitch>
-				<MkButton danger @click="updateRepliesAll(true)"><i class="ti ti-messages"></i> {{ i18n.ts.showRepliesToOthersInTimelineAll }}</MkButton>
-				<MkButton danger @click="updateRepliesAll(false)"><i class="ti ti-messages-off"></i> {{ i18n.ts.hideRepliesToOthersInTimelineAll }}</MkButton>
-			</div>
-		</FormSection>
+		<FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
 	</div>
 </SearchMarker>
 </template>
@@ -137,7 +127,6 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { store } from '@/store.js';
 import { signout, signinRequired } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
@@ -152,7 +141,6 @@ const reportError = prefer.model('reportError');
 const enableCondensedLine = prefer.model('enableCondensedLine');
 const skipNoteRender = prefer.model('skipNoteRender');
 const devMode = prefer.model('devMode');
-const defaultWithReplies = computed(store.makeGetterSetter('defaultWithReplies'));
 
 watch(skipNoteRender, async () => {
 	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
@@ -182,16 +170,6 @@ async function deleteAccount() {
 	await signout();
 }
 
-async function updateRepliesAll(withReplies: boolean) {
-	const { canceled } = await os.confirm({
-		type: 'warning',
-		text: withReplies ? i18n.ts.confirmShowRepliesAll : i18n.ts.confirmHideRepliesAll,
-	});
-	if (canceled) return;
-
-	misskeyApi('following/update-all', { withReplies });
-}
-
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 94d154e9c7..87d80602ad 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -393,6 +393,39 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</MkFolder>
 			</SearchMarker>
 
+			<SearchMarker :keywords="['datasaver']">
+				<MkFolder>
+					<template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template>
+
+					<div class="_gaps_m">
+						<MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
+
+						<div class="_buttons">
+							<MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
+							<MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
+						</div>
+						<div class="_gaps_m">
+							<MkSwitch v-model="dataSaver.media">
+								{{ i18n.ts._dataSaver._media.title }}
+								<template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
+							</MkSwitch>
+							<MkSwitch v-model="dataSaver.avatar">
+								{{ i18n.ts._dataSaver._avatar.title }}
+								<template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
+							</MkSwitch>
+							<MkSwitch v-model="dataSaver.urlPreview">
+								{{ i18n.ts._dataSaver._urlPreview.title }}
+								<template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
+							</MkSwitch>
+							<MkSwitch v-model="dataSaver.code">
+								{{ i18n.ts._dataSaver._code.title }}
+								<template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
+							</MkSwitch>
+						</div>
+					</div>
+				</MkFolder>
+			</SearchMarker>
+
 			<SearchMarker :keywords="['other']">
 				<MkFolder>
 					<template #label><SearchLabel>{{ i18n.ts.other }}</SearchLabel></template>
@@ -422,6 +455,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 									</MkSwitch>
 								</MkPreferenceContainer>
 							</SearchMarker>
+
+							<SearchMarker :keywords="['follow', 'replies']">
+								<MkPreferenceContainer k="defaultFollowWithReplies">
+									<MkSwitch v-model="defaultFollowWithReplies">
+										<template #label><SearchLabel>{{ i18n.ts.withRepliesByDefaultForNewlyFollowed }}</SearchLabel></template>
+									</MkSwitch>
+								</MkPreferenceContainer>
+							</SearchMarker>
 						</div>
 
 						<SearchMarker :keywords="['server', 'disconnect', 'reconnect', 'reload', 'streaming']">
@@ -477,43 +518,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</div>
 				</MkFolder>
 			</SearchMarker>
+		</div>
 
-			<SearchMarker :keywords="['datasaver']">
-				<MkFolder>
-					<template #label><SearchLabel>{{ i18n.ts.dataSaver }}</SearchLabel></template>
+		<hr>
 
-					<div class="_gaps_m">
-						<MkInfo>{{ i18n.ts.reloadRequiredToApplySettings }}</MkInfo>
-
-						<div class="_buttons">
-							<MkButton inline @click="enableAllDataSaver">{{ i18n.ts.enableAll }}</MkButton>
-							<MkButton inline @click="disableAllDataSaver">{{ i18n.ts.disableAll }}</MkButton>
-						</div>
-						<div class="_gaps_m">
-							<MkSwitch v-model="dataSaver.media">
-								{{ i18n.ts._dataSaver._media.title }}
-								<template #caption>{{ i18n.ts._dataSaver._media.description }}</template>
-							</MkSwitch>
-							<MkSwitch v-model="dataSaver.avatar">
-								{{ i18n.ts._dataSaver._avatar.title }}
-								<template #caption>{{ i18n.ts._dataSaver._avatar.description }}</template>
-							</MkSwitch>
-							<MkSwitch v-model="dataSaver.urlPreview">
-								{{ i18n.ts._dataSaver._urlPreview.title }}
-								<template #caption>{{ i18n.ts._dataSaver._urlPreview.description }}</template>
-							</MkSwitch>
-							<MkSwitch v-model="dataSaver.code">
-								{{ i18n.ts._dataSaver._code.title }}
-								<template #caption>{{ i18n.ts._dataSaver._code.description }}</template>
-							</MkSwitch>
-						</div>
-					</div>
-				</MkFolder>
-			</SearchMarker>
-
-			<FormLink to="/settings/navbar">{{ i18n.ts.navbar }}</FormLink>
-			<FormLink to="/settings/statusbar">{{ i18n.ts.statusbar }}</FormLink>
-			<FormLink to="/settings/deck">{{ i18n.ts.deck }}</FormLink>
+		<div class="_gaps_s">
+			<FormLink to="/settings/navbar"><template #icon><i class="ti ti-list"></i></template>{{ i18n.ts.navbar }}</FormLink>
+			<FormLink to="/settings/statusbar"><template #icon><i class="ti ti-list"></i></template>{{ i18n.ts.statusbar }}</FormLink>
+			<FormLink to="/settings/deck"><template #icon><i class="ti ti-columns"></i></template>{{ i18n.ts.deck }}</FormLink>
 			<FormLink to="/settings/custom-css"><template #icon><i class="ti ti-code"></i></template>{{ i18n.ts.customCss }}</FormLink>
 		</div>
 	</div>
@@ -592,6 +604,7 @@ const nsfw = prefer.model('nsfw');
 const emojiStyle = prefer.model('emojiStyle');
 const useBlurEffectForModal = prefer.model('useBlurEffectForModal');
 const useBlurEffect = prefer.model('useBlurEffect');
+const defaultFollowWithReplies = prefer.model('defaultFollowWithReplies');
 
 watch(lang, () => {
 	miLocalStorage.setItem('lang', lang.value as string);
diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts
index 6a926c4b26..eb3d6eeac4 100644
--- a/packages/frontend/src/preferences/def.ts
+++ b/packages/frontend/src/preferences/def.ts
@@ -306,6 +306,9 @@ export const PREF_DEF = {
 	confirmOnReact: {
 		default: false,
 	},
+	defaultFollowWithReplies: {
+		default: false,
+	},
 	plugins: {
 		default: [] as Plugin[],
 	},
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 6eebcd1ead..9a61e63d0e 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -100,10 +100,6 @@ export const store = markRaw(new Storage('base', {
 		where: 'device',
 		default: {} as Record<string, Record<string, string[]>>,
 	},
-	defaultWithReplies: {
-		where: 'account',
-		default: false,
-	},
 	pluginTokens: {
 		where: 'deviceAccount',
 		default: {} as Record<string, string>, // plugin id, token
@@ -119,6 +115,10 @@ export const store = markRaw(new Storage('base', {
 	},
 
 	//#region TODO: そのうち消す (preferに移行済み)
+	defaultWithReplies: {
+		where: 'account',
+		default: false,
+	},
 	reactions: {
 		where: 'account',
 		default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 48aacf10bc..fb2c805b1b 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -128,7 +128,7 @@ optgroup, option {
 }
 
 hr {
-	margin: var(--MI-margin) 0 var(--MI-margin) 0;
+	margin: 0;
 	border: none;
 	height: 1px;
 	background: var(--MI_THEME-divider);
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index e44910e850..ebc67eb58d 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -482,44 +482,54 @@ export const searchIndexes: SearchIndexItem[] = [
 			},
 			{
 				id: '2E7vdIUQd',
+				label: i18n.ts.dataSaver,
+				keywords: ['datasaver'],
+			},
+			{
+				id: '6ZbRRIhA6',
 				children: [
 					{
-						id: 'C2iXtZKb3',
+						id: 'soNZaKfiW',
 						label: i18n.ts.squareAvatars,
 						keywords: ['avatar', 'icon', 'square'],
 					},
 					{
-						id: 'DCfJg0bva',
+						id: 'nhwHJJ2tl',
 						label: i18n.ts.seasonalScreenEffect,
 						keywords: ['effect', 'show'],
 					},
 					{
-						id: 'AV0iGW0vg',
+						id: 'oMAVUuxTm',
 						label: i18n.ts.openImageInNewTab,
 						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
 					},
 					{
-						id: '5h8vhCX1S',
+						id: 'hSqX5JKM7',
+						label: i18n.ts.withRepliesByDefaultForNewlyFollowed,
+						keywords: ['follow', 'replies'],
+					},
+					{
+						id: 'fm98eqzke',
 						label: i18n.ts.whenServerDisconnected,
 						keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
 					},
 					{
-						id: 'zZxyXHk3A',
+						id: '1rWDVig8Y',
 						label: i18n.ts.numberOfPageCache,
 						keywords: ['cache', 'page'],
 					},
 					{
-						id: '7ix3kvMyU',
+						id: 'vXLtihtCp',
 						label: i18n.ts.forceShowAds,
 						keywords: ['ad', 'show'],
 					},
 					{
-						id: '6RxgjmMLN',
+						id: '77YljFpiH',
 						label: i18n.ts.hemisphere,
 						keywords: [],
 					},
 					{
-						id: '5iMpm5rES',
+						id: 'CZgDNPP1h',
 						label: i18n.ts.additionalEmojiDictionary,
 						keywords: ['emoji', 'dictionary', 'additional', 'extra'],
 					},
@@ -527,11 +537,6 @@ export const searchIndexes: SearchIndexItem[] = [
 				label: i18n.ts.other,
 				keywords: ['other'],
 			},
-			{
-				id: 'fnR7PRww5',
-				label: i18n.ts.dataSaver,
-				keywords: ['datasaver'],
-			},
 		],
 		label: i18n.ts.preferences,
 		keywords: ['general', 'preferences', i18n.ts._settings.preferencesBanner],
@@ -549,32 +554,32 @@ export const searchIndexes: SearchIndexItem[] = [
 		id: 'F1uK9ssiY',
 		children: [
 			{
-				id: 'msAcN6u3S',
+				id: 'E0ndmaP6Q',
 				label: i18n.ts._role.policies,
 				keywords: ['account', 'info'],
 			},
 			{
-				id: 'pbTLsgRO7',
+				id: 'r5SjfwZJc',
 				label: i18n.ts.rolesAssignedToMe,
 				keywords: ['roles'],
 			},
 			{
-				id: 'fQpvZyfLK',
+				id: 'cm7LrjgaW',
 				label: i18n.ts.accountMigration,
 				keywords: ['account', 'move', 'migration'],
 			},
 			{
-				id: 'xhfur5m2z',
+				id: 'ozfqNviP3',
 				label: i18n.ts.closeAccount,
 				keywords: ['account', 'close', 'delete', i18n.ts._accountDelete.requestAccountDelete],
 			},
 			{
-				id: 'oAXB8zm2U',
+				id: 'tpywgkpxy',
 				label: i18n.ts.experimentalFeatures,
 				keywords: ['experimental', 'feature', 'flags'],
 			},
 			{
-				id: '95OjjGSo7',
+				id: '54wETGawJ',
 				label: i18n.ts.developer,
 				keywords: ['developer', 'mode', 'debug'],
 			},

From 8c9ec5827fa2040c8d705d2a01329da593d19fa3 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Thu, 13 Mar 2025 22:12:23 +0900
Subject: [PATCH 15/16] enhance(frontend): improve accounts management

---
 packages/frontend/src/account.ts              | 390 ------------------
 packages/frontend/src/accounts.ts             | 341 +++++++++++++++
 packages/frontend/src/aiscript/api.ts         |   2 +-
 packages/frontend/src/boot/common.ts          |  10 +-
 packages/frontend/src/boot/main-boot.ts       |  24 +-
 .../src/components/MkAnnouncementDialog.vue   |   5 +-
 .../frontend/src/components/MkAuthConfirm.vue |   9 +-
 .../frontend/src/components/MkClipPreview.vue |   2 +-
 .../src/components/MkCropperDialog.vue        |   2 +-
 .../frontend/src/components/MkDrive.file.vue  |   2 +-
 .../frontend/src/components/MkEmojiPicker.vue |   2 +-
 .../src/components/MkFollowButton.vue         |   2 +-
 .../src/components/MkInstanceStats.vue        |   2 +-
 .../frontend/src/components/MkMediaAudio.vue  |   2 +-
 .../frontend/src/components/MkMediaImage.vue  |   2 +-
 .../frontend/src/components/MkMediaVideo.vue  |   2 +-
 .../frontend/src/components/MkMention.vue     |   2 +-
 packages/frontend/src/components/MkNote.vue   |   2 +-
 .../src/components/MkNoteDetailed.vue         |   2 +-
 .../frontend/src/components/MkNoteSub.vue     |   2 +-
 .../src/components/MkNotification.vue         |   2 +-
 .../src/components/MkPasswordDialog.vue       |   2 +-
 .../frontend/src/components/MkPostForm.vue    |   5 +-
 .../frontend/src/components/MkPreview.vue     |   2 +-
 .../MkPushNotificationAllowButton.vue         |   3 +-
 .../components/MkReactionsViewer.reaction.vue |   2 +-
 packages/frontend/src/components/MkSignin.vue |   5 +-
 .../src/components/MkSignupDialog.form.vue    |   4 +-
 .../frontend/src/components/MkTimeline.vue    |   2 +-
 .../src/components/MkTokenGenerateWindow.vue  |   2 +-
 .../src/components/MkTutorialDialog.Note.vue  |   2 +-
 .../components/MkTutorialDialog.Sensitive.vue |   2 +-
 .../frontend/src/components/MkUserInfo.vue    |   2 +-
 .../frontend/src/components/MkUserPopup.vue   |   2 +-
 .../src/components/MkUserSelectDialog.vue     |   2 +-
 .../components/MkUserSetupDialog.Profile.vue  |   2 +-
 .../frontend/src/components/global/MkAd.vue   |   2 +-
 .../src/components/global/MkCustomEmoji.vue   |   2 +-
 .../src/components/global/MkPageHeader.vue    |   3 +-
 packages/frontend/src/i.ts                    |  34 ++
 packages/frontend/src/local-storage.ts        |   1 -
 packages/frontend/src/navbar.ts               |   2 +-
 packages/frontend/src/pages/about-misskey.vue |   2 +-
 packages/frontend/src/pages/about.emojis.vue  |   2 +-
 packages/frontend/src/pages/achievements.vue  |   2 +-
 packages/frontend/src/pages/admin-file.vue    |   2 +-
 packages/frontend/src/pages/admin-user.vue    |   2 +-
 packages/frontend/src/pages/announcement.vue  |   5 +-
 packages/frontend/src/pages/announcements.vue |   5 +-
 packages/frontend/src/pages/auth.vue          |   3 +-
 .../pages/avatar-decoration-edit-dialog.vue   |   2 +-
 .../frontend/src/pages/avatar-decorations.vue |   2 +-
 packages/frontend/src/pages/channel.vue       |   2 +-
 packages/frontend/src/pages/clip.vue          |   2 +-
 .../src/pages/drop-and-fusion.game.vue        |   2 +-
 packages/frontend/src/pages/emojis.emoji.vue  |   2 +-
 packages/frontend/src/pages/flash/flash.vue   |   2 +-
 .../frontend/src/pages/follow-requests.vue    |   2 +-
 packages/frontend/src/pages/gallery/post.vue  |   2 +-
 packages/frontend/src/pages/instance-info.vue |   2 +-
 packages/frontend/src/pages/invite.vue        |   2 +-
 .../frontend/src/pages/my-lists/index.vue     |   2 +-
 packages/frontend/src/pages/my-lists/list.vue |   2 +-
 packages/frontend/src/pages/note.vue          |   2 +-
 .../src/pages/page-editor/page-editor.vue     |   2 +-
 packages/frontend/src/pages/page.vue          |   2 +-
 .../frontend/src/pages/reversi/game.board.vue |   2 +-
 .../src/pages/reversi/game.setting.vue        |   2 +-
 packages/frontend/src/pages/reversi/game.vue  |   2 +-
 packages/frontend/src/pages/reversi/index.vue |   2 +-
 packages/frontend/src/pages/scratchpad.vue    |   2 +-
 packages/frontend/src/pages/search.note.vue   |   2 +-
 .../src/pages/settings/2fa.qrdialog.vue       |   2 +-
 packages/frontend/src/pages/settings/2fa.vue  |   5 +-
 .../src/pages/settings/account-data.vue       |   2 +-
 .../frontend/src/pages/settings/accounts.vue  | 103 ++---
 .../settings/avatar-decoration.decoration.vue |   2 +-
 .../settings/avatar-decoration.dialog.vue     |   2 +-
 .../src/pages/settings/avatar-decoration.vue  |   2 +-
 .../frontend/src/pages/settings/drive.vue     |   2 +-
 .../frontend/src/pages/settings/email.vue     |   2 +-
 .../frontend/src/pages/settings/index.vue     |   3 +-
 .../frontend/src/pages/settings/migration.vue |   2 +-
 .../settings/mute-block.instance-mute.vue     |   2 +-
 .../src/pages/settings/mute-block.vue         |   2 +-
 .../src/pages/settings/notifications.vue      |   2 +-
 .../frontend/src/pages/settings/other.vue     |   3 +-
 .../frontend/src/pages/settings/privacy.vue   |   2 +-
 .../frontend/src/pages/settings/profile.vue   |   2 +-
 .../frontend/src/pages/signup-complete.vue    |   2 +-
 packages/frontend/src/pages/tag.vue           |   2 +-
 packages/frontend/src/pages/theme-editor.vue  |   2 +-
 packages/frontend/src/pages/timeline.vue      |   2 +-
 .../frontend/src/pages/user/achievements.vue  |   2 +-
 packages/frontend/src/pages/user/home.vue     |   2 +-
 packages/frontend/src/pages/user/index.vue    |   2 +-
 packages/frontend/src/pages/welcome.setup.vue |   2 +-
 packages/frontend/src/pizzax.ts               |   2 +-
 packages/frontend/src/preferences.ts          |   2 +-
 packages/frontend/src/preferences/def.ts      |   4 +
 packages/frontend/src/preferences/manager.ts  |   2 +-
 packages/frontend/src/preferences/utility.ts  |   2 +-
 packages/frontend/src/router/definition.ts    |   2 +-
 packages/frontend/src/signout.ts              |  54 +++
 packages/frontend/src/store.ts                |   4 +
 packages/frontend/src/stream.ts               |   2 +-
 packages/frontend/src/theme-store.ts          |   2 +-
 packages/frontend/src/timelines.ts            |   2 +-
 .../src/ui/_common_/PreferenceRestore.vue     |   2 +-
 .../src/ui/_common_/announcements.vue         |   2 +-
 packages/frontend/src/ui/_common_/common.ts   |   2 +-
 packages/frontend/src/ui/_common_/common.vue  |   2 +-
 .../src/ui/_common_/navbar-for-mobile.vue     |   3 +-
 packages/frontend/src/ui/_common_/navbar.vue  |   3 +-
 .../frontend/src/ui/_common_/sw-inject.ts     |   3 +-
 packages/frontend/src/ui/classic.header.vue   |   4 +-
 packages/frontend/src/ui/classic.sidebar.vue  |   3 +-
 packages/frontend/src/ui/deck.vue             |   2 +-
 packages/frontend/src/ui/universal.vue        |   2 +-
 packages/frontend/src/use/use-note-capture.ts |   2 +-
 packages/frontend/src/utility/achievements.ts |   2 +-
 .../utility/autogen/settings-search-index.ts  |   7 +
 .../frontend/src/utility/check-permissions.ts |   2 +-
 .../frontend/src/utility/get-note-menu.ts     |   2 +-
 .../frontend/src/utility/get-user-menu.ts     |   2 +-
 .../frontend/src/utility/isFfVisibleForMe.ts  |   2 +-
 packages/frontend/src/utility/misskey-api.ts  |   2 +-
 packages/frontend/src/utility/please-login.ts |   2 +-
 .../frontend/src/utility/show-moved-dialog.ts |   2 +-
 packages/frontend/src/utility/upload.ts       |   2 +-
 .../frontend/src/widgets/WidgetActivity.vue   |   2 +-
 .../frontend/src/widgets/WidgetAiscript.vue   |   2 +-
 .../src/widgets/WidgetAiscriptApp.vue         |   2 +-
 .../src/widgets/WidgetBirthdayFollowings.vue  |   2 +-
 .../frontend/src/widgets/WidgetButton.vue     |   2 +-
 .../frontend/src/widgets/WidgetProfile.vue    |   2 +-
 packages/frontend/test/aiscript/api.test.ts   |   2 +-
 137 files changed, 640 insertions(+), 622 deletions(-)
 delete mode 100644 packages/frontend/src/account.ts
 create mode 100644 packages/frontend/src/accounts.ts
 create mode 100644 packages/frontend/src/i.ts
 create mode 100644 packages/frontend/src/signout.ts

diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
deleted file mode 100644
index c90d4da5ec..0000000000
--- a/packages/frontend/src/account.ts
+++ /dev/null
@@ -1,390 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { defineAsyncComponent, reactive, ref } from 'vue';
-import * as Misskey from 'misskey-js';
-import { apiUrl } from '@@/js/config.js';
-import type { MenuItem, MenuButton } from '@/types/menu.js';
-import { defaultMemoryStorage } from '@/memory-storage';
-import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
-import { i18n } from '@/i18n.js';
-import { miLocalStorage } from '@/local-storage.js';
-import { del, get, set } from '@/utility/idb-proxy.js';
-import { waiting, popup, popupMenu, success, alert } from '@/os.js';
-import { misskeyApi } from '@/utility/misskey-api.js';
-import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
-
-// TODO: 他のタブと永続化されたstateを同期
-// TODO: accountsはpreferences管理にする(tokenは別管理)
-
-type Account = Misskey.entities.MeDetailed & { token: string };
-
-const accountData = miLocalStorage.getItem('account');
-
-// TODO: 外部からはreadonlyに
-export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
-
-export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
-export const iAmAdmin = $i != null && $i.isAdmin;
-
-export function signinRequired() {
-	if ($i == null) throw new Error('signin required');
-	return $i;
-}
-
-export let notesCount = $i == null ? 0 : $i.notesCount;
-export function incNotesCount() {
-	notesCount++;
-}
-
-export async function signout() {
-	if (!$i) return;
-
-	defaultMemoryStorage.clear();
-
-	waiting();
-	document.cookie.split(';').forEach((cookie) => {
-		const cookieName = cookie.split('=')[0].trim();
-		if (cookieName === 'token') {
-			document.cookie = `${cookieName}=; max-age=0; path=/`;
-		}
-	});
-	miLocalStorage.removeItem('account');
-	await removeAccount($i.id);
-	const accounts = await getAccounts();
-
-	//#region Remove service worker registration
-	try {
-		if (navigator.serviceWorker.controller) {
-			const registration = await navigator.serviceWorker.ready;
-			const push = await registration.pushManager.getSubscription();
-			if (push) {
-				await window.fetch(`${apiUrl}/sw/unregister`, {
-					method: 'POST',
-					body: JSON.stringify({
-						i: $i.token,
-						endpoint: push.endpoint,
-					}),
-					headers: {
-						'Content-Type': 'application/json',
-					},
-				});
-			}
-		}
-
-		if (accounts.length === 0) {
-			await navigator.serviceWorker.getRegistrations()
-				.then(registrations => {
-					return Promise.all(registrations.map(registration => registration.unregister()));
-				});
-		}
-	} catch (err) {}
-	//#endregion
-
-	if (accounts.length > 0) login(accounts[0].token);
-	else unisonReload('/');
-}
-
-export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> {
-	return (await get('accounts')) || [];
-}
-
-export async function addAccount(id: Account['id'], token: Account['token']) {
-	const accounts = await getAccounts();
-	if (!accounts.some(x => x.id === id)) {
-		await set('accounts', accounts.concat([{ id, token }]));
-	}
-}
-
-export async function removeAccount(idOrToken: Account['id']) {
-	const accounts = await getAccounts();
-	const i = accounts.findIndex(x => x.id === idOrToken || x.token === idOrToken);
-	if (i !== -1) accounts.splice(i, 1);
-
-	if (accounts.length > 0) {
-		await set('accounts', accounts);
-	} else {
-		await del('accounts');
-	}
-}
-
-function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Account> {
-	document.cookie = 'token=; path=/; max-age=0';
-	document.cookie = `token=${token}; path=/queue; max-age=86400; SameSite=Strict; Secure`; // bull dashboardの認証とかで使う
-
-	return new Promise((done, fail) => {
-		window.fetch(`${apiUrl}/i`, {
-			method: 'POST',
-			body: JSON.stringify({
-				i: token,
-			}),
-			headers: {
-				'Content-Type': 'application/json',
-			},
-		})
-			.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => {
-				if (res.status >= 500 && res.status < 600) {
-					// サーバーエラー(5xx)の場合をrejectとする
-					// (認証エラーなど4xxはresolve)
-					return fail2(res);
-				}
-				res.json().then(done2, fail2);
-			}))
-			.then(async res => {
-				if ('error' in res) {
-					if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
-						// SUSPENDED
-						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
-							await showSuspendedDialog();
-						}
-					} else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
-						// USER_IS_DELETED
-						// アカウントが削除されている
-						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
-							await alert({
-								type: 'error',
-								title: i18n.ts.accountDeleted,
-								text: i18n.ts.accountDeletedDescription,
-							});
-						}
-					} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
-						// AUTHENTICATION_FAILED
-						// トークンが無効化されていたりアカウントが削除されたりしている
-						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
-							await alert({
-								type: 'error',
-								title: i18n.ts.tokenRevoked,
-								text: i18n.ts.tokenRevokedDescription,
-							});
-						}
-					} else {
-						await alert({
-							type: 'error',
-							title: i18n.ts.failedToFetchAccountInformation,
-							text: JSON.stringify(res.error),
-						});
-					}
-
-					// rejectかつ理由がtrueの場合、削除対象であることを示す
-					fail(true);
-				} else {
-					(res as Account).token = token;
-					done(res as Account);
-				}
-			})
-			.catch(fail);
-	});
-}
-
-export function updateAccount(accountData: Account) {
-	if (!$i) return;
-	for (const key of Object.keys($i)) {
-		delete $i[key];
-	}
-	for (const [key, value] of Object.entries(accountData)) {
-		$i[key] = value;
-	}
-	miLocalStorage.setItem('account', JSON.stringify($i));
-}
-
-export function updateAccountPartial(accountData: Partial<Account>) {
-	if (!$i) return;
-	for (const [key, value] of Object.entries(accountData)) {
-		$i[key] = value;
-	}
-	miLocalStorage.setItem('account', JSON.stringify($i));
-}
-
-export async function refreshAccount() {
-	if (!$i) return;
-	return fetchAccount($i.token, $i.id)
-		.then(updateAccount, reason => {
-			if (reason === true) return signout();
-			return;
-		});
-}
-
-export async function login(token: Account['token'], redirect?: string) {
-	const showing = ref(true);
-	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
-		success: false,
-		showing: showing,
-	}, {
-		closed: () => dispose(),
-	});
-	if (_DEV_) console.log('logging as token ', token);
-	const me = await fetchAccount(token, undefined, true)
-		.catch(reason => {
-			if (reason === true) {
-				// 削除対象の場合
-				removeAccount(token);
-			}
-
-			showing.value = false;
-			throw reason;
-		});
-	miLocalStorage.setItem('account', JSON.stringify(me));
-	await addAccount(me.id, token);
-
-	if (redirect) {
-		// 他のタブは再読み込みするだけ
-		reloadChannel.postMessage(null);
-		// このページはredirectで指定された先に移動
-		location.href = redirect;
-		return;
-	}
-
-	unisonReload();
-}
-
-export async function openAccountMenu(opts: {
-	includeCurrentAccount?: boolean;
-	withExtraOperation: boolean;
-	active?: Misskey.entities.UserDetailed['id'];
-	onChoose?: (account: Misskey.entities.UserDetailed) => void;
-}, ev: MouseEvent) {
-	if (!$i) return;
-
-	async function switchAccount(account: Misskey.entities.UserDetailed) {
-		const storedAccounts = await getAccounts();
-		const found = storedAccounts.find(x => x.id === account.id);
-		if (found == null) return;
-		switchAccountWithToken(found.token);
-	}
-
-	function switchAccountWithToken(token: string) {
-		login(token);
-	}
-
-	const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
-	const accountsPromise = misskeyApi('users/show', { userIds: storedAccounts.map(x => x.id) });
-
-	function createItem(account: Misskey.entities.UserDetailed) {
-		return {
-			type: 'user' as const,
-			user: account,
-			active: opts.active != null ? opts.active === account.id : false,
-			action: () => {
-				if (opts.onChoose) {
-					opts.onChoose(account);
-				} else {
-					switchAccount(account);
-				}
-			},
-		};
-	}
-
-	const accountItemPromises = storedAccounts.map(a => new Promise<ReturnType<typeof createItem> | MenuButton>(res => {
-		accountsPromise.then(accounts => {
-			const account = accounts.find(x => x.id === a.id);
-			if (account == null) return res({
-				type: 'button' as const,
-				text: a.id,
-				action: () => {
-					switchAccountWithToken(a.token);
-				},
-			});
-
-			res(createItem(account));
-		});
-	}));
-
-	const menuItems: MenuItem[] = [];
-
-	if (opts.withExtraOperation) {
-		menuItems.push({
-			type: 'link',
-			text: i18n.ts.profile,
-			to: `/@${$i.username}`,
-			avatar: $i,
-		}, {
-			type: 'divider',
-		});
-
-		if (opts.includeCurrentAccount) {
-			menuItems.push(createItem($i));
-		}
-
-		menuItems.push(...accountItemPromises);
-
-		menuItems.push({
-			type: 'parent',
-			icon: 'ti ti-plus',
-			text: i18n.ts.addAccount,
-			children: [{
-				text: i18n.ts.existingAccount,
-				action: () => {
-					getAccountWithSigninDialog().then(res => {
-						if (res != null) {
-							success();
-						}
-					});
-				},
-			}, {
-				text: i18n.ts.createAccount,
-				action: () => {
-					getAccountWithSignupDialog().then(res => {
-						if (res != null) {
-							switchAccountWithToken(res.token);
-						}
-					});
-				},
-			}],
-		}, {
-			type: 'link',
-			icon: 'ti ti-users',
-			text: i18n.ts.manageAccounts,
-			to: '/settings/accounts',
-		});
-	} else {
-		if (opts.includeCurrentAccount) {
-			menuItems.push(createItem($i));
-		}
-
-		menuItems.push(...accountItemPromises);
-	}
-
-	popupMenu(menuItems, ev.currentTarget ?? ev.target, {
-		align: 'left',
-	});
-}
-
-export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
-	return new Promise((resolve) => {
-		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
-			done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
-				await addAccount(res.id, res.i);
-				resolve({ id: res.id, token: res.i });
-			},
-			cancelled: () => {
-				resolve(null);
-			},
-			closed: () => {
-				dispose();
-			},
-		});
-	});
-}
-
-export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> {
-	return new Promise((resolve) => {
-		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
-			done: async (res: Misskey.entities.SignupResponse) => {
-				await addAccount(res.id, res.token);
-				resolve({ id: res.id, token: res.token });
-			},
-			cancelled: () => {
-				resolve(null);
-			},
-			closed: () => {
-				dispose();
-			},
-		});
-	});
-}
-
-if (_DEV_) {
-	(window as any).$i = $i;
-}
diff --git a/packages/frontend/src/accounts.ts b/packages/frontend/src/accounts.ts
new file mode 100644
index 0000000000..2382a8ec32
--- /dev/null
+++ b/packages/frontend/src/accounts.ts
@@ -0,0 +1,341 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineAsyncComponent, ref } from 'vue';
+import * as Misskey from 'misskey-js';
+import { apiUrl, host } from '@@/js/config.js';
+import type { MenuItem } from '@/types/menu.js';
+import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
+import { i18n } from '@/i18n.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { waiting, popup, popupMenu, success, alert } from '@/os.js';
+import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
+import { prefer } from '@/preferences.js';
+import { store } from '@/store.js';
+import { $i } from '@/i.js';
+import { signout } from '@/signout.js';
+
+// TODO: 他のタブと永続化されたstateを同期
+
+type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
+
+export async function getAccounts(): Promise<{
+	host: string;
+	user: Misskey.entities.User;
+	token: string | null;
+}[]> {
+	const tokens = store.s.accountTokens;
+	const accounts = prefer.s.accounts;
+	return accounts.map(([host, user]) => ({
+		host,
+		user,
+		token: tokens[host + '/' + user.id] ?? null,
+	}));
+}
+
+async function addAccount(host: string, user: Misskey.entities.User, token: AccountWithToken['token']) {
+	if (!prefer.s.accounts.some(x => x[0] === host && x[1].id === user.id)) {
+		store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token });
+		prefer.commit('accounts', [...prefer.s.accounts, [host, user]]);
+	}
+}
+
+export async function removeAccount(host: string, id: AccountWithToken['id']) {
+	const tokens = JSON.parse(JSON.stringify(store.s.accountTokens));
+	delete tokens[host + '/' + id];
+	store.set('accountTokens', tokens);
+	prefer.commit('accounts', prefer.s.accounts.filter(x => x[0] !== host || x[1].id !== id));
+}
+
+const isAccountDeleted = Symbol('isAccountDeleted');
+
+function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Misskey.entities.MeDetailed> {
+	return new Promise((done, fail) => {
+		window.fetch(`${apiUrl}/i`, {
+			method: 'POST',
+			body: JSON.stringify({
+				i: token,
+			}),
+			headers: {
+				'Content-Type': 'application/json',
+			},
+		})
+			.then(res => new Promise<Misskey.entities.MeDetailed | { error: Record<string, any> }>((done2, fail2) => {
+				if (res.status >= 500 && res.status < 600) {
+					// サーバーエラー(5xx)の場合をrejectとする
+					// (認証エラーなど4xxはresolve)
+					return fail2(res);
+				}
+				res.json().then(done2, fail2);
+			}))
+			.then(async res => {
+				if ('error' in res) {
+					if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
+						// SUSPENDED
+						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
+							await showSuspendedDialog();
+						}
+					} else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
+						// USER_IS_DELETED
+						// アカウントが削除されている
+						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
+							await alert({
+								type: 'error',
+								title: i18n.ts.accountDeleted,
+								text: i18n.ts.accountDeletedDescription,
+							});
+						}
+					} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
+						// AUTHENTICATION_FAILED
+						// トークンが無効化されていたりアカウントが削除されたりしている
+						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
+							await alert({
+								type: 'error',
+								title: i18n.ts.tokenRevoked,
+								text: i18n.ts.tokenRevokedDescription,
+							});
+						}
+					} else {
+						await alert({
+							type: 'error',
+							title: i18n.ts.failedToFetchAccountInformation,
+							text: JSON.stringify(res.error),
+						});
+					}
+
+					fail(isAccountDeleted);
+				} else {
+					done(res);
+				}
+			})
+			.catch(fail);
+	});
+}
+
+export function updateCurrentAccount(accountData: Misskey.entities.MeDetailed) {
+	if (!$i) return;
+	const token = $i.token;
+	for (const key of Object.keys($i)) {
+		delete $i[key];
+	}
+	for (const [key, value] of Object.entries(accountData)) {
+		$i[key] = value;
+	}
+	prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
+		// TODO: $iのホストも比較したいけど通常null
+		if (user.id === $i.id) {
+			return [host, $i];
+		} else {
+			return [host, user];
+		}
+	}));
+	$i.token = token;
+	miLocalStorage.setItem('account', JSON.stringify($i));
+}
+
+export function updateCurrentAccountPartial(accountData: Partial<Misskey.entities.MeDetailed>) {
+	if (!$i) return;
+	for (const [key, value] of Object.entries(accountData)) {
+		$i[key] = value;
+	}
+	prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
+		// TODO: $iのホストも比較したいけど通常null
+		if (user.id === $i.id) {
+			const newUser = JSON.parse(JSON.stringify($i));
+			for (const [key, value] of Object.entries(accountData)) {
+				newUser[key] = value;
+			}
+			return [host, newUser];
+		}
+		return [host, user];
+	}));
+	miLocalStorage.setItem('account', JSON.stringify($i));
+}
+
+export async function refreshCurrentAccount() {
+	if (!$i) return;
+	return fetchAccount($i.token, $i.id).then(updateCurrentAccount).catch(reason => {
+		if (reason === isAccountDeleted) {
+			removeAccount(host, $i.id);
+			if (Object.keys(store.s.accountTokens).length > 0) {
+				login(Object.values(store.s.accountTokens)[0]);
+			} else {
+				signout();
+			}
+		}
+	});
+}
+
+export async function login(token: AccountWithToken['token'], redirect?: string) {
+	const showing = ref(true);
+	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
+		success: false,
+		showing: showing,
+	}, {
+		closed: () => dispose(),
+	});
+
+	const me = await fetchAccount(token, undefined, true).catch(reason => {
+		showing.value = false;
+		throw reason;
+	});
+
+	miLocalStorage.setItem('account', JSON.stringify({
+		...me,
+		token,
+	}));
+
+	await addAccount(host, me, token);
+
+	if (redirect) {
+		// 他のタブは再読み込みするだけ
+		reloadChannel.postMessage(null);
+		// このページはredirectで指定された先に移動
+		location.href = redirect;
+		return;
+	}
+
+	unisonReload();
+}
+
+export async function switchAccount(host: string, id: string) {
+	const token = store.s.accountTokens[host + '/' + id];
+	if (token) {
+		login(token);
+	} else {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
+			done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
+				store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i });
+				login(res.i);
+			},
+			closed: () => {
+				dispose();
+			},
+		});
+	}
+}
+
+export async function openAccountMenu(opts: {
+	includeCurrentAccount?: boolean;
+	withExtraOperation: boolean;
+	active?: Misskey.entities.User['id'];
+	onChoose?: (account: Misskey.entities.User) => void;
+}, ev: MouseEvent) {
+	if (!$i) return;
+
+	function createItem(host: string, account: Misskey.entities.User): MenuItem {
+		return {
+			type: 'user' as const,
+			user: account,
+			active: opts.active != null ? opts.active === account.id : false,
+			action: async () => {
+				if (opts.onChoose) {
+					opts.onChoose(account);
+				} else {
+					switchAccount(host, account.id);
+				}
+			},
+		};
+	}
+
+	const menuItems: MenuItem[] = [];
+
+	// TODO: $iのホストも比較したいけど通常null
+	const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.user.id !== $i.id))).map(a => createItem(a.host, a.user));
+
+	if (opts.withExtraOperation) {
+		menuItems.push({
+			type: 'link',
+			text: i18n.ts.profile,
+			to: `/@${$i.username}`,
+			avatar: $i,
+		}, {
+			type: 'divider',
+		});
+
+		if (opts.includeCurrentAccount) {
+			menuItems.push(createItem(host, $i));
+		}
+
+		menuItems.push(...accountItems);
+
+		menuItems.push({
+			type: 'parent',
+			icon: 'ti ti-plus',
+			text: i18n.ts.addAccount,
+			children: [{
+				text: i18n.ts.existingAccount,
+				action: () => {
+					getAccountWithSigninDialog().then(res => {
+						if (res != null) {
+							success();
+						}
+					});
+				},
+			}, {
+				text: i18n.ts.createAccount,
+				action: () => {
+					getAccountWithSignupDialog().then(res => {
+						if (res != null) {
+							switchAccount(host, res.id);
+						}
+					});
+				},
+			}],
+		}, {
+			type: 'link',
+			icon: 'ti ti-users',
+			text: i18n.ts.manageAccounts,
+			to: '/settings/accounts',
+		});
+	} else {
+		if (opts.includeCurrentAccount) {
+			menuItems.push(createItem(host, $i));
+		}
+
+		menuItems.push(...accountItems);
+	}
+
+	popupMenu(menuItems, ev.currentTarget ?? ev.target, {
+		align: 'left',
+	});
+}
+
+export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
+	return new Promise((resolve) => {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
+			done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
+				const user = await fetchAccount(res.i, res.id, true);
+				await addAccount(host, user, res.i);
+				resolve({ id: res.id, token: res.i });
+			},
+			cancelled: () => {
+				resolve(null);
+			},
+			closed: () => {
+				dispose();
+			},
+		});
+	});
+}
+
+export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> {
+	return new Promise((resolve) => {
+		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
+			done: async (res: Misskey.entities.SignupResponse) => {
+				const user = JSON.parse(JSON.stringify(res));
+				delete user.token;
+				await addAccount(host, user, res.token);
+				resolve({ id: res.id, token: res.token });
+			},
+			cancelled: () => {
+				resolve(null);
+			},
+			closed: () => {
+				dispose();
+			},
+		});
+	});
+}
diff --git a/packages/frontend/src/aiscript/api.ts b/packages/frontend/src/aiscript/api.ts
index 3acc1127c9..e7e396023d 100644
--- a/packages/frontend/src/aiscript/api.ts
+++ b/packages/frontend/src/aiscript/api.ts
@@ -9,7 +9,7 @@ import { url, lang } from '@@/js/config.js';
 import { assertStringAndIsIn } from './common.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { customEmojis } from '@/custom-emojis.js';
 
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index 122aa50ac0..73c4256c4b 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -15,7 +15,7 @@ import components from '@/components/index.js';
 import { applyTheme } from '@/theme.js';
 import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
 import { updateI18n, i18n } from '@/i18n.js';
-import { $i, refreshAccount, login } from '@/account.js';
+import { refreshCurrentAccount, login } from '@/accounts.js';
 import { store } from '@/store.js';
 import { fetchInstance, instance } from '@/instance.js';
 import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js';
@@ -29,6 +29,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.js';
 import { setupRouter } from '@/router/main.js';
 import { createMainRouter } from '@/router/definition.js';
 import { prefer } from '@/preferences.js';
+import { $i } from '@/i.js';
 
 export async function common(createVue: () => App<Element>) {
 	console.info(`Misskey v${version}`);
@@ -38,11 +39,6 @@ export async function common(createVue: () => App<Element>) {
 
 		console.info(`vue ${vueVersion}`);
 
-		// eslint-disable-next-line @typescript-eslint/no-explicit-any
-		(window as any).$i = $i;
-		// eslint-disable-next-line @typescript-eslint/no-explicit-any
-		(window as any).$store = store;
-
 		window.addEventListener('error', event => {
 			console.error(event);
 			/*
@@ -244,7 +240,7 @@ export async function common(createVue: () => App<Element>) {
 			console.log('account cache found. refreshing...');
 		}
 
-		refreshAccount();
+		refreshCurrentAccount();
 	}
 	//#endregion
 
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index be72eeb9e1..64e3a236e8 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -16,7 +16,7 @@ import { i18n } from '@/i18n.js';
 import { alert, confirm, popup, post, toast } from '@/os.js';
 import { useStream } from '@/stream.js';
 import * as sound from '@/utility/sound.js';
-import { $i, signout, updateAccountPartial } from '@/account.js';
+import { $i } from '@/i.js';
 import { instance } from '@/instance.js';
 import { ColdDeviceStorage, store } from '@/store.js';
 import { reactionPicker } from '@/utility/reaction-picker.js';
@@ -32,6 +32,8 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { launchPlugins } from '@/plugin.js';
 import { unisonReload } from '@/utility/unison-reload.js';
+import { updateCurrentAccountPartial } from '@/accounts.js';
+import { signout } from '@/signout.js';
 
 export async function mainBoot() {
 	const { isClientUpdated, lastVersion } = await common(() => {
@@ -480,11 +482,11 @@ export async function mainBoot() {
 
 		// 自分の情報が更新されたとき
 		main.on('meUpdated', i => {
-			updateAccountPartial(i);
+			updateCurrentAccountPartial(i);
 		});
 
 		main.on('readAllNotifications', () => {
-			updateAccountPartial({
+			updateCurrentAccountPartial({
 				hasUnreadNotification: false,
 				unreadNotificationsCount: 0,
 			});
@@ -492,39 +494,39 @@ export async function mainBoot() {
 
 		main.on('unreadNotification', () => {
 			const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
-			updateAccountPartial({
+			updateCurrentAccountPartial({
 				hasUnreadNotification: true,
 				unreadNotificationsCount,
 			});
 		});
 
 		main.on('unreadMention', () => {
-			updateAccountPartial({ hasUnreadMentions: true });
+			updateCurrentAccountPartial({ hasUnreadMentions: true });
 		});
 
 		main.on('readAllUnreadMentions', () => {
-			updateAccountPartial({ hasUnreadMentions: false });
+			updateCurrentAccountPartial({ hasUnreadMentions: false });
 		});
 
 		main.on('unreadSpecifiedNote', () => {
-			updateAccountPartial({ hasUnreadSpecifiedNotes: true });
+			updateCurrentAccountPartial({ hasUnreadSpecifiedNotes: true });
 		});
 
 		main.on('readAllUnreadSpecifiedNotes', () => {
-			updateAccountPartial({ hasUnreadSpecifiedNotes: false });
+			updateCurrentAccountPartial({ hasUnreadSpecifiedNotes: false });
 		});
 
 		main.on('readAllAntennas', () => {
-			updateAccountPartial({ hasUnreadAntenna: false });
+			updateCurrentAccountPartial({ hasUnreadAntenna: false });
 		});
 
 		main.on('unreadAntenna', () => {
-			updateAccountPartial({ hasUnreadAntenna: true });
+			updateCurrentAccountPartial({ hasUnreadAntenna: true });
 			sound.playMisskeySfx('antenna');
 		});
 
 		main.on('readAllAnnouncements', () => {
-			updateAccountPartial({ hasUnreadAnnouncement: false });
+			updateCurrentAccountPartial({ hasUnreadAnnouncement: false });
 		});
 
 		// 個人宛てお知らせが発行されたとき
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index 41fd2564d8..582bb137bc 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -29,7 +29,8 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 import MkModal from '@/components/MkModal.vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { $i, updateAccountPartial } from '@/account.js';
+import { $i } from '@/i.js';
+import { updateCurrentAccountPartial } from '@/accounts.js';
 
 const props = withDefaults(defineProps<{
 	announcement: Misskey.entities.Announcement;
@@ -51,7 +52,7 @@ async function ok() {
 
 	modal.value?.close();
 	misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
-	updateAccountPartial({
+	updateCurrentAccountPartial({
 		unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
 	});
 }
diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue
index 090c31044e..00bf8e68d9 100644
--- a/packages/frontend/src/components/MkAuthConfirm.vue
+++ b/packages/frontend/src/components/MkAuthConfirm.vue
@@ -117,10 +117,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script setup lang="ts">
 import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
-
 import MkButton from '@/components/MkButton.vue';
-
-import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
+import { $i } from '@/i.js';
+import { getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { getProxiedImageUrl } from '@/utility/media-proxy.js';
@@ -158,7 +157,7 @@ async function init() {
 
 	const accounts = await getAccounts();
 
-	const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id));
+	const accountIdsToFetch = accounts.map(a => a.user.id).filter(id => !users.value.has(id));
 
 	if (accountIdsToFetch.length > 0) {
 		const usersRes = await misskeyApi('users/show', {
@@ -170,7 +169,7 @@ async function init() {
 
 			users.value.set(user.id, {
 				...user,
-				token: accounts.find(a => a.id === user.id)!.token,
+				token: accounts.find(a => a.user.id === user.id)!.token,
 			});
 		}
 	}
diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue
index 5b09ec90dd..2154c08ab3 100644
--- a/packages/frontend/src/components/MkClipPreview.vue
+++ b/packages/frontend/src/components/MkClipPreview.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { computed } from 'vue';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import number from '@/filters/number.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 3c41d597de..4a89fb30ca 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -38,7 +38,7 @@ import tinycolor from 'tinycolor2';
 import { apiUrl } from '@@/js/config.js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { getProxiedImageUrl } from '@/utility/media-proxy.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index 733d50728e..f02a767186 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -44,7 +44,7 @@ import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import bytes from '@/filters/bytes.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
 import { deviceKind } from '@/utility/device-kind.js';
 import { useRouter } from '@/router/supplier.js';
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 384682277e..c0883b1342 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -136,7 +136,7 @@ import { deviceKind } from '@/utility/device-kind.js';
 import { i18n } from '@/i18n.js';
 import { store } from '@/store.js';
 import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index a063854520..b62494fa20 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -44,7 +44,7 @@ import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 import { claimAchievement } from '@/utility/achievements.js';
 import { pleaseLogin } from '@/utility/please-login.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index 70777bb89a..3113c6fad6 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -89,7 +89,7 @@ import { Chart } from 'chart.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkChart from '@/components/MkChart.vue';
 import { useChartTooltip } from '@/use/use-chart-tooltip.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import * as os from '@/os.js';
 import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { instance } from '@/instance.js';
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 33d4d269e7..096c51bbd6 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -98,7 +98,7 @@ import * as os from '@/os.js';
 import bytes from '@/filters/bytes.js';
 import { hms } from '@/filters/hms.js';
 import MkMediaRange from '@/components/MkMediaRange.vue';
-import { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 6029b1e0b6..20ac1a917e 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -60,7 +60,7 @@ import bytes from '@/filters/bytes.js';
 import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 74c1aefc3a..403ec61736 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -121,7 +121,7 @@ import * as os from '@/os.js';
 import { exitFullscreen, requestFullscreen } from '@/utility/fullscreen.js';
 import hasAudio from '@/utility/media-has-audio.js';
 import MkMediaRange from '@/components/MkMediaRange.vue';
-import { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index 419407955f..f2cf33eb65 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -18,7 +18,7 @@ import { toUnicode } from 'punycode.js';
 import { computed } from 'vue';
 import { host as localHost } from '@@/js/config.js';
 import type { MkABehavior } from '@/components/global/MkA.vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index aa352d5163..a22ad346bf 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -208,7 +208,7 @@ import * as sound from '@/utility/sound.js';
 import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { reactionPicker } from '@/utility/reaction-picker.js';
 import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
 import { useNoteCapture } from '@/use/use-note-capture.js';
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index b3f99b702a..dd8d3567b2 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -238,7 +238,7 @@ import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import * as sound from '@/utility/sound.js';
 import { reactionPicker } from '@/utility/reaction-picker.js';
 import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
 import { useNoteCapture } from '@/use/use-note-capture.js';
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index bb7347cd26..4fd1c210cb 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -48,7 +48,7 @@ import MkCwButton from '@/components/MkCwButton.vue';
 import { notePage } from '@/filters/note.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { userPage } from '@/filters/user.js';
 import { checkWordMute } from '@/utility/check-word-mute.js';
 
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 5d096cf92d..b2380a5e0e 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -169,7 +169,7 @@ import { notePage } from '@/filters/note.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { infoImageUrl } from '@/instance.js';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/components/MkPasswordDialog.vue b/packages/frontend/src/components/MkPasswordDialog.vue
index 4d1787d420..24750012d0 100644
--- a/packages/frontend/src/components/MkPasswordDialog.vue
+++ b/packages/frontend/src/components/MkPasswordDialog.vue
@@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 5e379d08b7..c6958eea77 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -127,7 +127,8 @@ import { store } from '@/store.js';
 import MkInfo from '@/components/MkInfo.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { signinRequired, notesCount, incNotesCount, getAccounts, openAccountMenu as openAccountMenu_ } from '@/account.js';
+import { signinRequired, notesCount, incNotesCount } from '@/i.js';
+import { getAccounts, openAccountMenu as openAccountMenu_ } from '@/accounts.js';
 import { uploadFile } from '@/utility/upload.js';
 import { deepClone } from '@/utility/clone.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
@@ -837,7 +838,7 @@ async function post(ev?: MouseEvent) {
 
 	if (postAccount.value) {
 		const storedAccounts = await getAccounts();
-		token = storedAccounts.find(x => x.id === postAccount.value?.id)?.token;
+		token = storedAccounts.find(x => x.user.id === postAccount.value?.id)?.token;
 	}
 
 	posting.value = true;
diff --git a/packages/frontend/src/components/MkPreview.vue b/packages/frontend/src/components/MkPreview.vue
index 6efd99d14b..d8dfbd1655 100644
--- a/packages/frontend/src/components/MkPreview.vue
+++ b/packages/frontend/src/components/MkPreview.vue
@@ -43,7 +43,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkRadio from '@/components/MkRadio.vue';
 import * as os from '@/os.js';
 import * as config from '@@/js/config.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const text = ref('');
 const flag = ref(true);
diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
index 780f8bc6d0..9c37eb5e72 100644
--- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
@@ -42,12 +42,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script setup lang="ts">
 import { ref } from 'vue';
-import { $i, getAccounts } from '@/account.js';
+import { $i } from '@/i.js';
 import MkButton from '@/components/MkButton.vue';
 import { instance } from '@/instance.js';
 import { apiWithDialog, promiseDialog } from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
+import { getAccounts } from '@/accounts.js';
 
 defineProps<{
 	primary?: boolean;
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index d079e68cde..12a066c710 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -27,7 +27,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import * as os from '@/os.js';
 import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { useTooltip } from '@/use/use-tooltip.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkReactionEffect from '@/components/MkReactionEffect.vue';
 import { claimAchievement } from '@/utility/achievements.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 2dcc87f425..b0fbe3c490 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -67,20 +67,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
-
 import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
 import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
+import type { PwResponse } from '@/components/MkSignin.password.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
-import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 
 import XInput from '@/components/MkSignin.input.vue';
 import XPassword from '@/components/MkSignin.password.vue';
-import type { PwResponse } from '@/components/MkSignin.password.vue';
 import XTotp from '@/components/MkSignin.totp.vue';
 import XPasskey from '@/components/MkSignin.passkey.vue';
+import { login } from '@/accounts.js';
 
 const emit = defineEmits<{
 	(ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 151cd60fb2..78b6722c1e 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -85,13 +85,13 @@ import * as Misskey from 'misskey-js';
 import * as config from '@@/js/config.js';
 import MkButton from './MkButton.vue';
 import MkInput from './MkInput.vue';
-import MkCaptcha from '@/components/MkCaptcha.vue';
 import type { Captcha } from '@/components/MkCaptcha.vue';
+import MkCaptcha from '@/components/MkCaptcha.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { login } from '@/account.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
+import { login } from '@/accounts.js';
 
 const props = withDefaults(defineProps<{
 	autoSet?: boolean;
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 62ae9a048a..e8b740eae2 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -25,7 +25,7 @@ import MkNotes from '@/components/MkNotes.vue';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import { useStream } from '@/stream.js';
 import * as sound from '@/utility/sound.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { instance } from '@/instance.js';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index 31ecb15ab8..bed15031cb 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -55,7 +55,7 @@ import MkButton from './MkButton.vue';
 import MkInfo from './MkInfo.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n.js';
-import { iAmAdmin } from '@/account.js';
+import { iAmAdmin } from '@/i.js';
 
 const props = withDefaults(defineProps<{
 	title?: string | null;
diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue
index b26a01737e..59e1b096ae 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Note.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue
@@ -27,7 +27,7 @@ import * as Misskey from 'misskey-js';
 import { ref, reactive } from 'vue';
 import { i18n } from '@/i18n.js';
 import { globalEvents } from '@/events.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkNote from '@/components/MkNote.vue';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
index f7b60fbc45..8ae6c1ceaa 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
@@ -31,7 +31,7 @@ import MkPostForm from '@/components/MkPostForm.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkNote from '@/components/MkNote.vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const emit = defineEmits<{
 	(ev: 'succeeded'): void;
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index eb189b446b..cff531b2ca 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -39,7 +39,7 @@ import MkFollowButton from '@/components/MkFollowButton.vue';
 import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
 import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index 8a89b253e3..7e8b1200d5 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -65,7 +65,7 @@ import { getUserMenu } from '@/utility/get-user-menu.js';
 import number from '@/filters/number.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
 import { getStaticImageUrl } from '@/utility/media-proxy.js';
 
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index e5c6df267b..6bf3eb44dc 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -70,7 +70,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { instance } from '@/instance.js';
 
 const emit = defineEmits<{
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 14acfd3f89..7a5e5772a4 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -39,7 +39,7 @@ import FormSlot from '@/components/form/slot.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { chooseFileFromPc } from '@/utility/select-file.js';
 import * as os from '@/os.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index c196519c15..b55069ca25 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -47,7 +47,7 @@ import { instance } from '@/instance.js';
 import MkButton from '@/components/MkButton.vue';
 import { store } from '@/store.js';
 import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 type Ad = (typeof instance)['ads'][number];
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 9981092dff..20a07e9c28 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -35,7 +35,7 @@ import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import * as sound from '@/utility/sound.js';
 import { i18n } from '@/i18n.js';
 import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 728e37cf51..69bbd88cb6 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -50,7 +50,8 @@ import type { PageHeaderItem } from '@/types/page-header.js';
 import type { PageMetadata } from '@/page.js';
 import { globalEvents } from '@/events.js';
 import { injectReactiveMetadata } from '@/page.js';
-import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
+import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
+import { $i } from '@/i.js';
 
 const props = withDefaults(defineProps<{
 	overridePageMetadata?: PageMetadata;
diff --git a/packages/frontend/src/i.ts b/packages/frontend/src/i.ts
new file mode 100644
index 0000000000..aa84c6aa61
--- /dev/null
+++ b/packages/frontend/src/i.ts
@@ -0,0 +1,34 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { reactive } from 'vue';
+import * as Misskey from 'misskey-js';
+import { miLocalStorage } from '@/local-storage.js';
+
+// TODO: 他のタブと永続化されたstateを同期
+
+type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
+
+const accountData = miLocalStorage.getItem('account');
+
+// TODO: 外部からはreadonlyに
+export const $i = accountData ? reactive(JSON.parse(accountData) as AccountWithToken) : null;
+
+export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
+export const iAmAdmin = $i != null && $i.isAdmin;
+
+export function signinRequired() {
+	if ($i == null) throw new Error('signin required');
+	return $i;
+}
+
+export let notesCount = $i == null ? 0 : $i.notesCount;
+export function incNotesCount() {
+	notesCount++;
+}
+
+if (_DEV_) {
+	(window as any).$i = $i;
+}
diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts
index 3977edb91b..099339fbee 100644
--- a/packages/frontend/src/local-storage.ts
+++ b/packages/frontend/src/local-storage.ts
@@ -9,7 +9,6 @@ export type Keys = (
 	'instance' |
 	'instanceCachedAt' |
 	'account' |
-	'accounts' |
 	'latestDonationInfoShownAt' |
 	'neverShowDonationInfo' |
 	'neverShowLocalOnlyInfo' |
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index 837f333c9a..c0a6a370fc 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -5,7 +5,7 @@
 
 import { computed, reactive } from 'vue';
 import { clearCache } from './utility/clear-cache.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js';
 import { lookup } from '@/utility/lookup.js';
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index 5395429d7f..36dac2954d 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -146,7 +146,7 @@ import { instance } from '@/instance.js';
 import * as os from '@/os.js';
 import { definePage } from '@/page.js';
 import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 const patronsWithIcon = [{
diff --git a/packages/frontend/src/pages/about.emojis.vue b/packages/frontend/src/pages/about.emojis.vue
index d7d526f3ba..b166dfd940 100644
--- a/packages/frontend/src/pages/about.emojis.vue
+++ b/packages/frontend/src/pages/about.emojis.vue
@@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { customEmojis, customEmojiCategories, getCustomEmojiTags } from '@/custom-emojis.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const customEmojiTags = getCustomEmojiTags();
 const q = ref('');
diff --git a/packages/frontend/src/pages/achievements.vue b/packages/frontend/src/pages/achievements.vue
index 53ce75f9bf..ca2443cc5b 100644
--- a/packages/frontend/src/pages/achievements.vue
+++ b/packages/frontend/src/pages/achievements.vue
@@ -17,7 +17,7 @@ import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
 import MkAchievements from '@/components/MkAchievements.vue';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { claimAchievement } from '@/utility/achievements.js';
 
 let timer: number | null;
diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue
index 0af28e94fa..9274b90892 100644
--- a/packages/frontend/src/pages/admin-file.vue
+++ b/packages/frontend/src/pages/admin-file.vue
@@ -86,7 +86,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { iAmAdmin, iAmModerator } from '@/account.js';
+import { iAmAdmin, iAmModerator } from '@/i.js';
 
 const tab = ref('overview');
 const file = ref<Misskey.entities.DriveFile | null>(null);
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 2efae999ce..ff1d5e64d3 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -233,7 +233,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 import { acct } from '@/filters/user.js';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
-import { iAmAdmin, $i, iAmModerator } from '@/account.js';
+import { iAmAdmin, $i, iAmModerator } from '@/i.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 import MkPagination from '@/components/MkPagination.vue';
 
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue
index 977bbe0b47..6562610b12 100644
--- a/packages/frontend/src/pages/announcement.vue
+++ b/packages/frontend/src/pages/announcement.vue
@@ -55,8 +55,9 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { $i, updateAccountPartial } from '@/account.js';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
+import { updateCurrentAccountPartial } from '@/accounts.js';
 
 const props = defineProps<{
 	announcementId: string;
@@ -90,7 +91,7 @@ async function read(target: Misskey.entities.Announcement): Promise<void> {
 	target.isRead = true;
 	await misskeyApi('i/read-announcement', { announcementId: target.id });
 	if ($i) {
-		updateAccountPartial({
+		updateCurrentAccountPartial({
 			unreadAnnouncements: $i.unreadAnnouncements.filter((a: { id: string; }) => a.id !== target.id),
 		});
 	}
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index 13f28d9b35..2387ac728b 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -56,7 +56,8 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { $i, updateAccountPartial } from '@/account.js';
+import { $i } from '@/i.js';
+import { updateCurrentAccountPartial } from '@/accounts.js';
 
 const paginationCurrent = {
 	endpoint: 'announcements' as const,
@@ -94,7 +95,7 @@ async function read(target) {
 		return a;
 	});
 	misskeyApi('i/read-announcement', { announcementId: target.id });
-	updateAccountPartial({
+	updateCurrentAccountPartial({
 		unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== target.id),
 	});
 }
diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue
index 8b0fde4a25..e4699379f0 100644
--- a/packages/frontend/src/pages/auth.vue
+++ b/packages/frontend/src/pages/auth.vue
@@ -47,9 +47,10 @@ import * as Misskey from 'misskey-js';
 import XForm from './auth.form.vue';
 import MkSignin from '@/components/MkSignin.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i, login } from '@/account.js';
+import { $i } from '@/i.js';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
+import { login } from '@/accounts.js';
 
 const props = defineProps<{
 	token: string;
diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
index a3c5a36614..884429dfeb 100644
--- a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
+++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
@@ -73,7 +73,7 @@ import { i18n } from '@/i18n.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index eb1015b19e..b84b9efc1a 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, computed, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index a774aa6e44..c5951f0e13 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -82,7 +82,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
 import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { deviceKind } from '@/utility/device-kind.js';
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 9765ebf216..590a506a55 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -36,7 +36,7 @@ import * as Misskey from 'misskey-js';
 import { url } from '@@/js/config.js';
 import type { MenuItem } from '@/types/menu.js';
 import MkNotes from '@/components/MkNotes.vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index f760aca8ae..364006e9ad 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -208,7 +208,7 @@ import { claimAchievement } from '@/utility/achievements.js';
 import { store } from '@/store.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import * as sound from '@/utility/sound.js';
 import MkRange from '@/components/MkRange.vue';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index 35a240b9ba..bedb0b64f9 100644
--- a/packages/frontend/src/pages/emojis.emoji.vue
+++ b/packages/frontend/src/pages/emojis.emoji.vue
@@ -22,7 +22,7 @@ import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { i18n } from '@/i18n.js';
 import MkCustomEmojiDetailedDialog from '@/components/MkCustomEmojiDetailedDialog.vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const props = defineProps<{
   emoji: Misskey.entities.EmojiSimple;
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 6bce6689d4..08ac913958 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -80,7 +80,7 @@ import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkCode from '@/components/MkCode.vue';
 import { prefer } from '@/preferences.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { isSupportShare } from '@/utility/navigator.js';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { pleaseLogin } from '@/utility/please-login.js';
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index bd48b882d2..7e496f522d 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -54,7 +54,7 @@ import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { infoImageUrl } from '@/instance.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
 const paginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index 56ddb820cf..eb01aadbcc 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -77,7 +77,7 @@ import MkFollowButton from '@/components/MkFollowButton.vue';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { prefer } from '@/preferences.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { isSupportShare } from '@/utility/navigator.js';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { useRouter } from '@/router/supplier.js';
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index eddeb4aba9..c4aed8d6df 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -148,7 +148,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import number from '@/filters/number.js';
-import { iAmModerator, iAmAdmin } from '@/account.js';
+import { iAmModerator, iAmAdmin } from '@/i.js';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index 8369927d85..352e1d9386 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -45,7 +45,7 @@ import type { Paging } from '@/components/MkPagination.vue';
 import MkInviteCode from '@/components/MkInviteCode.vue';
 import { definePage } from '@/page.js';
 import { serverErrorImageUrl, instance } from '@/instance.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
 const currentInviteLimit = ref<null | number>(null);
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index 0bc9b3f3c2..cc701cb16b 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -37,7 +37,7 @@ import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { userListsCache } from '@/cache.js';
 import { infoImageUrl } from '@/instance.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index fdee890cfd..6481c0da0c 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -66,7 +66,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkInput from '@/components/MkInput.vue';
 import { userListsCache } from '@/cache.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import MkPagination from '@/components/MkPagination.vue';
 import { mainRouter } from '@/router/main.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 6f53cba806..fb83993fee 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -65,7 +65,7 @@ import { prefer } from '@/preferences.js';
 import { pleaseLogin } from '@/utility/please-login.js';
 import { getAppearNote } from '@/utility/get-appear-note.js';
 import { serverContext, assertServerContext } from '@/server-context.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 // contextは非ログイン状態の情報しかないためログイン時は利用できない
 const CTX_NOTE = !$i && assertServerContext(serverContext, 'note') ? serverContext.note : null;
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index ed701ed3c0..e2f6084252 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -75,7 +75,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 import { selectFile } from '@/utility/select-file.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { mainRouter } from '@/router/main.js';
 import { getPageBlockList } from '@/pages/page-editor/common.js';
 
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index 1c288442b5..00c664d2a0 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -115,7 +115,7 @@ import MkPagePreview from '@/components/MkPagePreview.vue';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { deepClone } from '@/utility/clone.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { isSupportShare } from '@/utility/navigator.js';
 import { instance } from '@/instance.js';
 import { getStaticImageUrl } from '@/utility/media-proxy.js';
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 71dd220cfe..ef9cc242c6 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -150,7 +150,7 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import { deepClone } from '@/utility/clone.js';
 import { useInterval } from '@@/js/use-interval.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index 03b75f89ae..2715b70b95 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -114,7 +114,7 @@ import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as Reversi from 'misskey-reversi';
 import { i18n } from '@/i18n.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { deepClone } from '@/utility/clone.js';
 import MkButton from '@/components/MkButton.vue';
 import MkRadios from '@/components/MkRadios.vue';
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
index 053ec2aa08..662df00d9b 100644
--- a/packages/frontend/src/pages/reversi/game.vue
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -17,7 +17,7 @@ import GameBoard from './game.board.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { definePage } from '@/page.js';
 import { useStream } from '@/stream.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { useRouter } from '@/router/supplier.js';
 import * as os from '@/os.js';
 import { url } from '@@/js/config.js';
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index ff2e7e922f..d66ff8db05 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -113,7 +113,7 @@ import { useStream } from '@/stream.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkPagination from '@/components/MkPagination.vue';
 import { useRouter } from '@/router/supplier.js';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index ed5cd23b23..b0d3b5bbd2 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -66,7 +66,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { registerAsUiLib } from '@/aiscript/ui.js';
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index bfe56e793b..4801e9bc27 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -114,7 +114,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref, shallowRef, toRef } from 'vue';
 import type * as Misskey from 'misskey-js';
 import type { Paging } from '@/components/MkPagination.vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { host as localHost } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 9093ffd7a9..41a2535813 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -117,7 +117,7 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkLink from '@/components/MkLink.vue';
 import { confetti } from '@/utility/confetti.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 806599e801..20d1b0fe0f 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -92,8 +92,9 @@ import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkLink from '@/components/MkLink.vue';
 import * as os from '@/os.js';
-import { signinRequired, updateAccountPartial } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { i18n } from '@/i18n.js';
+import { updateCurrentAccountPartial } from '@/accounts.js';
 
 const $i = signinRequired();
 
@@ -131,7 +132,7 @@ async function unregisterTOTP(): Promise<void> {
 		password: auth.result.password,
 		token: auth.result.token,
 	}).then(res => {
-		updateAccountPartial({
+		updateCurrentAccountPartial({
 			twoFactorEnabled: false,
 		});
 	}).catch(error => {
diff --git a/packages/frontend/src/pages/settings/account-data.vue b/packages/frontend/src/pages/settings/account-data.vue
index ed5fe48821..14bea577a3 100644
--- a/packages/frontend/src/pages/settings/account-data.vue
+++ b/packages/frontend/src/pages/settings/account-data.vue
@@ -167,7 +167,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
 import { selectFile } from '@/utility/select-file.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
 import { prefer } from '@/preferences.js';
 
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 2cf65be2d0..749ae5147f 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -4,80 +4,51 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div class="">
-	<FormSuspense :p="init">
-		<div class="_gaps">
-			<div class="_buttons">
-				<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
-				<MkButton @click="init"><i class="ti ti-refresh"></i> {{ i18n.ts.reloadAccountsList }}</MkButton>
-			</div>
-
-			<template v-for="[id, user] in accounts">
-				<MkUserCardMini v-if="user != null" :key="user.id" :user="user" :class="$style.user" @click.prevent="menu(user, $event)"/>
-				<button v-else v-panel class="_button" :class="$style.unknownUser" @click="menu(id, $event)">
-					<div :class="$style.unknownUserAvatarMock"><i class="ti ti-user-question"></i></div>
-					<div>
-						<div :class="$style.unknownUserTitle">{{ i18n.ts.unknown }}</div>
-						<div :class="$style.unknownUserSub">ID: <span class="_monospace">{{ id }}</span></div>
-					</div>
-				</button>
-			</template>
+<SearchMarker path="/settings/accounts" :label="i18n.ts.accounts" :keywords="['accounts']" icon="ti ti-users">
+	<div class="_gaps">
+		<div class="_buttons">
+			<MkButton primary @click="addAccount"><i class="ti ti-plus"></i> {{ i18n.ts.addAccount }}</MkButton>
+			<!--<MkButton @click="refreshAllAccounts"><i class="ti ti-refresh"></i></MkButton>-->
 		</div>
-	</FormSuspense>
-</div>
+
+		<MkUserCardMini v-for="x in accounts" :key="x[0] + x[1].id" :user="x[1]" :class="$style.user" @click.prevent="menu(x[0], x[1], $event)"/>
+	</div>
+</SearchMarker>
 </template>
 
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
-import FormSuspense from '@/components/form/suspense.vue';
+import type { MenuItem } from '@/types/menu.js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
+import { $i } from '@/i.js';
+import { switchAccount, removeAccount, login, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import type { MenuItem } from '@/types/menu.js';
+import { prefer } from '@/preferences.js';
 
-const storedAccounts = ref<{ id: string, token: string }[] | null>(null);
-const accounts = ref(new Map<string, Misskey.entities.UserDetailed | null>());
+const accounts = prefer.r.accounts;
 
-const init = async () => {
-	getAccounts().then(accounts => {
-		storedAccounts.value = accounts.filter(x => x.id !== $i!.id);
+function refreshAllAccounts() {
+	// TODO
+}
 
-		return misskeyApi('users/show', {
-			userIds: storedAccounts.value.map(x => x.id),
-		});
-	}).then(response => {
-		if (storedAccounts.value == null) return;
-		accounts.value = new Map(storedAccounts.value.map(x => [x.id, response.find((y: Misskey.entities.UserDetailed) => y.id === x.id) ?? null]));
-	});
-};
-
-function menu(account: Misskey.entities.UserDetailed | string, ev: MouseEvent) {
+function menu(host: string, account: Misskey.entities.UserDetailed, ev: MouseEvent) {
 	let menu: MenuItem[];
 
-	if (typeof account === 'string') {
-		menu = [{
-			text: i18n.ts.logout,
-			icon: 'ti ti-trash',
-			danger: true,
-			action: () => removeAccount(account),
-		}];
-	} else {
-		menu = [{
-			text: i18n.ts.switch,
-			icon: 'ti ti-switch-horizontal',
-			action: () => switchAccount(account.id),
-		}, {
-			text: i18n.ts.logout,
-			icon: 'ti ti-trash',
-			danger: true,
-			action: () => removeAccount(account.id),
-		}];
-	}
+	menu = [{
+		text: i18n.ts.switch,
+		icon: 'ti ti-switch-horizontal',
+		action: () => switchAccount(host, account.id),
+	}, {
+		text: i18n.ts.logout,
+		icon: 'ti ti-trash',
+		danger: true,
+		action: () => removeAccount(host, account.id),
+	}];
 
 	os.popupMenu(menu, ev.currentTarget ?? ev.target);
 }
@@ -92,16 +63,10 @@ function addAccount(ev: MouseEvent) {
 	}], ev.currentTarget ?? ev.target);
 }
 
-async function removeAccount(id: string) {
-	await _removeAccount(id);
-	accounts.value.delete(id);
-}
-
 function addExistingAccount() {
 	getAccountWithSigninDialog().then((res) => {
 		if (res != null) {
 			os.success();
-			init();
 		}
 	});
 }
@@ -109,21 +74,11 @@ function addExistingAccount() {
 function createAccount() {
 	getAccountWithSignupDialog().then((res) => {
 		if (res != null) {
-			switchAccountWithToken(res.token);
+			login(res.token);
 		}
 	});
 }
 
-async function switchAccount(id: string) {
-	const fetchedAccounts = await getAccounts();
-	const token = fetchedAccounts.find(x => x.id === id)!.token;
-	switchAccountWithToken(token);
-}
-
-function switchAccountWithToken(token: string) {
-	login(token);
-}
-
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
index 3c9914b4e2..6b3bb1b513 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed } from 'vue';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index 40542ad5b2..e4803eda2e 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -51,7 +51,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import { i18n } from '@/i18n.js';
 import MkRange from '@/components/MkRange.vue';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue
index ba25eee175..91549e5240 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.vue
@@ -54,7 +54,7 @@ import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import MkInfo from '@/components/MkInfo.vue';
 import { definePage } from '@/page.js';
 
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 34941d5af0..0b25ee5e37 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -104,7 +104,7 @@ import bytes from '@/filters/bytes.js';
 import MkChart from '@/components/MkChart.vue';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { prefer } from '@/preferences.js';
 import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 import MkFeatureBanner from '@/components/MkFeatureBanner.vue';
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index 0cbda44882..10e2a000d4 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -67,7 +67,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkDisableSection from '@/components/MkDisableSection.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { instance } from '@/instance.js';
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index 0579b6d14a..ba5957f06a 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -38,7 +38,7 @@ import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
 import { i18n } from '@/i18n.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkSuperMenu from '@/components/MkSuperMenu.vue';
-import { signout, $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { clearCache } from '@/utility/clear-cache.js';
 import { instance } from '@/instance.js';
 import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
@@ -47,6 +47,7 @@ import { useRouter } from '@/router/supplier.js';
 import { searchIndexes } from '@/utility/autogen/settings-search-index.js';
 import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js';
 import { store } from '@/store.js';
+import { signout } from '@/signout.js';
 
 const SETTING_INDEX = searchIndexes; // TODO: lazy load
 
diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue
index 60386da545..260e390b51 100644
--- a/packages/frontend/src/pages/settings/migration.vue
+++ b/packages/frontend/src/pages/settings/migration.vue
@@ -68,7 +68,7 @@ import MkUserInfo from '@/components/MkUserInfo.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { unisonReload } from '@/utility/unison-reload.js';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
index 52e1937663..1c942e715a 100644
--- a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
@@ -19,7 +19,7 @@ import { ref, watch } from 'vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkButton from '@/components/MkButton.vue';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index a5ab7caf99..ce762af071 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -188,7 +188,7 @@ import { definePage } from '@/page.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import * as os from '@/os.js';
 import { instance, infoImageUrl } from '@/instance.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index 49910cdf4a..785fcdfbce 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -75,7 +75,7 @@ import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import * as os from '@/os.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 62b0f5c941..27fb743cb2 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -127,13 +127,14 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { signout, signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { definePage } from '@/page.js';
 import { reloadAsk } from '@/utility/reload-ask.js';
 import FormSection from '@/components/form/section.vue';
 import { prefer } from '@/preferences.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
+import { signout } from '@/signout.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index edc750c295..b73f781b9c 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -185,7 +185,7 @@ import MkFolder from '@/components/MkFolder.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { definePage } from '@/page.js';
 import FormSlot from '@/components/form/slot.vue';
 import { formatDateTimeString } from '@/utility/format-time-string.js';
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 1d85ba7834..b12ba9fe93 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -164,7 +164,7 @@ import FormSlot from '@/components/form/slot.vue';
 import { selectFile } from '@/utility/select-file.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { signinRequired } from '@/account.js';
+import { signinRequired } from '@/i.js';
 import { langmap } from '@/utility/langmap.js';
 import { definePage } from '@/page.js';
 import { claimAchievement } from '@/utility/achievements.js';
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index c3f1b4b18e..e37f05f1be 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -28,10 +28,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import MkAnimBg from '@/components/MkAnimBg.vue';
-import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
+import { login } from '@/accounts.js';
 
 const submitting = ref(false);
 
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 1af69d82db..868c64d06d 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -25,7 +25,7 @@ import MkNotes from '@/components/MkNotes.vue';
 import MkButton from '@/components/MkButton.vue';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { store } from '@/store.js';
 import * as os from '@/os.js';
 import { genEmbedCode } from '@/utility/get-embed-code.js';
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index 5ee42cc1ec..45d8c96ab7 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -86,7 +86,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkFolder from '@/components/MkFolder.vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { applyTheme } from '@/theme.js';
 import * as os from '@/os.js';
 import { store } from '@/store.js';
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 0ac6979b05..68f4b7a26d 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -48,7 +48,7 @@ import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { definePage } from '@/page.js';
 import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js';
 import { deviceKind } from '@/utility/device-kind.js';
diff --git a/packages/frontend/src/pages/user/achievements.vue b/packages/frontend/src/pages/user/achievements.vue
index b78ac2dc17..8f13e959e1 100644
--- a/packages/frontend/src/pages/user/achievements.vue
+++ b/packages/frontend/src/pages/user/achievements.vue
@@ -14,7 +14,7 @@ import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkAchievements from '@/components/MkAchievements.vue';
 import { claimAchievement } from '@/utility/achievements.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const props = defineProps<{
 	user: Misskey.entities.User;
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 6450f1e077..149481f99b 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -176,7 +176,7 @@ import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import { dateString } from '@/filters/date.js';
 import { confetti } from '@/utility/confetti.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index b5127de390..16413a55cc 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -38,7 +38,7 @@ import { acct as getAcct } from '@/filters/user.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { serverContext, assertServerContext } from '@/server-context.js';
 
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 939ca934e8..d9e3ca9966 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -45,9 +45,9 @@ import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import MkAnimBg from '@/components/MkAnimBg.vue';
+import { login } from '@/accounts.js';
 
 const username = ref('');
 const password = ref('');
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index f55b1e93cf..3ebf2ab4e4 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -8,7 +8,7 @@
 import { onUnmounted, ref, watch } from 'vue';
 import { BroadcastChannel } from 'broadcast-channel';
 import type { Ref } from 'vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { get, set } from '@/utility/idb-proxy.js';
 import { store } from '@/store.js';
diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
index 8785acd7d1..f9e6ab2a75 100644
--- a/packages/frontend/src/preferences.ts
+++ b/packages/frontend/src/preferences.ts
@@ -9,7 +9,7 @@ import { cloudBackup } from '@/preferences/utility.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { isSameScope, PreferencesManager } from '@/preferences/manager.js';
 import { store } from '@/store.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 
 const TAB_ID = uuid();
diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts
index eb3d6eeac4..e460359acd 100644
--- a/packages/frontend/src/preferences/def.ts
+++ b/packages/frontend/src/preferences/def.ts
@@ -32,6 +32,10 @@ export type SoundStore = {
 // NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
 
 export const PREF_DEF = {
+	accounts: {
+		default: [] as [host: string, user: Misskey.entities.User][],
+	},
+
 	pinnedUserLists: {
 		accountDependent: true,
 		default: [] as Misskey.entities.UserList[],
diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts
index b053cadacb..fad0226b6e 100644
--- a/packages/frontend/src/preferences/manager.ts
+++ b/packages/frontend/src/preferences/manager.ts
@@ -9,7 +9,7 @@ import { host, version } from '@@/js/config.js';
 import { PREF_DEF } from './def.js';
 import type { Ref, WritableComputedRef } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts
index c37dbcf96b..bf3dfa157f 100644
--- a/packages/frontend/src/preferences/utility.ts
+++ b/packages/frontend/src/preferences/utility.ts
@@ -12,7 +12,7 @@ import { miLocalStorage } from '@/local-storage.js';
 import { prefer } from '@/preferences.js';
 import * as os from '@/os.js';
 import { store } from '@/store.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { unisonReload } from '@/utility/unison-reload.js';
 
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 73920766d7..9a81032cc8 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -7,7 +7,7 @@ import { defineAsyncComponent } from 'vue';
 import type { AsyncComponentLoader } from 'vue';
 import type { IRouter, RouteDef } from '@/nirax.js';
 import { Router } from '@/nirax.js';
-import { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import MkLoading from '@/pages/_loading_.vue';
 import MkError from '@/pages/_error_.vue';
 
diff --git a/packages/frontend/src/signout.ts b/packages/frontend/src/signout.ts
new file mode 100644
index 0000000000..8e90552546
--- /dev/null
+++ b/packages/frontend/src/signout.ts
@@ -0,0 +1,54 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { defineAsyncComponent, ref } from 'vue';
+import { apiUrl, host } from '@@/js/config.js';
+import { defaultMemoryStorage } from '@/memory-storage';
+import { i18n } from '@/i18n.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { waiting, popup, popupMenu, success, alert } from '@/os.js';
+import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
+import { prefer } from '@/preferences.js';
+import { store } from '@/store.js';
+import { $i } from '@/i.js';
+
+export async function signout() {
+	if (!$i) return;
+
+	defaultMemoryStorage.clear();
+
+	waiting();
+	miLocalStorage.removeItem('account');
+
+	// TODO: preferencesも削除
+
+	//#region Remove service worker registration
+	try {
+		if (navigator.serviceWorker.controller) {
+			const registration = await navigator.serviceWorker.ready;
+			const push = await registration.pushManager.getSubscription();
+			if (push) {
+				await window.fetch(`${apiUrl}/sw/unregister`, {
+					method: 'POST',
+					body: JSON.stringify({
+						i: $i.token,
+						endpoint: push.endpoint,
+					}),
+					headers: {
+						'Content-Type': 'application/json',
+					},
+				});
+			}
+		}
+
+		await navigator.serviceWorker.getRegistrations()
+			.then(registrations => {
+				return Promise.all(registrations.map(registration => registration.unregister()));
+			});
+	} catch (err) {}
+	//#endregion
+
+	unisonReload('/');
+}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 9a61e63d0e..ffbd8d10ef 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -104,6 +104,10 @@ export const store = markRaw(new Storage('base', {
 		where: 'deviceAccount',
 		default: {} as Record<string, string>, // plugin id, token
 	},
+	accountTokens: {
+		where: 'device',
+		default: {} as Record<string, string>, // host/userId, token
+	},
 
 	enablePreferencesAutoCloudBackup: {
 		where: 'device',
diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index e194e96a7f..c97d7d4071 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -5,7 +5,7 @@
 
 import * as Misskey from 'misskey-js';
 import { markRaw } from 'vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { wsOrigin } from '@@/js/config.js';
 // TODO: No WebsocketモードでStreamMockが使えそう
 //import { StreamMock } from '@/utility/stream-mock.js';
diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts
index 5d09ec27f9..2ae5d8730e 100644
--- a/packages/frontend/src/theme-store.ts
+++ b/packages/frontend/src/theme-store.ts
@@ -5,7 +5,7 @@
 
 import type { Theme } from '@/theme.js';
 import { getBuiltinThemes } from '@/theme.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { prefer } from '@/preferences.js';
 
 export function getThemes(): Theme[] {
diff --git a/packages/frontend/src/timelines.ts b/packages/frontend/src/timelines.ts
index 94eda3545e..a39ccd481d 100644
--- a/packages/frontend/src/timelines.ts
+++ b/packages/frontend/src/timelines.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { instance } from '@/instance.js';
 
 export const basicTimelineTypes = [
diff --git a/packages/frontend/src/ui/_common_/PreferenceRestore.vue b/packages/frontend/src/ui/_common_/PreferenceRestore.vue
index c70b82cd0e..5fd9f5e44b 100644
--- a/packages/frontend/src/ui/_common_/PreferenceRestore.vue
+++ b/packages/frontend/src/ui/_common_/PreferenceRestore.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { hideRestoreBackupSuggestion, restoreFromCloudBackup } from '@/preferences/utility.js';
 
diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue
index d153dc8726..f9af8e1ee7 100644
--- a/packages/frontend/src/ui/_common_/announcements.vue
+++ b/packages/frontend/src/ui/_common_/announcements.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/ui/_common_/common.ts b/packages/frontend/src/ui/_common_/common.ts
index 8e5ba8927a..819e1fa42f 100644
--- a/packages/frontend/src/ui/_common_/common.ts
+++ b/packages/frontend/src/ui/_common_/common.ts
@@ -9,7 +9,7 @@ import * as os from '@/os.js';
 import { instance } from '@/instance.js';
 import { host } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 function toolsMenuItems(): MenuItem[] {
 	return [{
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index e218cd8c62..a39a4ee86b 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -53,7 +53,7 @@ import { popups } from '@/os.js';
 import { pendingApiRequestsCount } from '@/utility/misskey-api.js';
 import { uploads } from '@/utility/upload.js';
 import * as sound from '@/utility/sound.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index 698e9d8d47..2fbc9ab4b3 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -53,10 +53,11 @@ import { computed, defineAsyncComponent, toRef } from 'vue';
 import { openInstanceMenu } from './common.js';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
-import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
 import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
+import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
+import { $i } from '@/i.js';
 
 const menu = toRef(prefer.s, 'menu');
 const otherMenuItemIndicated = computed(() => {
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 234972e76d..1810ec1743 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -93,13 +93,14 @@ import { computed, defineAsyncComponent, ref, watch } from 'vue';
 import { openInstanceMenu } from './common.js';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
-import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
 import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
 import { useRouter } from '@/router/supplier.js';
 import { prefer } from '@/preferences.js';
+import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
+import { $i } from '@/i.js';
 
 const router = useRouter();
 
diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts
index df392c6532..ae61e497b5 100644
--- a/packages/frontend/src/ui/_common_/sw-inject.ts
+++ b/packages/frontend/src/ui/_common_/sw-inject.ts
@@ -5,10 +5,11 @@
 
 import { post } from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i, login } from '@/account.js';
+import { $i } from '@/i.js';
 import { getAccountFromId } from '@/utility/get-account-from-id.js';
 import { deepClone } from '@/utility/clone.js';
 import { mainRouter } from '@/router/main.js';
+import { login } from '@/accounts.js';
 
 export function swInject() {
 	navigator.serviceWorker.addEventListener('message', async ev => {
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index 39b40754ff..1e8a342977 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -51,12 +51,12 @@ import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
 import { openInstanceMenu } from './_common_/common.js';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
-import { openAccountMenu as openAccountMenu_, $i } from '@/account.js';
 import MkButton from '@/components/MkButton.vue';
-import { store } from '@/store.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
+import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
+import { $i } from '@/i.js';
 
 const WINDOW_THRESHOLD = 1400;
 
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 259aad7401..096ea0d4cf 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -54,7 +54,6 @@ import { openInstanceMenu } from './_common_/common.js';
 // import { host } from '@@/js/config.js';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
-import { openAccountMenu as openAccountMenu_, $i } from '@/account.js';
 import MkButton from '@/components/MkButton.vue';
 // import { StickySidebar } from '@/utility/sticky-sidebar.js';
 // import { mainRouter } from '@/router.js';
@@ -63,6 +62,8 @@ import { store } from '@/store.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { prefer } from '@/preferences.js';
+import { openAccountMenu as openAccountMenu_ } from '@/accounts.js';
+import { $i } from '@/i.js';
 
 const WINDOW_THRESHOLD = 1400;
 
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 337b0dac94..a5db4031e2 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -100,7 +100,7 @@ import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { deviceKind } from '@/utility/device-kind.js';
 import { prefer } from '@/preferences.js';
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index cb93b20c24..29248b2720 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -108,7 +108,7 @@ import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { i18n } from '@/i18n.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
 import { deviceKind } from '@/utility/device-kind.js';
 import { miLocalStorage } from '@/local-storage.js';
diff --git a/packages/frontend/src/use/use-note-capture.ts b/packages/frontend/src/use/use-note-capture.ts
index 0bc10e90e4..0de2dbb3c5 100644
--- a/packages/frontend/src/use/use-note-capture.ts
+++ b/packages/frontend/src/use/use-note-capture.ts
@@ -7,7 +7,7 @@ import { onUnmounted } from 'vue';
 import type { Ref, ShallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useStream } from '@/stream.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 export function useNoteCapture(props: {
 	rootEl: ShallowRef<HTMLElement | undefined>;
diff --git a/packages/frontend/src/utility/achievements.ts b/packages/frontend/src/utility/achievements.ts
index 3025a985ba..f6ab587ae1 100644
--- a/packages/frontend/src/utility/achievements.ts
+++ b/packages/frontend/src/utility/achievements.ts
@@ -4,7 +4,7 @@
  */
 
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 export const ACHIEVEMENT_TYPES = [
 	'notes1',
diff --git a/packages/frontend/src/utility/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
index ebc67eb58d..e768d8a5ae 100644
--- a/packages/frontend/src/utility/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -814,6 +814,13 @@ export const searchIndexes: SearchIndexItem[] = [
 		path: '/settings/avatar-decoration',
 		icon: 'ti ti-sparkles',
 	},
+	{
+		id: 'zK6posor9',
+		label: i18n.ts.accounts,
+		keywords: ['accounts'],
+		path: '/settings/accounts',
+		icon: 'ti ti-users',
+	},
 	{
 		id: '330Q4mf8E',
 		children: [
diff --git a/packages/frontend/src/utility/check-permissions.ts b/packages/frontend/src/utility/check-permissions.ts
index ed86529d5b..2de8fd2cd1 100644
--- a/packages/frontend/src/utility/check-permissions.ts
+++ b/packages/frontend/src/utility/check-permissions.ts
@@ -4,7 +4,7 @@
  */
 
 import { instance } from '@/instance.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 export const notesSearchAvailable = (
 	// FIXME: instance.policies would be null in Vitest
diff --git a/packages/frontend/src/utility/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts
index 74eb326c4c..d2026de0b6 100644
--- a/packages/frontend/src/utility/get-note-menu.ts
+++ b/packages/frontend/src/utility/get-note-menu.ts
@@ -9,7 +9,7 @@ import { url } from '@@/js/config.js';
 import { claimAchievement } from './achievements.js';
 import type { Ref, ShallowRef } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/utility/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts
index 1b9b0eac95..b89c7537e2 100644
--- a/packages/frontend/src/utility/get-user-menu.ts
+++ b/packages/frontend/src/utility/get-user-menu.ts
@@ -13,7 +13,7 @@ import { i18n } from '@/i18n.js';
 import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import * as os from '@/os.js';
 import { misskeyApi } from '@/utility/misskey-api.js';
-import { $i, iAmModerator } from '@/account.js';
+import { $i, iAmModerator } from '@/i.js';
 import { notesSearchAvailable, canSearchNonLocalNotes } from '@/utility/check-permissions.js';
 import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
 import { mainRouter } from '@/router/main.js';
diff --git a/packages/frontend/src/utility/isFfVisibleForMe.ts b/packages/frontend/src/utility/isFfVisibleForMe.ts
index e28e5725bc..48ef1c4e49 100644
--- a/packages/frontend/src/utility/isFfVisibleForMe.ts
+++ b/packages/frontend/src/utility/isFfVisibleForMe.ts
@@ -4,7 +4,7 @@
  */
 
 import * as Misskey from 'misskey-js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 export function isFollowingVisibleForMe(user: Misskey.entities.UserDetailed): boolean {
 	if ($i && ($i.id === user.id || $i.isAdmin || $i.isModerator)) return true;
diff --git a/packages/frontend/src/utility/misskey-api.ts b/packages/frontend/src/utility/misskey-api.ts
index dc07ad477b..72ba54ade3 100644
--- a/packages/frontend/src/utility/misskey-api.ts
+++ b/packages/frontend/src/utility/misskey-api.ts
@@ -6,7 +6,7 @@
 import * as Misskey from 'misskey-js';
 import { ref } from 'vue';
 import { apiUrl } from '@@/js/config.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 export const pendingApiRequestsCount = ref(0);
 
 export type Endpoint = keyof Misskey.Endpoints;
diff --git a/packages/frontend/src/utility/please-login.ts b/packages/frontend/src/utility/please-login.ts
index a8a330eb6d..9253105f48 100644
--- a/packages/frontend/src/utility/please-login.ts
+++ b/packages/frontend/src/utility/please-login.ts
@@ -4,7 +4,7 @@
  */
 
 import { defineAsyncComponent } from 'vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import { popup } from '@/os.js';
diff --git a/packages/frontend/src/utility/show-moved-dialog.ts b/packages/frontend/src/utility/show-moved-dialog.ts
index 35b3ef79d8..db21b028cd 100644
--- a/packages/frontend/src/utility/show-moved-dialog.ts
+++ b/packages/frontend/src/utility/show-moved-dialog.ts
@@ -4,7 +4,7 @@
  */
 
 import * as os from '@/os.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 
 export function showMovedDialog() {
diff --git a/packages/frontend/src/utility/upload.ts b/packages/frontend/src/utility/upload.ts
index d105a318a7..eb3cbd3dfa 100644
--- a/packages/frontend/src/utility/upload.ts
+++ b/packages/frontend/src/utility/upload.ts
@@ -9,7 +9,7 @@ import { v4 as uuid } from 'uuid';
 import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
 import { apiUrl } from '@@/js/config.js';
 import { getCompressionConfig } from './upload/compress-config.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { alert } from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue
index d911e71ab2..db03d1406c 100644
--- a/packages/frontend/src/widgets/WidgetActivity.vue
+++ b/packages/frontend/src/widgets/WidgetActivity.vue
@@ -28,7 +28,7 @@ import XChart from './WidgetActivity.chart.vue';
 import type { GetFormResultType } from '@/utility/form.js';
 import { misskeyApiGet } from '@/utility/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'activity';
diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index b49041158f..c46fd81466 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -27,7 +27,7 @@ import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'aiscript';
diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
index fb9dea1847..429b0e0ffb 100644
--- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue
+++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
@@ -21,7 +21,7 @@ import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps
 import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
 import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkAsUi from '@/components/MkAsUi.vue';
 import MkContainer from '@/components/MkContainer.vue';
 import { registerAsUiLib } from '@/aiscript/ui.js';
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index 8c7507ef44..be11a26917 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -33,7 +33,7 @@ import MkContainer from '@/components/MkContainer.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 
 const name = i18n.ts._widgets.birthdayFollowings;
 
diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue
index 3f0f9eb9fd..4afe735a22 100644
--- a/packages/frontend/src/widgets/WidgetButton.vue
+++ b/packages/frontend/src/widgets/WidgetButton.vue
@@ -18,7 +18,7 @@ import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps
 import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
 import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import MkButton from '@/components/MkButton.vue';
 
 const name = 'button';
diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue
index c86d1c9653..3fe8378a39 100644
--- a/packages/frontend/src/widgets/WidgetProfile.vue
+++ b/packages/frontend/src/widgets/WidgetProfile.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import type { GetFormResultType } from '@/utility/form.js';
-import { $i } from '@/account.js';
+import { $i } from '@/i.js';
 import { userPage } from '@/filters/user.js';
 
 const name = 'profile';
diff --git a/packages/frontend/test/aiscript/api.test.ts b/packages/frontend/test/aiscript/api.test.ts
index a569c0fa51..36838af163 100644
--- a/packages/frontend/test/aiscript/api.test.ts
+++ b/packages/frontend/test/aiscript/api.test.ts
@@ -33,7 +33,7 @@ async function exe(script: string): Promise<values.Value[]> {
 	return outputs;
 }
 
-let $iMock = vi.hoisted<Partial<typeof import('@/account.js').$i> | null >(
+let $iMock = vi.hoisted<Partial<typeof import('@/i.js').$i> | null >(
 	() => null
 );
 

From 7acd3d1a889a4a6ddef3bf1fb9e1b5a8a3c5adcf Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 13 Mar 2025 22:32:26 +0900
Subject: [PATCH 16/16] fix(frontend): fix test

---
 packages/frontend/test/aiscript/api.test.ts | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/test/aiscript/api.test.ts b/packages/frontend/test/aiscript/api.test.ts
index 36838af163..ad24625b96 100644
--- a/packages/frontend/test/aiscript/api.test.ts
+++ b/packages/frontend/test/aiscript/api.test.ts
@@ -37,7 +37,7 @@ let $iMock = vi.hoisted<Partial<typeof import('@/i.js').$i> | null >(
 	() => null
 );
 
-vi.mock('@/account.js', () => {
+vi.mock('@/i.js', () => {
 	return {
 		get $i() {
 			return $iMock;