diff --git a/CHANGELOG.md b/CHANGELOG.md
index e5df6de4e6..748f3aa8eb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,21 @@
+## 2025.3.2
+
+### General
+-
+
+### Client
+- Feat: 設定の管理が強化されました
+  - 自動でバックアップされるように
+- Enhance: プラグインの管理が強化されました
+- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
+- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
+- Enhance: テーマ設定画面のデザインを改善
+- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
+
+### Server
+- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正
+
+
 ## 2025.3.1
 
 ### General
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 7a11a4fb1c..46a08db2d8 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5282,6 +5282,86 @@ export interface Locale extends ILocale {
      * アクセシビリティ
      */
     "accessibility": string;
+    /**
+     * 設定のプロファイル
+     */
+    "preferencesProfile": string;
+    /**
+     * 設定IDをコピー
+     */
+    "copyPreferenceId": string;
+    /**
+     * 初期値に戻す
+     */
+    "resetToDefaultValue": string;
+    /**
+     * アカウントで上書き
+     */
+    "overrideByAccount": string;
+    /**
+     * 無題
+     */
+    "untitled": string;
+    /**
+     * 名前はありません
+     */
+    "noName": string;
+    /**
+     * スキップ
+     */
+    "skip": string;
+    /**
+     * 復元
+     */
+    "restore": string;
+    "_preferencesProfile": {
+        /**
+         * プロファイル名
+         */
+        "profileName": string;
+        /**
+         * このデバイスを識別する名前を設定してください。
+         */
+        "profileNameDescription": string;
+        /**
+         * 例: 「メインPC」、「スマホ」など
+         */
+        "profileNameDescription2": string;
+    };
+    "_preferencesBackup": {
+        /**
+         * 自動バックアップ
+         */
+        "autoBackup": string;
+        /**
+         * バックアップから復元
+         */
+        "restoreFromBackup": string;
+        /**
+         * バックアップが見つかりませんでした
+         */
+        "noBackupsFoundTitle": string;
+        /**
+         * 自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。
+         */
+        "noBackupsFoundDescription": string;
+        /**
+         * 復元するバックアップを選択してください
+         */
+        "selectBackupToRestore": string;
+        /**
+         * 自動バックアップを有効にするにはプロファイル名の設定が必要です。
+         */
+        "youNeedToNameYourProfileToEnableAutoBackup": string;
+        /**
+         * このデバイスで設定の自動バックアップは有効になっていません。
+         */
+        "autoPreferencesBackupIsNotEnabledForThisDevice": string;
+        /**
+         * 設定のバックアップが見つかりました
+         */
+        "backupFound": string;
+    };
     "_accountSettings": {
         /**
          * コンテンツの表示にログインを必須にする
@@ -5319,6 +5399,10 @@ export interface Locale extends ILocale {
          * リモートサーバーに連合されたノートには効果が及ばない場合があります。
          */
         "mayNotEffectForFederatedNotes": string;
+        /**
+         * これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。
+         */
+        "mayNotEffectSomeSituations": string;
         /**
          * 指定した時間を経過しているノート
          */
@@ -7679,6 +7763,10 @@ export interface Locale extends ILocale {
          * 標準のテーマ
          */
         "builtinThemes": string;
+        /**
+         * サーバーのテーマ
+         */
+        "instanceTheme": string;
         /**
          * そのテーマは既にインストールされています
          */
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 4304b2e44b..e123898a21 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1316,6 +1316,29 @@ markAsSensitiveConfirm: "このメディアをセンシティブとして設定
 unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか?"
 preferences: "環境設定"
 accessibility: "アクセシビリティ"
+preferencesProfile: "設定のプロファイル"
+copyPreferenceId: "設定IDをコピー"
+resetToDefaultValue: "初期値に戻す"
+overrideByAccount: "アカウントで上書き"
+untitled: "無題"
+noName: "名前はありません"
+skip: "スキップ"
+restore: "復元"
+
+_preferencesProfile:
+  profileName: "プロファイル名"
+  profileNameDescription: "このデバイスを識別する名前を設定してください。"
+  profileNameDescription2: "例: 「メインPC」、「スマホ」など"
+
+_preferencesBackup:
+  autoBackup: "自動バックアップ"
+  restoreFromBackup: "バックアップから復元"
+  noBackupsFoundTitle: "バックアップが見つかりませんでした"
+  noBackupsFoundDescription: "自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。"
+  selectBackupToRestore: "復元するバックアップを選択してください"
+  youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効にするにはプロファイル名の設定が必要です。"
+  autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になっていません。"
+  backupFound: "設定のバックアップが見つかりました"
 
 _accountSettings:
   requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
@@ -1327,6 +1350,7 @@ _accountSettings:
   makeNotesHiddenBefore: "過去のノートを非公開化する"
   makeNotesHiddenBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。"
   mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばない場合があります。"
+  mayNotEffectSomeSituations: "これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。"
   notesHavePassedSpecifiedPeriod: "指定した時間を経過しているノート"
   notesOlderThanSpecifiedDateAndTime: "指定した日時より前のノート"
 
@@ -2011,6 +2035,7 @@ _theme:
   installed: "{name}をインストールしました"
   installedThemes: "インストールされたテーマ"
   builtinThemes: "標準のテーマ"
+  instanceTheme: "サーバーのテーマ"
   alreadyInstalled: "そのテーマは既にインストールされています"
   invalid: "テーマの形式が間違っています"
   make: "テーマを作る"
diff --git a/package.json b/package.json
index 50370aef2e..18a2a9cf14 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "2025.3.1-pari.2",
+	"version": "2025.3.2-pari.0",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",
@@ -65,12 +65,12 @@
 	},
 	"devDependencies": {
 		"@misskey-dev/eslint-plugin": "2.1.0",
-		"@types/node": "22.13.9",
+		"@types/node": "22.13.10",
 		"@typescript-eslint/eslint-plugin": "8.26.0",
 		"@typescript-eslint/parser": "8.26.0",
 		"cross-env": "7.0.3",
 		"cypress": "14.1.0",
-		"eslint": "9.21.0",
+		"eslint": "9.22.0",
 		"globals": "16.0.0",
 		"ncp": "2.0.0",
 		"pnpm": "10.6.1",
diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 45f7ec8d40..3f2407e9da 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -502,11 +502,28 @@ export class ApRendererService {
 			this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
 		]);
 
+		const tryRewriteUrl = (maybeUrl: string) => {
+			const urlSafeRegex = /^(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/;
+			try {
+				const match = maybeUrl.match(urlSafeRegex);
+				if (!match) {
+					return maybeUrl;
+				}
+				const urlPart = match[0];
+				const urlPartParsed = new URL(urlPart);
+				const restPart = maybeUrl.slice(match[0].length);
+				
+				return `<a href="${urlPartParsed.href}" rel="me nofollow noopener" target="_blank">${urlPart}</a>${restPart}`;
+			} catch (e) {
+				return maybeUrl;
+			}
+		};
+
 		const attachment = profile.fields.map(field => ({
 			type: 'PropertyValue',
 			name: field.name,
 			value: (field.value.startsWith('http://') || field.value.startsWith('https://'))
-				? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
+				? tryRewriteUrl(field.value)
 				: field.value,
 		}));
 
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index d000a28232..c47b48bd7f 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -21,7 +21,7 @@ let moduleInitialized = false;
 let unobserve = () => {};
 let misskeyOS = null;
 
-function loadTheme(applyTheme: typeof import('../src/scripts/theme')['applyTheme']) {
+function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
 	unobserve();
 	const theme = themes[document.documentElement.dataset.misskeyTheme];
 	if (theme) {
@@ -67,10 +67,10 @@ queueMicrotask(() => {
 		import('../src/components'),
 		import('../src/directives'),
 		import('../src/widgets'),
-		import('../src/scripts/theme'),
-		import('../src/store'),
+		import('../src/theme'),
+		import('../src/preferences'),
 		import('../src/os'),
-	]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { defaultStore }, os]) => {
+	]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { prefer }, os]) => {
 		setup((app) => {
 			moduleInitialized = true;
 			if (app[appInitialized]) {
@@ -83,7 +83,7 @@ queueMicrotask(() => {
 			widgets(app);
 			misskeyOS = os;
 			if (isChromatic()) {
-				defaultStore.set('animation', false);
+				prefer.set('animation', false);
 			}
 		});
 	});
@@ -104,9 +104,9 @@ const preview = {
 							}
 						}).catch(() => {})
 					: Promise.resolve();
-				const resetDefaultStorePromise = import('../src/store').then(({ defaultStore }) => {
+				const resetDefaultStorePromise = import('../src/store').then(({ store }) => {
 					// @ts-expect-error
-					defaultStore.init();
+					store.init();
 				}).catch(() => {});
 				Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => {
 					initLocalStorage();
diff --git a/packages/frontend/@types/theme.d.ts b/packages/frontend/@types/theme.d.ts
index 70afc356c1..6ac1037493 100644
--- a/packages/frontend/@types/theme.d.ts
+++ b/packages/frontend/@types/theme.d.ts
@@ -4,7 +4,7 @@
  */
 
 declare module '@@/themes/*.json5' {
-	import { Theme } from '@/scripts/theme.js';
+	import { Theme } from '@/theme.js';
 
 	const theme: Theme;
 
diff --git a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
index 5d8cf05fff..ccfa08575b 100644
--- a/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
+++ b/packages/frontend/lib/rollup-plugin-unwind-css-module-class-name.test.ts
@@ -58,7 +58,7 @@ describe(normalizeClass.name, () => {
 
 it('Composition API (standard)', () => {
 	const ast = parse(`
-import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
+import { c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
 import { M as MkContainer } from './MkContainer-!~{03M}~.js';
 import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js';
 import './photoswipe-!~{003}~.js';
@@ -74,7 +74,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
     let fetching = ref(true);
     let images = ref([]);
     function thumbnail(image) {
-      return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
+      return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
     }
     onMounted(() => {
       const image = [
@@ -173,7 +173,7 @@ export { index_photos as default };
 `.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
 	unwindCssModuleClassName(ast);
 	expect(generate(ast)).toBe(`
-import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
+import {c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
 import {M as MkContainer} from './MkContainer-!~{03M}~.js';
 import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js';
 import './photoswipe-!~{003}~.js';
@@ -190,7 +190,7 @@ const index_photos = defineComponent({
     let fetching = ref(true);
     let images = ref([]);
     function thumbnail(image) {
-      return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
+      return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
     }
     onMounted(() => {
       const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];
@@ -268,7 +268,7 @@ export {index_photos as default};
 it('Composition API (with `useCssModule()`)', () => {
 	const ast = parse(`
 import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js';
-import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
+import { d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
 
 function isDebuggerEnabled(id) {
   try {
@@ -393,7 +393,7 @@ const _sfc_main = defineComponent({
       el.style.left = "";
     }
     return () => h(
-      defaultStore.state.animation ? TransitionGroup : "div",
+      prefer.s.animation ? TransitionGroup : "div",
       {
         class: {
           [$style["date-separated-list"]]: true,
@@ -402,7 +402,7 @@ const _sfc_main = defineComponent({
           [$style["direction-down"]]: props.direction === "down",
           [$style["direction-up"]]: props.direction === "up"
         },
-        ...defaultStore.state.animation ? {
+        ...prefer.s.animation ? {
           name: "list",
           tag: "div",
           onBeforeLeave,
@@ -441,7 +441,7 @@ export { MkDateSeparatedList as M };
 	unwindCssModuleClassName(ast);
 	expect(generate(ast)).toBe(`
 import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js';
-import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
+import {d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
 function isDebuggerEnabled(id) {
   try {
     return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
@@ -555,7 +555,7 @@ const _sfc_main = defineComponent({
       el.style.top = "";
       el.style.left = "";
     }
-    return () => h(defaultStore.state.animation ? TransitionGroup : "div", {
+    return () => h(prefer.s.animation ? TransitionGroup : "div", {
       class: {
         [$style["date-separated-list"]]: true,
         [$style["date-separated-list-nogap"]]: props.noGap,
@@ -563,7 +563,7 @@ const _sfc_main = defineComponent({
         [$style["direction-down"]]: props.direction === "down",
         [$style["direction-up"]]: props.direction === "up"
       },
-      ...defaultStore.state.animation ? {
+      ...prefer.s.animation ? {
         name: "list",
         tag: "div",
         onBeforeLeave,
diff --git a/packages/frontend/lib/vite-plugin-create-search-index.ts b/packages/frontend/lib/vite-plugin-create-search-index.ts
index 509eb804cb..e194872640 100644
--- a/packages/frontend/lib/vite-plugin-create-search-index.ts
+++ b/packages/frontend/lib/vite-plugin-create-search-index.ts
@@ -1213,22 +1213,37 @@ async function processVueFile(
 	transformedCodeCache: Record<string, string>
 }> {
 	const normalizedId = id.replace(/\\/g, '/'); // ファイルパスを正規化
-	// すでにキャッシュに存在する場合は、そのまま返す
-	if (transformedCodeCache[normalizedId] && transformedCodeCache[normalizedId].includes('markerId=')) {
+
+	// 開発モード時はコード内容に変更があれば常に再処理する
+	// コード内容が同じ場合のみキャッシュを使用
+	const isDevMode = process.env.NODE_ENV === 'development';
+
+	const s = new MagicString(code); // magic-string のインスタンスを作成
+
+	if (!isDevMode && transformedCodeCache[normalizedId] && transformedCodeCache[normalizedId].includes('markerId=')) {
 		logger.info(`Using cached version for ${id}`);
 		return {
 			code: transformedCodeCache[normalizedId],
-			map: null,
+			map: s.generateMap({ source: id, includeContent: true }),
+			transformedCodeCache
+		};
+	}
+
+	// すでに処理済みのファイルでコードに変更がない場合はキャッシュを返す
+	if (transformedCodeCache[normalizedId] === code) {
+		logger.info(`Code unchanged for ${id}, using cached version`);
+		return {
+			code: transformedCodeCache[normalizedId],
+			map: s.generateMap({ source: id, includeContent: true }),
 			transformedCodeCache
 		};
 	}
 
-	const s = new MagicString(code); // magic-string のインスタンスを作成
 	const parsed = vueSfcParse(code, { filename: id });
 	if (!parsed.descriptor.template) {
 		return {
 			code,
-			map: null,
+			map: s.generateMap({ source: id, includeContent: true }),
 			transformedCodeCache
 		};
 	}
@@ -1466,16 +1481,21 @@ export default function pluginCreateSearchIndex(options: Options): Plugin {
 				if (isMatch) break; // いずれかのパターンでマッチしたら、outer loop も抜ける
 			}
 
-
 			if (!isMatch) {
 				return;
 			}
 
+			// ファイルの内容が変更された場合は再処理を行う
+			const normalizedId = id.replace(/\\/g, '/');
+			const hasContentChanged = !transformedCodeCache[normalizedId] || transformedCodeCache[normalizedId] !== code;
+
 			const transformed = await processVueFile(code, id, options, transformedCodeCache);
 			transformedCodeCache = transformed.transformedCodeCache; // キャッシュを更新
-			if (isDevServer) {
-				await analyzeVueProps({ ...options, transformedCodeCache }); // analyzeVueProps を呼び出す
+
+			if (isDevServer && hasContentChanged) {
+				await analyzeVueProps({ ...options, transformedCodeCache }); // ファイルが変更されたときのみ分析を実行
 			}
+
 			return transformed;
 		},
 
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index ae723101fe..c9eaa48d29 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -76,7 +76,8 @@
 		"v-code-diff": "1.13.1",
 		"vite": "6.2.1",
 		"vue": "3.5.13",
-		"vuedraggable": "next"
+		"vuedraggable": "next",
+		"wanakana": "5.3.1"
 	},
 	"devDependencies": {
 		"@misskey-dev/summaly": "5.2.0",
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index 1bc759d385..7863f0a874 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -8,13 +8,13 @@ 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 '@/scripts/show-suspended-dialog.js';
+import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { del, get, set } from '@/scripts/idb-proxy.js';
+import { del, get, set } from '@/utility/idb-proxy.js';
 import { waiting, popup, popupMenu, success, alert } from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
 
 // TODO: 他のタブと永続化されたstateを同期
 
diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/aiscript/api.ts
similarity index 98%
rename from packages/frontend/src/scripts/aiscript/api.ts
rename to packages/frontend/src/aiscript/api.ts
index 2c0c8c816e..3acc1127c9 100644
--- a/packages/frontend/src/scripts/aiscript/api.ts
+++ b/packages/frontend/src/aiscript/api.ts
@@ -8,7 +8,7 @@ import * as Misskey from 'misskey-js';
 import { url, lang } from '@@/js/config.js';
 import { assertStringAndIsIn } from './common.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { $i } from '@/account.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { customEmojis } from '@/custom-emojis.js';
diff --git a/packages/frontend/src/scripts/aiscript/common.ts b/packages/frontend/src/aiscript/common.ts
similarity index 100%
rename from packages/frontend/src/scripts/aiscript/common.ts
rename to packages/frontend/src/aiscript/common.ts
diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/aiscript/ui.ts
similarity index 100%
rename from packages/frontend/src/scripts/aiscript/ui.ts
rename to packages/frontend/src/aiscript/ui.ts
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index fd655acb8e..6a7b92a01c 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -6,20 +6,22 @@
 import { computed, watch, version as vueVersion } from 'vue';
 import { compareVersions } from 'compare-versions';
 import { version, lang, updateLocale, locale } from '@@/js/config.js';
+import defaultLightTheme from '@@/themes/l-light.json5';
+import defaultDarkTheme from '@@/themes/d-green-lime.json5';
 import type { App } from 'vue';
 import widgets from '@/widgets/index.js';
 import directives from '@/directives/index.js';
 import components from '@/components/index.js';
-import { applyTheme } from '@/scripts/theme.js';
-import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.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 { defaultStore, ColdDeviceStorage } from '@/store.js';
+import { store } from '@/store.js';
 import { fetchInstance, instance } from '@/instance.js';
-import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js';
-import { reloadChannel } from '@/scripts/unison-reload.js';
-import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
-import { getAccountFromId } from '@/scripts/get-account-from-id.js';
+import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js';
+import { reloadChannel } from '@/utility/unison-reload.js';
+import { getUrlWithoutLoginId } from '@/utility/login-id.js';
+import { getAccountFromId } from '@/utility/get-account-from-id.js';
 import { deckStore } from '@/ui/deck/deck-store.js';
 import { analytics, initAnalytics } from '@/analytics.js';
 import { miLocalStorage } from '@/local-storage.js';
@@ -27,6 +29,7 @@ import { fetchCustomEmojis } from '@/custom-emojis.js';
 import { setupRouter } from '@/router/main.js';
 import { createMainRouter } from '@/router/definition.js';
 import { loadFontStyle } from '@/scripts/load-font.js';
+import { prefer } from '@/preferences.js';
 
 export async function common(createVue: () => App<Element>) {
 	console.info(`Misskey v${version}`);
@@ -39,7 +42,7 @@ export async function common(createVue: () => App<Element>) {
 		// 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 = defaultStore;
+		(window as any).$store = store;
 
 		window.addEventListener('error', event => {
 			console.error(event);
@@ -124,7 +127,7 @@ export async function common(createVue: () => App<Element>) {
 	html.setAttribute('lang', lang);
 	//#endregion
 
-	await defaultStore.ready;
+	await store.ready;
 	await deckStore.ready;
 
 	const fetchInstanceMetaPromise = fetchInstance();
@@ -152,56 +155,63 @@ export async function common(createVue: () => App<Element>) {
 	//#endregion
 
 	// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
-	watch(defaultStore.reactiveState.darkMode, (darkMode) => {
-		applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
+	watch(store.r.darkMode, (darkMode) => {
+		applyTheme(darkMode
+			? (prefer.s.darkTheme ?? defaultDarkTheme)
+			: (prefer.s.lightTheme ?? defaultLightTheme),
+		);
 	}, { immediate: miLocalStorage.getItem('theme') == null });
 
-	document.documentElement.dataset.colorScheme = defaultStore.state.darkMode ? 'dark' : 'light';
+	document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
 
-	const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
-	const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
+	const darkTheme = prefer.model('darkTheme');
+	const lightTheme = prefer.model('lightTheme');
 
 	watch(darkTheme, (theme) => {
-		if (defaultStore.state.darkMode) {
-			applyTheme(theme);
+		if (store.s.darkMode) {
+			applyTheme(theme ?? defaultDarkTheme);
 		}
 	});
 
 	watch(lightTheme, (theme) => {
-		if (!defaultStore.state.darkMode) {
-			applyTheme(theme);
+		if (!store.s.darkMode) {
+			applyTheme(theme ?? defaultLightTheme);
 		}
 	});
 
 	//#region Sync dark mode
-	if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
-		defaultStore.set('darkMode', isDeviceDarkmode());
+	if (prefer.s.syncDeviceDarkMode) {
+		store.set('darkMode', isDeviceDarkmode());
 	}
 
 	window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
-		if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
-			defaultStore.set('darkMode', mql.matches);
+		if (prefer.s.syncDeviceDarkMode) {
+			store.set('darkMode', mql.matches);
 		}
 	});
 	//#endregion
 
+	if (prefer.s.darkTheme && store.s.darkMode) {
+		if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
+	} else if (prefer.s.lightTheme && !store.s.darkMode) {
+		if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
+	}
+
 	fetchInstanceMetaPromise.then(() => {
-		if (defaultStore.state.themeInitial) {
-			if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
-			if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
-			defaultStore.set('themeInitial', false);
-		}
+		// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
+		if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
+		if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
 	});
 
-	watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => {
+	watch(prefer.r.overridedDeviceKind, (kind) => {
 		updateDeviceKind(kind);
 	}, { immediate: true });
 
-	watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
+	watch(prefer.r.useBlurEffectForModal, v => {
 		document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
 	}, { immediate: true });
 
-	watch(defaultStore.reactiveState.useBlurEffect, v => {
+	watch(prefer.r.useBlurEffect, v => {
 		if (v) {
 			document.documentElement.style.removeProperty('--MI-blur');
 		} else {
@@ -215,7 +225,7 @@ export async function common(createVue: () => App<Element>) {
 			navigator.wakeLock.request('screen');
 		}
 	});
-	if (defaultStore.state.keepScreenOn && 'wakeLock' in navigator) {
+	if (prefer.s.keepScreenOn && 'wakeLock' in navigator) {
 		navigator.wakeLock.request('screen')
 			.then(onVisibilityChange)
 			.catch(() => {
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 0967191585..e2ed5b3eea 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -8,23 +8,26 @@ import { ui } from '@@/js/config.js';
 import * as Misskey from 'misskey-js';
 import { common } from './common.js';
 import type { Component } from 'vue';
-import type { Keymap } from '@/scripts/hotkey.js';
+import type { Keymap } from '@/utility/hotkey.js';
 import { i18n } from '@/i18n.js';
 import { alert, confirm, popup, post, toast } from '@/os.js';
 import { useStream } from '@/stream.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 import { $i, signout, updateAccountPartial } from '@/account.js';
 import { instance } from '@/instance.js';
-import { ColdDeviceStorage, defaultStore } from '@/store.js';
-import { reactionPicker } from '@/scripts/reaction-picker.js';
+import { ColdDeviceStorage, store } from '@/store.js';
+import { reactionPicker } from '@/utility/reaction-picker.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
-import { initializeSw } from '@/scripts/initialize-sw.js';
-import { deckStore } from '@/ui/deck/deck-store.js';
-import { emojiPicker } from '@/scripts/emoji-picker.js';
+import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
+import { initializeSw } from '@/utility/initialize-sw.js';
+import { emojiPicker } from '@/utility/emoji-picker.js';
 import { mainRouter } from '@/router/main.js';
-import { makeHotkey } from '@/scripts/hotkey.js';
+import { makeHotkey } from '@/utility/hotkey.js';
 import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
+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';
 
 export async function mainBoot() {
 	const { isClientUpdated } = await common(() => {
@@ -34,7 +37,7 @@ export async function mainBoot() {
 		if (!$i) uiStyle = 'visitor';
 
 		if (searchParams.has('zen')) uiStyle = 'zen';
-		if (uiStyle === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') uiStyle = 'zen';
+		if (uiStyle === 'deck' && prefer.s['deck.useSimpleUiForNonRootPages'] && location.pathname !== '/') uiStyle = 'zen';
 
 		if (searchParams.has('ui')) uiStyle = searchParams.get('ui');
 
@@ -73,9 +76,9 @@ export async function mainBoot() {
 
 	let reloadDialogShowing = false;
 	stream.on('_disconnected_', async () => {
-		if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
+		if (prefer.s.serverDisconnectedBehavior === 'reload') {
 			location.reload();
-		} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
+		} else if (prefer.s.serverDisconnectedBehavior === 'dialog') {
 			if (reloadDialogShowing) return;
 			reloadDialogShowing = true;
 			const { canceled } = await confirm({
@@ -102,30 +105,24 @@ export async function mainBoot() {
 		removeCustomEmojis(emojiData.emojis);
 	});
 
-	for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
-		import('@/plugin.js').then(async ({ install }) => {
-			// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
-			await new Promise(r => setTimeout(r, 0));
-			install(plugin);
-		});
-	}
+	launchPlugins();
 
 	try {
-		if (defaultStore.state.enableSeasonalScreenEffect) {
+		if (prefer.s.enableSeasonalScreenEffect) {
 			const month = new Date().getMonth() + 1;
-			if (defaultStore.state.hemisphere === 'S') {
+			if (prefer.s.hemisphere === 'S') {
 				// ▼南半球
 				if (month === 7 || month === 8) {
-					const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
+					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
 					new SnowfallEffect({}).render();
 				}
 			} else {
 				// ▼北半球
 				if (month === 12 || month === 1) {
-					const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
+					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
 					new SnowfallEffect({}).render();
 				} else if (month === 3 || month === 4) {
-					const SakuraEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
+					const SakuraEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
 					new SakuraEffect({
 						sakura: true,
 					}).render();
@@ -138,8 +135,101 @@ export async function mainBoot() {
 	}
 
 	if ($i) {
-		defaultStore.loaded.then(() => {
-			if (defaultStore.state.accountSetupWizard !== -1) {
+		store.loaded.then(async () => {
+			// prefereces migration
+			// TODO: そのうち消す
+			if (store.s.menu.length > 0) {
+				const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []);
+				if (themes.length > 0) {
+					prefer.commit('themes', themes);
+				}
+				const plugins = ColdDeviceStorage.get('plugins');
+				prefer.commit('plugins', plugins.map(p => ({
+					...p,
+					installId: (p as any).id,
+					id: undefined,
+				})));
+				prefer.commit('lightTheme', ColdDeviceStorage.get('lightTheme'));
+				prefer.commit('darkTheme', ColdDeviceStorage.get('darkTheme'));
+				prefer.commit('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
+				prefer.commit('overridedDeviceKind', store.s.overridedDeviceKind);
+				prefer.commit('widgets', store.s.widgets);
+				prefer.commit('keepCw', store.s.keepCw);
+				prefer.commit('collapseRenotes', store.s.collapseRenotes);
+				prefer.commit('rememberNoteVisibility', store.s.rememberNoteVisibility);
+				prefer.commit('uploadFolder', store.s.uploadFolder);
+				prefer.commit('keepOriginalUploading', store.s.keepOriginalUploading);
+				prefer.commit('menu', store.s.menu);
+				prefer.commit('statusbars', store.s.statusbars);
+				prefer.commit('pinnedUserLists', store.s.pinnedUserLists);
+				prefer.commit('serverDisconnectedBehavior', store.s.serverDisconnectedBehavior);
+				prefer.commit('nsfw', store.s.nsfw);
+				prefer.commit('highlightSensitiveMedia', store.s.highlightSensitiveMedia);
+				prefer.commit('animation', store.s.animation);
+				prefer.commit('animatedMfm', store.s.animatedMfm);
+				prefer.commit('advancedMfm', store.s.advancedMfm);
+				prefer.commit('showReactionsCount', store.s.showReactionsCount);
+				prefer.commit('enableQuickAddMfmFunction', store.s.enableQuickAddMfmFunction);
+				prefer.commit('loadRawImages', store.s.loadRawImages);
+				prefer.commit('imageNewTab', store.s.imageNewTab);
+				prefer.commit('disableShowingAnimatedImages', store.s.disableShowingAnimatedImages);
+				prefer.commit('emojiStyle', store.s.emojiStyle);
+				prefer.commit('menuStyle', store.s.menuStyle);
+				prefer.commit('useBlurEffectForModal', store.s.useBlurEffectForModal);
+				prefer.commit('useBlurEffect', store.s.useBlurEffect);
+				prefer.commit('showFixedPostForm', store.s.showFixedPostForm);
+				prefer.commit('showFixedPostFormInChannel', store.s.showFixedPostFormInChannel);
+				prefer.commit('enableInfiniteScroll', store.s.enableInfiniteScroll);
+				prefer.commit('useReactionPickerForContextMenu', store.s.useReactionPickerForContextMenu);
+				prefer.commit('showGapBetweenNotesInTimeline', store.s.showGapBetweenNotesInTimeline);
+				prefer.commit('instanceTicker', store.s.instanceTicker);
+				prefer.commit('emojiPickerScale', store.s.emojiPickerScale);
+				prefer.commit('emojiPickerWidth', store.s.emojiPickerWidth);
+				prefer.commit('emojiPickerHeight', store.s.emojiPickerHeight);
+				prefer.commit('emojiPickerStyle', store.s.emojiPickerStyle);
+				prefer.commit('reportError', store.s.reportError);
+				prefer.commit('squareAvatars', store.s.squareAvatars);
+				prefer.commit('showAvatarDecorations', store.s.showAvatarDecorations);
+				prefer.commit('numberOfPageCache', store.s.numberOfPageCache);
+				prefer.commit('showNoteActionsOnlyHover', store.s.showNoteActionsOnlyHover);
+				prefer.commit('showClipButtonInNoteFooter', store.s.showClipButtonInNoteFooter);
+				prefer.commit('reactionsDisplaySize', store.s.reactionsDisplaySize);
+				prefer.commit('limitWidthOfReaction', store.s.limitWidthOfReaction);
+				prefer.commit('forceShowAds', store.s.forceShowAds);
+				prefer.commit('aiChanMode', store.s.aiChanMode);
+				prefer.commit('devMode', store.s.devMode);
+				prefer.commit('mediaListWithOneImageAppearance', store.s.mediaListWithOneImageAppearance);
+				prefer.commit('notificationPosition', store.s.notificationPosition);
+				prefer.commit('notificationStackAxis', store.s.notificationStackAxis);
+				prefer.commit('enableCondensedLine', store.s.enableCondensedLine);
+				prefer.commit('keepScreenOn', store.s.keepScreenOn);
+				prefer.commit('disableStreamingTimeline', store.s.disableStreamingTimeline);
+				prefer.commit('useGroupedNotifications', store.s.useGroupedNotifications);
+				prefer.commit('dataSaver', store.s.dataSaver);
+				prefer.commit('enableSeasonalScreenEffect', store.s.enableSeasonalScreenEffect);
+				prefer.commit('enableHorizontalSwipe', store.s.enableHorizontalSwipe);
+				prefer.commit('useNativeUiForVideoAudioPlayer', store.s.useNativeUIForVideoAudioPlayer);
+				prefer.commit('keepOriginalFilename', store.s.keepOriginalFilename);
+				prefer.commit('alwaysConfirmFollow', store.s.alwaysConfirmFollow);
+				prefer.commit('confirmWhenRevealingSensitiveMedia', store.s.confirmWhenRevealingSensitiveMedia);
+				prefer.commit('contextMenu', store.s.contextMenu);
+				prefer.commit('skipNoteRender', store.s.skipNoteRender);
+				prefer.commit('showSoftWordMutedWord', store.s.showSoftWordMutedWord);
+				prefer.commit('confirmOnReact', store.s.confirmOnReact);
+				prefer.commit('sound.masterVolume', store.s.sound_masterVolume);
+				prefer.commit('sound.notUseSound', store.s.sound_notUseSound);
+				prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive);
+				prefer.commit('sound.on.note', store.s.sound_note as any);
+				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('deck.profile', deckStore.s.profile);
+				store.set('deck.columns', deckStore.s.columns);
+				store.set('deck.layout', deckStore.s.layout);
+				store.set('menu', []);
+			}
+
+			if (store.s.accountSetupWizard !== -1) {
 				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
 					closed: () => dispose(),
 				});
@@ -154,7 +244,7 @@ export async function mainBoot() {
 			});
 		}
 
-		function onAnnouncementCreated (ev: { announcement: Misskey.entities.Announcement }) {
+		function onAnnouncementCreated(ev: { announcement: Misskey.entities.Announcement }) {
 			const announcement = ev.announcement;
 			if (announcement.display === 'dialog') {
 				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
@@ -412,7 +502,7 @@ export async function mainBoot() {
 			post();
 		},
 		'd': () => {
-			defaultStore.set('darkMode', !defaultStore.state.darkMode);
+			store.set('darkMode', !store.s.darkMode);
 		},
 		's': () => {
 			mainRouter.push('/search');
diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts
index 35c84d5568..e24c324dfb 100644
--- a/packages/frontend/src/boot/sub-boot.ts
+++ b/packages/frontend/src/boot/sub-boot.ts
@@ -5,7 +5,7 @@
 
 import { createApp, defineAsyncComponent } from 'vue';
 import { common } from './common.js';
-import { emojiPicker } from '@/scripts/emoji-picker.js';
+import { emojiPicker } from '@/utility/emoji-picker.js';
 
 export async function subBoot() {
 	const { isClientUpdated } = await common(() => createApp(
diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts
index bfe8fbe0e4..70078b410d 100644
--- a/packages/frontend/src/cache.ts
+++ b/packages/frontend/src/cache.ts
@@ -4,8 +4,8 @@
  */
 
 import * as Misskey from 'misskey-js';
-import { Cache } from '@/scripts/cache.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { Cache } from '@/utility/cache.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list'));
 export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list'));
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index e48b6ef781..8e37317779 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -90,7 +90,7 @@ import MkFolder from '@/components/MkFolder.vue';
 import RouterView from '@/components/global/RouterView.vue';
 import { useRouterFactory } from '@/router/supplier';
 import MkTextarea from '@/components/MkTextarea.vue';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 
 const props = defineProps<{
 	report: Misskey.entities.AdminAbuseUserReportsResponse[number];
diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue
index 0839955d9d..cb8032c019 100644
--- a/packages/frontend/src/components/MkAccountMoved.vue
+++ b/packages/frontend/src/components/MkAccountMoved.vue
@@ -17,7 +17,7 @@ import * as Misskey from 'misskey-js';
 import MkMention from './MkMention.vue';
 import { i18n } from '@/i18n.js';
 import { host as localHost } from '@@/js/config.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const user = ref<Misskey.entities.UserLite>();
 
diff --git a/packages/frontend/src/components/MkAchievements.stories.impl.ts b/packages/frontend/src/components/MkAchievements.stories.impl.ts
index bbd3f69d7c..d838997616 100644
--- a/packages/frontend/src/components/MkAchievements.stories.impl.ts
+++ b/packages/frontend/src/components/MkAchievements.stories.impl.ts
@@ -9,7 +9,7 @@ import { HttpResponse, http } from 'msw';
 import { userDetailed } from '../../.storybook/fakes.js';
 import { commonHandlers } from '../../.storybook/mocks.js';
 import MkAchievements from './MkAchievements.vue';
-import { ACHIEVEMENT_TYPES } from '@/scripts/achievements.js';
+import { ACHIEVEMENT_TYPES } from '@/utility/achievements.js';
 export const Empty = {
 	render(args) {
 		return {
diff --git a/packages/frontend/src/components/MkAchievements.vue b/packages/frontend/src/components/MkAchievements.vue
index c8134416b5..70766634ce 100644
--- a/packages/frontend/src/components/MkAchievements.vue
+++ b/packages/frontend/src/components/MkAchievements.vue
@@ -55,9 +55,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { onMounted, ref, computed } from 'vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js';
+import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/utility/achievements.js';
 
 const props = withDefaults(defineProps<{
 	user: Misskey.entities.User;
diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue
index c8fa6246e0..b39bca5b27 100644
--- a/packages/frontend/src/components/MkAnalogClock.vue
+++ b/packages/frontend/src/components/MkAnalogClock.vue
@@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
 import tinycolor from 'tinycolor2';
 import { globalEvents } from '@/events.js';
-import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
+import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js';
 
 // https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
 const angleDiff = (a: number, b: number) => {
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index 3045a47585..41fd2564d8 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkModal from '@/components/MkModal.vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue
index e622d57f1e..ac71618ee2 100644
--- a/packages/frontend/src/components/MkAntennaEditor.vue
+++ b/packages/frontend/src/components/MkAntennaEditor.vue
@@ -59,10 +59,10 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { deepMerge } from '@/scripts/merge.js';
-import type { DeepPartial } from '@/scripts/merge.js';
+import { deepMerge } from '@/utility/merge.js';
+import type { DeepPartial } from '@/utility/merge.js';
 
 type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
 	id?: string;
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index 5c4d887e0c..20a953c72c 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -71,7 +71,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkSelect from '@/components/MkSelect.vue';
-import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/scripts/aiscript/ui.js';
+import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
 
diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue
index f78d2d38f0..090c31044e 100644
--- a/packages/frontend/src/components/MkAuthConfirm.vue
+++ b/packages/frontend/src/components/MkAuthConfirm.vue
@@ -123,8 +123,8 @@ import MkButton from '@/components/MkButton.vue';
 import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { getProxiedImageUrl } from '@/utility/media-proxy.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const props = defineProps<{
 	name?: string;
diff --git a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts
index af5dd4784d..64ccb708aa 100644
--- a/packages/frontend/src/components/MkAutocomplete.stories.impl.ts
+++ b/packages/frontend/src/components/MkAutocomplete.stories.impl.ts
@@ -12,7 +12,7 @@ import { userDetailed } from '../../.storybook/fakes.js';
 import { commonHandlers } from '../../.storybook/mocks.js';
 import MkAutocomplete from './MkAutocomplete.vue';
 import MkInput from './MkInput.vue';
-import { tick } from '@/scripts/test-utils.js';
+import { tick } from '@/utility/test-utils.js';
 const common = {
 	render(args) {
 		return {
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 33495c8af6..7436d4f1b2 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -49,22 +49,23 @@ import sanitizeHtml from 'sanitize-html';
 import { emojilist, getEmojiName } from '@@/js/emojilist.js';
 import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
 import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
-import type { EmojiDef } from '@/scripts/search-emoji.js';
-import contains from '@/scripts/contains.js';
+import type { EmojiDef } from '@/utility/search-emoji.js';
+import contains from '@/utility/contains.js';
 import { acct } from '@/filters/user.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { customEmojis } from '@/custom-emojis.js';
-import { searchEmoji } from '@/scripts/search-emoji.js';
+import { searchEmoji } from '@/utility/search-emoji.js';
+import { prefer } from '@/preferences.js';
 
 const lib = emojilist.filter(x => x.category !== 'flags');
 
 const emojiDb = computed(() => {
 	//#region Unicode Emoji
-	const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
+	const char2path = prefer.r.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 
 	const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({
 		emoji: x.char,
@@ -72,7 +73,7 @@ const emojiDb = computed(() => {
 		url: char2path(x.char),
 	}));
 
-	for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+	for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
 		for (const [emoji, keywords] of Object.entries(index)) {
 			for (const k of keywords) {
 				unicodeEmojiDB.push({
@@ -154,10 +155,10 @@ function complete(type: string, value: any) {
 	emit('done', { type, value });
 	emit('closed');
 	if (type === 'emoji') {
-		let recents = defaultStore.state.recentlyUsedEmojis;
+		let recents = store.s.recentlyUsedEmojis;
 		recents = recents.filter((emoji: any) => emoji !== value);
 		recents.unshift(value);
-		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
+		store.set('recentlyUsedEmojis', recents.splice(0, 32));
 	}
 }
 
@@ -237,7 +238,7 @@ function exec() {
 	} else if (props.type === 'emoji') {
 		if (!props.q || props.q === '') {
 			// 最近使った絵文字をサジェスト
-			emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
+			emojis.value = store.s.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
 			return;
 		}
 
diff --git a/packages/frontend/src/components/MkAvatars.vue b/packages/frontend/src/components/MkAvatars.vue
index 8236d0ddb9..1c44ed60d8 100644
--- a/packages/frontend/src/components/MkAvatars.vue
+++ b/packages/frontend/src/components/MkAvatars.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const props = withDefaults(defineProps<{
 	userIds: string[];
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index 134f8226d4..30eafb7a5b 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 // APIs provided by Captcha services
 // see: https://docs.hcaptcha.com/configuration/#javascript-api
@@ -154,7 +154,7 @@ async function requestRender() {
 
 		captchaWidgetId.value = captcha.value.render(elem, {
 			sitekey: props.sitekey,
-			theme: defaultStore.state.darkMode ? 'dark' : 'light',
+			theme: store.s.darkMode ? 'dark' : 'light',
 			callback: callback,
 			'expired-callback': () => callback(undefined),
 			'error-callback': () => callback(undefined),
diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue
index d4e4f6179a..2ef2d84198 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.vue
+++ b/packages/frontend/src/components/MkChannelFollowButton.vue
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index d05f4921f6..c1a55906ae 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -53,15 +53,15 @@ export type ChartSrc =
 import { onMounted, ref, shallowRef, watch } from 'vue';
 import { Chart } from 'chart.js';
 import * as Misskey from 'misskey-js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { alpha } from '@/scripts/color.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { alpha } from '@/utility/color.js';
 import date from '@/filters/date.js';
 import bytes from '@/filters/bytes.js';
-import { initChart } from '@/scripts/init-chart.js';
-import { chartLegend } from '@/scripts/chart-legend.js';
+import { initChart } from '@/utility/init-chart.js';
+import { chartLegend } from '@/utility/chart-legend.js';
 import MkChartLegend from '@/components/MkChartLegend.vue';
 
 initChart();
@@ -161,7 +161,7 @@ const render = () => {
 		chartInstance.destroy();
 	}
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y)));
 
diff --git a/packages/frontend/src/components/MkClickerGame.vue b/packages/frontend/src/components/MkClickerGame.vue
index 9a0a9fba05..775964af50 100644
--- a/packages/frontend/src/components/MkClickerGame.vue
+++ b/packages/frontend/src/components/MkClickerGame.vue
@@ -23,9 +23,9 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
 import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
 import * as os from '@/os.js';
 import { useInterval } from '@@/js/use-interval.js';
-import * as game from '@/scripts/clicker-game.js';
+import * as game from '@/utility/clicker-game.js';
 import number from '@/filters/number.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 
 const saveData = game.saveData;
 const cookies = computed(() => saveData.value?.cookies);
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index 0d7a67eaec..8b39468d4c 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref, watch } from 'vue';
 import { bundledLanguagesInfo } from 'shiki/langs';
 import type { BundledLanguage } from 'shiki/langs';
-import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
-import { defaultStore } from '@/store.js';
+import { getHighlighter, getTheme } from '@/utility/code-highlighter.js';
+import { store } from '@/store.js';
 
 const props = defineProps<{
 	code: string;
@@ -22,7 +22,7 @@ const props = defineProps<{
 }>();
 
 const highlighter = await getHighlighter();
-const darkMode = defaultStore.reactiveState.darkMode;
+const darkMode = store.r.darkMode;
 const codeLang = ref<BundledLanguage | 'aiscript'>('js');
 
 const [lightThemeName, darkThemeName] = await Promise.all([
@@ -74,10 +74,8 @@ watch(() => props.lang, (to) => {
 <style module lang="scss">
 .codeBlockRoot :global(.shiki) {
 	padding: 1em;
-	margin: .5em 0;
+	margin: 0;
 	overflow: auto;
-	border-radius: 8px;
-	border: 1px solid var(--MI_THEME-divider);
 	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
 
 	color: var(--shiki-fallback);
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index cb82bfd98b..9708b78a30 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</button>
 	<Suspense>
 		<template #fallback>
-			<MkLoading />
+			<MkLoading/>
 		</template>
 		<XCode v-if="show && lang" :code="code" :lang="lang"/>
 		<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
@@ -28,9 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent, ref } from 'vue';
 import * as os from '@/os.js';
 import MkLoading from '@/components/global/MkLoading.vue';
-import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	code: string;
@@ -42,7 +42,7 @@ const props = withDefaults(defineProps<{
 	forceShow: false,
 });
 
-const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code);
+const show = ref(props.forceShow === true ? true : !prefer.s.dataSaver.code);
 
 const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
 
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index 6a278250fa..169f3df7a4 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</header>
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''"
 		@enter="enter"
 		@afterEnter="afterEnter"
 		@leave="leave"
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkContextMenu.vue b/packages/frontend/src/components/MkContextMenu.vue
index f51fefa0c0..46da0840fa 100644
--- a/packages/frontend/src/components/MkContextMenu.vue
+++ b/packages/frontend/src/components/MkContextMenu.vue
@@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <Transition
 	appear
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
 >
 	<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
 		<MkMenu :items="items" :align="'left'" @close="emit('closed')"/>
@@ -21,8 +21,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
 import MkMenu from './MkMenu.vue';
 import type { MenuItem } from '@/types/menu.js';
-import contains from '@/scripts/contains.js';
-import { defaultStore } from '@/store.js';
+import contains from '@/utility/contains.js';
+import { prefer } from '@/preferences.js';
 import * as os from '@/os.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 0186cfc2c0..3c41d597de 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -35,13 +35,13 @@ import { onMounted, shallowRef, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import Cropper from 'cropperjs';
 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 { defaultStore } from '@/store.js';
-import { apiUrl } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrl } from '@/utility/media-proxy.js';
+import { prefer } from '@/preferences.js';
 
 const emit = defineEmits<{
 	(ev: 'ok', cropped: Misskey.entities.DriveFile): void;
@@ -81,8 +81,8 @@ const ok = async () => {
 			formData.append('i', $i!.token);
 			if (props.uploadFolder) {
 				formData.append('folderId', props.uploadFolder);
-			} else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) {
-				formData.append('folderId', defaultStore.state.uploadFolder);
+			} else if (props.uploadFolder !== null && prefer.s.uploadFolder) {
+				formData.append('folderId', prefer.s.uploadFolder);
 			}
 
 			window.fetch(apiUrl + '/drive/files/create', {
diff --git a/packages/frontend/src/components/MkCwButton.vue b/packages/frontend/src/components/MkCwButton.vue
index b5f6e78b6c..cc8bbf1104 100644
--- a/packages/frontend/src/components/MkCwButton.vue
+++ b/packages/frontend/src/components/MkCwButton.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
-import { concat } from '@/scripts/array.js';
+import { concat } from '@/utility/array.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index 0d5a16126b..b5842876ac 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts">
 import { defineComponent, h, TransitionGroup, useCssModule } from 'vue';
 import type { PropType } from 'vue';
+import type { MisskeyEntity } from '@/types/date-separated-list.js';
 import MkAd from '@/components/global/MkAd.vue';
 import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { instance } from '@/instance.js';
-import { defaultStore } from '@/store.js';
-import type { MisskeyEntity } from '@/types/date-separated-list.js';
+import { prefer } from '@/preferences.js';
 
 export default defineComponent({
 	props: {
@@ -150,7 +150,7 @@ export default defineComponent({
 			[$style['direction-up']]: props.direction === 'up',
 		};
 
-		return () => defaultStore.state.animation ? h(TransitionGroup, {
+		return () => prefer.s.animation ? h(TransitionGroup, {
 			class: classes,
 			name: 'list',
 			tag: 'div',
diff --git a/packages/frontend/src/components/MkDigitalClock.vue b/packages/frontend/src/components/MkDigitalClock.vue
index 2e2321e6ac..8198356a76 100644
--- a/packages/frontend/src/components/MkDigitalClock.vue
+++ b/packages/frontend/src/components/MkDigitalClock.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, onUnmounted, ref, watch } from 'vue';
-import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
+import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js';
 
 const props = withDefaults(defineProps<{
 	showS?: boolean;
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index e45c3bd9ce..733d50728e 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -45,8 +45,8 @@ import bytes from '@/filters/bytes.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index 44e3b59ade..3bcd934f8a 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template v-if="!hover"><i :class="$style.icon" class="ti ti-folder ti-fw"></i></template>
 		{{ folder.name }}
 	</p>
-	<p v-if="defaultStore.state.uploadFolder == folder.id" :class="$style.upload">
+	<p v-if="prefer.s.uploadFolder == folder.id" :class="$style.upload">
 		{{ i18n.ts.uploadFolder }}
 	</p>
 	<button v-if="selectMode" class="_button" :class="$style.checkboxWrapper" @click.prevent.stop="checkboxClicked">
@@ -38,11 +38,11 @@ import { computed, defineAsyncComponent, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	folder: Misskey.entities.DriveFolder;
@@ -244,8 +244,8 @@ function deleteFolder() {
 	misskeyApi('drive/folders/delete', {
 		folderId: props.folder.id,
 	}).then(() => {
-		if (defaultStore.state.uploadFolder === props.folder.id) {
-			defaultStore.set('uploadFolder', null);
+		if (prefer.s.uploadFolder === props.folder.id) {
+			prefer.commit('uploadFolder', null);
 		}
 	}).catch(err => {
 		switch (err.id) {
@@ -266,7 +266,7 @@ function deleteFolder() {
 }
 
 function setAsUploadFolder() {
-	defaultStore.set('uploadFolder', props.folder.id);
+	prefer.commit('uploadFolder', props.folder.id);
 }
 
 function onContextmenu(ev: MouseEvent) {
@@ -295,7 +295,7 @@ function onContextmenu(ev: MouseEvent) {
 		danger: true,
 		action: deleteFolder,
 	}];
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menu = menu.concat([{ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFolderId,
diff --git a/packages/frontend/src/components/MkDrive.navFolder.vue b/packages/frontend/src/components/MkDrive.navFolder.vue
index 8df3c86ebf..7433aea061 100644
--- a/packages/frontend/src/components/MkDrive.navFolder.vue
+++ b/packages/frontend/src/components/MkDrive.navFolder.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index 8be6d6f53d..270ef51916 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -104,12 +104,12 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue';
 import XFolder from '@/components/MkDrive.folder.vue';
 import XFile from '@/components/MkDrive.file.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useStream } from '@/stream.js';
-import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { uploadFile, uploads } from '@/scripts/upload.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { uploadFile, uploads } from '@/utility/upload.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	initialFolder?: Misskey.entities.DriveFolder;
@@ -142,7 +142,7 @@ const selectedFiles = ref<Misskey.entities.DriveFile[]>([]);
 const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]);
 const uploadings = uploads;
 const connection = useStream().useChannel('drive');
-const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
+const keepOriginal = ref<boolean>(prefer.s.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい
 
 // ドロップされようとしているか
 const draghover = ref(false);
@@ -716,7 +716,7 @@ function onContextmenu(ev: MouseEvent) {
 }
 
 onMounted(() => {
-	if (defaultStore.state.enableInfiniteScroll && loadMoreFiles.value) {
+	if (prefer.s.enableInfiniteScroll && loadMoreFiles.value) {
 		nextTick(() => {
 			ilFilesObserver.observe(loadMoreFiles.value?.$el);
 		});
@@ -737,7 +737,7 @@ onMounted(() => {
 });
 
 onActivated(() => {
-	if (defaultStore.state.enableInfiniteScroll) {
+	if (prefer.s.enableInfiniteScroll) {
 		nextTick(() => {
 			ilFilesObserver.observe(loadMoreFiles.value?.$el);
 		});
diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
index 6e9eb75920..a53c9c7904 100644
--- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
+++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
@@ -105,8 +105,8 @@ import MkInfo from '@/components/MkInfo.vue';
 
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { normalizeEmbedParams, getEmbedCode } from '@/scripts/get-embed-code.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { normalizeEmbedParams, getEmbedCode } from '@/utility/get-embed-code.js';
 
 const emit = defineEmits<{
 	(ev: 'ok'): void;
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 62a1000674..384682277e 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -131,17 +131,18 @@ import type {
 import XSection from '@/components/MkEmojiPicker.section.vue';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import * as os from '@/os.js';
-import { isTouchUsing } from '@/scripts/touch.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { isTouchUsing } from '@/utility/touch.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
 import { $i } from '@/account.js';
-import { checkReactionPermissions } from '@/scripts/check-reaction-permissions.js';
+import { checkReactionPermissions } from '@/utility/check-reaction-permissions.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	showPinned?: boolean;
-  pinnedEmojis?: string[];
+	pinnedEmojis?: string[];
 	maxHeight?: number;
 	asDrawer?: boolean;
 	asWindow?: boolean;
@@ -163,8 +164,9 @@ const {
 	emojiPickerScale,
 	emojiPickerWidth,
 	emojiPickerHeight,
-	recentlyUsedEmojis,
-} = defaultStore.reactiveState;
+} = prefer.r;
+
+const recentlyUsedEmojis = store.r.recentlyUsedEmojis;
 
 const recentlyUsedEmojisDef = computed(() => {
 	return recentlyUsedEmojis.value.map(getDef);
@@ -317,7 +319,7 @@ watch(q, () => {
 			}
 			if (matches.size >= max) return matches;
 
-			for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+			for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
 				for (const emoji of emojis) {
 					if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
 						matches.add(emoji);
@@ -334,7 +336,7 @@ watch(q, () => {
 			}
 			if (matches.size >= max) return matches;
 
-			for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+			for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
 				for (const emoji of emojis) {
 					if (index[emoji.char].some(k => k.startsWith(newQ))) {
 						matches.add(emoji);
@@ -351,7 +353,7 @@ watch(q, () => {
 			}
 			if (matches.size >= max) return matches;
 
-			for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
+			for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
 				for (const emoji of emojis) {
 					if (index[emoji.char].some(k => k.includes(newQ))) {
 						matches.add(emoji);
@@ -413,7 +415,7 @@ function computeButtonTitle(ev: MouseEvent): void {
 
 function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
 	const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
-	if (el && defaultStore.state.animation) {
+	if (el && prefer.s.animation) {
 		const rect = el.getBoundingClientRect();
 		const x = rect.left + (el.offsetWidth / 2);
 		const y = rect.top + (el.offsetHeight / 2);
@@ -427,10 +429,10 @@ function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef,
 
 	// 最近使った絵文字更新
 	if (!pinned.value?.includes(key)) {
-		let recents = defaultStore.state.recentlyUsedEmojis;
+		let recents = store.s.recentlyUsedEmojis;
 		recents = recents.filter((emoji) => emoji !== key);
 		recents.unshift(key);
-		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
+		store.set('recentlyUsedEmojis', recents.splice(0, 32));
 	}
 }
 
diff --git a/packages/frontend/src/components/MkEmojiPickerDialog.vue b/packages/frontend/src/components/MkEmojiPickerDialog.vue
index 21c712b441..6d7062b41c 100644
--- a/packages/frontend/src/components/MkEmojiPickerDialog.vue
+++ b/packages/frontend/src/components/MkEmojiPickerDialog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="modal"
 	v-slot="{ type, maxHeight }"
 	:zPriority="'middle'"
-	:preferType="defaultStore.state.emojiPickerStyle"
+	:preferType="prefer.s.emojiPickerStyle"
 	:hasInteractionWithOtherFocusTrappedEls="true"
 	:transparentBg="true"
 	:manualShowing="manualShowing"
@@ -40,16 +40,16 @@ import * as Misskey from 'misskey-js';
 import { shallowRef } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 import MkEmojiPicker from '@/components/MkEmojiPicker.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	manualShowing?: boolean | null;
 	src?: HTMLElement;
 	showPinned?: boolean;
-  pinnedEmojis?: string[],
+	pinnedEmojis?: string[],
 	asReactionPicker?: boolean;
 	targetNote?: Misskey.entities.Note;
-  choseAndClose?: boolean;
+	choseAndClose?: boolean;
 }>(), {
 	manualShowing: null,
 	showPinned: true,
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index fb1b5220fb..554586b2f3 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -14,10 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</button>
 	</header>
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.folderToggleEnterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.folderToggleLeaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.folderToggleEnterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.folderToggleLeaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.folderToggleEnterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.folderToggleLeaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.folderToggleEnterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.folderToggleLeaveTo : ''"
 		@enter="enter"
 		@afterEnter="afterEnter"
 		@leave="leave"
@@ -33,8 +33,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref, shallowRef, watch } from 'vue';
 import { miLocalStorage } from '@/local-storage.js';
-import { defaultStore } from '@/store.js';
-import { getBgColor } from '@/scripts/get-bg-color.js';
+import { prefer } from '@/preferences.js';
+import { getBgColor } from '@/utility/get-bg-color.js';
 
 const miLocalStoragePrefix = 'ui:folder:' as const;
 
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 384c0c0b34..1e2057d28e 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -27,10 +27,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 		<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
 			<Transition
-				:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
-				:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
-				:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
-				:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
+				:enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''"
+				:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''"
+				:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''"
+				:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''"
 				@enter="enter"
 				@afterEnter="afterEnter"
 				@leave="leave"
@@ -57,8 +57,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { nextTick, onMounted, ref, shallowRef } from 'vue';
-import { defaultStore } from '@/store.js';
-import { getBgColor } from '@/scripts/get-bg-color.js';
+import { prefer } from '@/preferences.js';
+import { getBgColor } from '@/utility/get-bg-color.js';
 
 const props = withDefaults(defineProps<{
 	defaultOpen?: boolean;
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index c1dc67f776..3d5d0ec5ab 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -39,13 +39,14 @@ import { onBeforeUnmount, onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { host } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { pleaseLogin } from '@/utility/please-login.js';
 import { $i } from '@/account.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	user: Misskey.entities.UserDetailed,
@@ -100,7 +101,7 @@ async function onClick() {
 				userId: props.user.id,
 			});
 		} else {
-			if (defaultStore.state.alwaysConfirmFollow) {
+			if (prefer.s.alwaysConfirmFollow) {
 				const { canceled } = await os.confirm({
 					type: 'question',
 					text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }),
@@ -120,11 +121,11 @@ async function onClick() {
 			} else {
 				await misskeyApi('following/create', {
 					userId: props.user.id,
-					withReplies: defaultStore.state.defaultWithReplies,
+					withReplies: store.s.defaultWithReplies,
 				});
 				emit('update:user', {
 					...props.user,
-					withReplies: defaultStore.state.defaultWithReplies,
+					withReplies: store.s.defaultWithReplies,
 				});
 				hasPendingFollowRequestFromYou.value = true;
 
diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue
index ecb6cf882b..0a902f3400 100644
--- a/packages/frontend/src/components/MkFormDialog.file.vue
+++ b/packages/frontend/src/components/MkFormDialog.file.vue
@@ -15,8 +15,8 @@ import * as Misskey from 'misskey-js';
 import { computed, ref } from 'vue';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
-import { selectFile } from '@/scripts/select-file.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { selectFile } from '@/utility/select-file.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const props = defineProps<{
 	fileId?: string | null;
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index a639eae208..8fd16e13d4 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -80,7 +80,7 @@ import MkRange from './MkRange.vue';
 import MkButton from './MkButton.vue';
 import MkRadios from './MkRadios.vue';
 import XFile from './MkFormDialog.file.vue';
-import type { Form } from '@/scripts/form.js';
+import type { Form } from '@/utility/form.js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue
index 22f8355acf..49a6c65170 100644
--- a/packages/frontend/src/components/MkGalleryPostPreview.vue
+++ b/packages/frontend/src/components/MkGalleryPostPreview.vue
@@ -35,14 +35,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { computed, ref } from 'vue';
 import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	post: Misskey.entities.GalleryPost;
 }>();
 
 const hover = ref(false);
-const safe = computed(() => defaultStore.state.nsfw === 'ignore' || defaultStore.state.nsfw === 'respect' && !props.post.isSensitive);
+const safe = computed(() => prefer.s.nsfw === 'ignore' || prefer.s.nsfw === 'respect' && !props.post.isSensitive);
 const show = computed(() => safe.value || hover.value);
 
 function enterHover(): void {
diff --git a/packages/frontend/src/components/MkHeatmap.vue b/packages/frontend/src/components/MkHeatmap.vue
index 0cc0df9911..8339e68b07 100644
--- a/packages/frontend/src/components/MkHeatmap.vue
+++ b/packages/frontend/src/components/MkHeatmap.vue
@@ -16,11 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, nextTick, watch, shallowRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
@@ -106,7 +106,7 @@ async function renderChart() {
 
 	await nextTick();
 
-	const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
+	const color = store.s.darkMode ? '#b4e900' : '#86b300';
 
 	// 視覚上の分かりやすさのため上から最も大きい3つの値の平均を最大値とする
 	const max = values.slice().sort((a, b) => b - a).slice(0, 3).reduce((a, b) => a + b, 0) / 3;
diff --git a/packages/frontend/src/components/MkHorizontalSwipe.vue b/packages/frontend/src/components/MkHorizontalSwipe.vue
index 196c962a06..ad85660fc4 100644
--- a/packages/frontend/src/components/MkHorizontalSwipe.vue
+++ b/packages/frontend/src/components/MkHorizontalSwipe.vue
@@ -28,12 +28,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, shallowRef, computed, nextTick, watch } from 'vue';
 import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
-import { defaultStore } from '@/store.js';
-import { isHorizontalSwipeSwiping as isSwiping } from '@/scripts/touch.js';
+import { isHorizontalSwipeSwiping as isSwiping } from '@/utility/touch.js';
+import { prefer } from '@/preferences.js';
 
 const rootEl = shallowRef<HTMLDivElement>();
 
-// eslint-disable-next-line no-undef
 const tabModel = defineModel<string>('tab');
 
 const props = defineProps<{
@@ -44,7 +43,7 @@ const emit = defineEmits<{
 	(ev: 'swiped', newKey: string, direction: 'left' | 'right'): void;
 }>();
 
-const shouldAnimate = computed(() => defaultStore.reactiveState.enableHorizontalSwipe.value || defaultStore.reactiveState.animation.value);
+const shouldAnimate = computed(() => prefer.r.enableHorizontalSwipe.value || prefer.r.animation.value);
 
 // ▼ しきい値 ▼ //
 
@@ -72,7 +71,7 @@ const isSwipingForClass = ref(false);
 let swipeAborted = false;
 
 function touchStart(event: TouchEvent) {
-	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
+	if (!prefer.r.enableHorizontalSwipe.value) return;
 
 	if (event.touches.length !== 1) return;
 
@@ -83,7 +82,7 @@ function touchStart(event: TouchEvent) {
 }
 
 function touchMove(event: TouchEvent) {
-	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
+	if (!prefer.r.enableHorizontalSwipe.value) return;
 
 	if (event.touches.length !== 1) return;
 
@@ -134,7 +133,7 @@ function touchEnd(event: TouchEvent) {
 		return;
 	}
 
-	if (!defaultStore.reactiveState.enableHorizontalSwipe.value) return;
+	if (!prefer.r.enableHorizontalSwipe.value) return;
 
 	if (event.touches.length !== 0) return;
 
diff --git a/packages/frontend/src/components/MkImgWithBlurhash.vue b/packages/frontend/src/components/MkImgWithBlurhash.vue
index c04d0864fb..37cbc5d06b 100644
--- a/packages/frontend/src/components/MkImgWithBlurhash.vue
+++ b/packages/frontend/src/components/MkImgWithBlurhash.vue
@@ -6,13 +6,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div ref="root" :class="['chromatic-ignore', $style.root, { [$style.cover]: cover }]" :title="title ?? ''">
 	<TransitionGroup
-		:duration="defaultStore.state.animation && props.transition?.duration || undefined"
-		:enterActiveClass="defaultStore.state.animation && props.transition?.enterActiveClass || undefined"
-		:leaveActiveClass="defaultStore.state.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined"
-		:enterFromClass="defaultStore.state.animation && props.transition?.enterFromClass || undefined"
-		:leaveToClass="defaultStore.state.animation && props.transition?.leaveToClass || undefined"
-		:enterToClass="defaultStore.state.animation && props.transition?.enterToClass || undefined"
-		:leaveFromClass="defaultStore.state.animation && props.transition?.leaveFromClass || undefined"
+		:duration="prefer.s.animation && props.transition?.duration || undefined"
+		:enterActiveClass="prefer.s.animation && props.transition?.enterActiveClass || undefined"
+		:leaveActiveClass="prefer.s.animation && (props.transition?.leaveActiveClass ?? $style.transition_leaveActive) || undefined"
+		:enterFromClass="prefer.s.animation && props.transition?.enterFromClass || undefined"
+		:leaveToClass="prefer.s.animation && props.transition?.leaveToClass || undefined"
+		:enterToClass="prefer.s.animation && props.transition?.enterToClass || undefined"
+		:leaveFromClass="prefer.s.animation && props.transition?.leaveFromClass || undefined"
 	>
 		<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
 		<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="$style.img" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
@@ -60,7 +60,7 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
 import { computed, nextTick, onMounted, onUnmounted, shallowRef, watch, ref } from 'vue';
 import { v4 as uuid } from 'uuid';
 import { render } from 'buraha';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	transition?: {
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index 739061bce1..aff9a67020 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -50,8 +50,8 @@ import { debounce } from 'throttle-debounce';
 import MkButton from '@/components/MkButton.vue';
 import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
-import { Autocomplete } from '@/scripts/autocomplete.js';
-import type { SuggestionType } from '@/scripts/autocomplete.js';
+import { Autocomplete } from '@/utility/autocomplete.js';
+import type { SuggestionType } from '@/utility/autocomplete.js';
 
 const props = defineProps<{
 	modelValue: string | number | null;
diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue
index b0601cf7f9..7902151921 100644
--- a/packages/frontend/src/components/MkInstanceCardMini.vue
+++ b/packages/frontend/src/components/MkInstanceCardMini.vue
@@ -18,8 +18,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkMiniChart from '@/components/MkMiniChart.vue';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 
 const props = defineProps<{
 	instance: Misskey.entities.FederationInstance;
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index c2860ed89b..9d475bc8aa 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -88,10 +88,10 @@ 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 '@/scripts/use-chart-tooltip.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
 import { $i } from '@/account.js';
 import * as os from '@/os.js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import MkHeatmap from '@/components/MkHeatmap.vue';
@@ -99,7 +99,7 @@ import type { HeatmapSource } from '@/components/MkHeatmap.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkRetentionHeatmap from '@/components/MkRetentionHeatmap.vue';
 import MkRetentionLineChart from '@/components/MkRetentionLineChart.vue';
-import { initChart } from '@/scripts/init-chart.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
diff --git a/packages/frontend/src/components/MkInstanceTicker.vue b/packages/frontend/src/components/MkInstanceTicker.vue
index cc7380f170..e2a55a803f 100644
--- a/packages/frontend/src/components/MkInstanceTicker.vue
+++ b/packages/frontend/src/components/MkInstanceTicker.vue
@@ -12,12 +12,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed } from 'vue';
-import type { CSSProperties } from 'vue';
 import { instanceName as localInstanceName } from '@@/js/config.js';
+import type { CSSProperties } from 'vue';
 import { instance as localInstance } from '@/instance.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
-import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 
 const props = defineProps<{
 	host: string | null;
diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue
index 1a71f6574f..08eddfc03c 100644
--- a/packages/frontend/src/components/MkInviteCode.vue
+++ b/packages/frontend/src/components/MkInviteCode.vue
@@ -64,7 +64,7 @@ import { computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkButton from '@/components/MkButton.vue';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.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/components/MkKeyValue.vue b/packages/frontend/src/components/MkKeyValue.vue
index 50c9e16e5e..6a4b298523 100644
--- a/packages/frontend/src/components/MkKeyValue.vue
+++ b/packages/frontend/src/components/MkKeyValue.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { } from 'vue';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index 32c1a2d172..9a78eb67f2 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -30,8 +30,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { shallowRef } from 'vue';
 import MkModal from '@/components/MkModal.vue';
 import { navbarItemDef } from '@/navbar.js';
-import { defaultStore } from '@/store.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { deviceKind } from '@/utility/device-kind.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	src?: HTMLElement;
@@ -50,7 +50,7 @@ const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'pop
 
 const modal = shallowRef<InstanceType<typeof MkModal>>();
 
-const menu = defaultStore.state.menu;
+const menu = prefer.s.menu;
 
 const items = Object.keys(navbarItemDef).filter(k => !menu.includes(k)).map(k => navbarItemDef[k]).filter(def => def.show == null ? true : def.show).map(def => ({
 	type: def.to ? 'link' : 'button',
diff --git a/packages/frontend/src/components/MkLink.vue b/packages/frontend/src/components/MkLink.vue
index 68091432ed..252adb6ae9 100644
--- a/packages/frontend/src/components/MkLink.vue
+++ b/packages/frontend/src/components/MkLink.vue
@@ -18,7 +18,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 '@/scripts/use-tooltip.js';
+import { useTooltip } from '@/utility/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/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 3d2795b37a..33d4d269e7 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -10,20 +10,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 	tabindex="0"
 	:class="[
 		$style.audioContainer,
-		(audio.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive,
+		(audio.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive,
 	]"
 	@contextmenu.stop
 	@keydown.stop
 >
 	<button v-if="hide" :class="$style.hidden" @click="show">
 		<div :class="$style.hiddenTextWrapper">
-			<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
-			<b v-else style="display: block;"><i class="ti ti-music"></i> {{ defaultStore.state.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
+			<b v-if="audio.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.audio}${audio.size ? ' ' + bytes(audio.size) : ''})` : '' }}</b>
+			<b v-else style="display: block;"><i class="ti ti-music"></i> {{ prefer.s.dataSaver.media && audio.size ? bytes(audio.size) : i18n.ts.audio }}</b>
 			<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
 		</div>
 	</button>
 
-	<div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.nativeAudioContainer">
+	<div v-else-if="prefer.s.useNativeUiForVideoAudioPlayer" :class="$style.nativeAudioContainer">
 		<audio
 			ref="audioEl"
 			preload="metadata"
@@ -91,15 +91,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { shallowRef, watch, computed, ref, onDeactivated, onActivated, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu.js';
-import type { Keymap } from '@/scripts/hotkey.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard';
-import { defaultStore } from '@/store.js';
+import type { Keymap } from '@/utility/hotkey.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard';
 import { i18n } from '@/i18n.js';
 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 { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	audio: Misskey.entities.DriveFile;
@@ -155,10 +155,10 @@ const playerEl = shallowRef<HTMLDivElement>();
 const audioEl = shallowRef<HTMLAudioElement>();
 
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
-const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore'));
+const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore'));
 
 async function show() {
-	if (props.audio.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+	if (props.audio.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
 		const { canceled } = await os.confirm({
 			type: 'question',
 			text: i18n.ts.sensitiveMediaRevealConfirm,
@@ -240,7 +240,7 @@ function showMenu(ev: MouseEvent) {
 		menu.push({ type: 'divider' }, ...details);
 	}
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menu.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
@@ -407,7 +407,7 @@ onDeactivated(() => {
 	elapsedTimeMs.value = 0;
 	durationMs.value = 0;
 	bufferedEnd.value = 0;
-	hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.audio.isSensitive && defaultStore.state.nsfw !== 'ignore');
+	hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.audio.isSensitive && prefer.s.nsfw !== 'ignore');
 	stopAudioElWatch();
 	onceInit = false;
 	if (mediaTickFrameId) {
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index 3e521e0a03..f23cf507fb 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -27,9 +27,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
 import * as os from '@/os.js';
 import MkMediaAudio from '@/components/MkMediaAudio.vue';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	media: Misskey.entities.DriveFile;
@@ -38,7 +38,7 @@ const props = defineProps<{
 const hide = ref(true);
 
 async function show() {
-	if (props.media.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+	if (props.media.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
 		const { canceled } = await os.confirm({
 			type: 'question',
 			text: i18n.ts.sensitiveMediaRevealConfirm,
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 8ab990b926..6029b1e0b6 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive]" @click="onclick">
+<div :class="[hide ? $style.hidden : $style.visible, (image.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive]" @click="onclick">
 	<component
 		:is="disableImageLink ? 'div' : 'a'"
 		v-bind="disableImageLink ? {
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	>
 		<ImgWithBlurhash
 			:hash="image.blurhash"
-			:src="(defaultStore.state.dataSaver.media && hide) ? null : url"
+			:src="(prefer.s.dataSaver.media && hide) ? null : url"
 			:forceBlurhash="hide"
 			:cover="hide || cover"
 			:alt="image.comment || image.name"
@@ -32,8 +32,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template v-if="hide">
 		<div :class="$style.hiddenText">
 			<div :class="$style.hiddenTextWrapper">
-				<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
-				<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b>
+				<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
+				<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ prefer.s.dataSaver.media && image.size ? bytes(image.size) : i18n.ts.image }}</b>
 				<span v-if="controls" style="display: block;">{{ i18n.ts.clickToShow }}</span>
 			</div>
 		</div>
@@ -54,14 +54,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { watch, ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import bytes from '@/filters/bytes.js';
 import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
-import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 import { $i, iAmModerator } from '@/account.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	image: Misskey.entities.DriveFile;
@@ -77,9 +77,9 @@ const props = withDefaults(defineProps<{
 
 const hide = ref(true);
 
-const url = computed(() => (props.raw || defaultStore.state.loadRawImages)
+const url = computed(() => (props.raw || prefer.s.loadRawImages)
 	? props.image.url
-	: defaultStore.state.disableShowingAnimatedImages
+	: prefer.s.disableShowingAnimatedImages
 		? getStaticImageUrl(props.image.url)
 		: props.image.thumbnailUrl,
 );
@@ -91,7 +91,7 @@ async function onclick(ev: MouseEvent) {
 
 	if (hide.value) {
 		ev.stopPropagation();
-		if (props.image.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+		if (props.image.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
 			const { canceled } = await os.confirm({
 				type: 'question',
 				text: i18n.ts.sensitiveMediaRevealConfirm,
@@ -105,7 +105,7 @@ async function onclick(ev: MouseEvent) {
 
 // Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
 watch(() => props.image, () => {
-	hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.image.isSensitive && defaultStore.state.nsfw !== 'ignore');
+	hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.image.isSensitive && prefer.s.nsfw !== 'ignore');
 }, {
 	deep: true,
 	immediate: true,
@@ -166,7 +166,7 @@ function showMenu(ev: MouseEvent) {
 		menuItems.push({ type: 'divider' }, ...details);
 	}
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 07b0736472..bb0bf54b68 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -12,9 +12,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 			:class="[
 				$style.medias,
 				count === 1 ? [$style.n1, {
-					[$style.n116_9]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '16_9',
-					[$style.n11_1]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '1_1',
-					[$style.n12_3]: defaultStore.reactiveState.mediaListWithOneImageAppearance.value === '2_3',
+					[$style.n116_9]: prefer.s.mediaListWithOneImageAppearance === '16_9',
+					[$style.n11_1]: prefer.s.mediaListWithOneImageAppearance === '1_1',
+					[$style.n12_3]: prefer.s.mediaListWithOneImageAppearance === '2_3',
 				}] : count === 2 ? $style.n2 : count === 3 ? $style.n3 : count === 4 ? $style.n4 : $style.nMany,
 			]"
 		>
@@ -33,13 +33,13 @@ import * as Misskey from 'misskey-js';
 import PhotoSwipeLightbox from 'photoswipe/lightbox';
 import PhotoSwipe from 'photoswipe';
 import 'photoswipe/style.css';
+import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js';
 import XBanner from '@/components/MkMediaBanner.vue';
 import XImage from '@/components/MkMediaImage.vue';
 import XVideo from '@/components/MkMediaVideo.vue';
 import * as os from '@/os.js';
-import { FILE_TYPE_BROWSERSAFE } from '@@/js/const.js';
-import { defaultStore } from '@/store.js';
-import { focusParent } from '@/scripts/focus.js';
+import { focusParent } from '@/utility/focus.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	mediaList: Misskey.entities.DriveFile[];
@@ -75,7 +75,7 @@ async function calcAspectRatio() {
 		return `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
 	};
 
-	switch (defaultStore.state.mediaListWithOneImageAppearance) {
+	switch (prefer.s.mediaListWithOneImageAppearance) {
 		case '16_9':
 			gallery.value.style.aspectRatio = ratioMax(16 / 9);
 			break;
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 682da22711..74c1aefc3a 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	:class="[
 		$style.videoContainer,
 		controlsShowing && $style.active,
-		(video.isSensitive && defaultStore.state.highlightSensitiveMedia) && $style.sensitive,
+		(video.isSensitive && prefer.s.highlightSensitiveMedia) && $style.sensitive,
 	]"
 	@mouseover="onMouseOver"
 	@mouseleave="onMouseLeave"
@@ -20,13 +20,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 >
 	<button v-if="hide" :class="$style.hidden" @click="show">
 		<div :class="$style.hiddenTextWrapper">
-			<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
-			<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ defaultStore.state.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
+			<b v-if="video.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media ? ` (${i18n.ts.video}${video.size ? ' ' + bytes(video.size) : ''})` : '' }}</b>
+			<b v-else style="display: block;"><i class="ti ti-movie"></i> {{ prefer.s.dataSaver.media && video.size ? bytes(video.size) : i18n.ts.video }}</b>
 			<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
 		</div>
 	</button>
 
-	<div v-else-if="defaultStore.reactiveState.useNativeUIForVideoAudioPlayer.value" :class="$style.videoRoot">
+	<div v-else-if="prefer.s.useNativeUiForVideoAudioPlayer" :class="$style.videoRoot">
 		<video
 			ref="videoEl"
 			:class="$style.video"
@@ -112,17 +112,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, shallowRef, computed, watch, onDeactivated, onActivated, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu.js';
-import type { Keymap } from '@/scripts/hotkey.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard';
+import type { Keymap } from '@/utility/hotkey.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard';
 import bytes from '@/filters/bytes.js';
 import { hms } from '@/filters/hms.js';
-import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js';
-import hasAudio from '@/scripts/media-has-audio.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 { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	video: Misskey.entities.DriveFile;
@@ -175,10 +175,10 @@ function hasFocus() {
 }
 
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
-const hide = ref((defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore'));
+const hide = ref((prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore'));
 
 async function show() {
-	if (props.video.isSensitive && defaultStore.state.confirmWhenRevealingSensitiveMedia) {
+	if (props.video.isSensitive && prefer.s.confirmWhenRevealingSensitiveMedia) {
 		const { canceled } = await os.confirm({
 			type: 'question',
 			text: i18n.ts.sensitiveMediaRevealConfirm,
@@ -265,7 +265,7 @@ function showMenu(ev: MouseEvent) {
 		menu.push({ type: 'divider' }, ...details);
 	}
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menu.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
@@ -502,7 +502,7 @@ onDeactivated(() => {
 	elapsedTimeMs.value = 0;
 	durationMs.value = 0;
 	bufferedEnd.value = 0;
-	hide.value = (defaultStore.state.nsfw === 'force' || defaultStore.state.dataSaver.media) ? true : (props.video.isSensitive && defaultStore.state.nsfw !== 'ignore');
+	hide.value = (prefer.s.nsfw === 'force' || prefer.s.dataSaver.media) ? true : (props.video.isSensitive && prefer.s.nsfw !== 'ignore');
 	stopVideoElWatch();
 	onceInit = false;
 	if (mediaTickFrameId) {
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index 8b610a022d..a43161867f 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -19,8 +19,8 @@ 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 { defaultStore } from '@/store.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	username: string;
@@ -36,7 +36,7 @@ const isMe = $i && (
 	`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase()
 );
 
-const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar
+const avatarUrl = computed(() => prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar
 	? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
 	: `/avatar/@${props.username}@${props.host}`,
 );
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index d484c1b338..61b3fa2fee 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -181,10 +181,10 @@ import MkSwitchButton from '@/components/MkSwitch.button.vue';
 import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { isTouchUsing } from '@/scripts/touch.js';
-import type { Keymap } from '@/scripts/hotkey.js';
-import { isFocusable } from '@/scripts/focus.js';
-import { getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
+import { isTouchUsing } from '@/utility/touch.js';
+import type { Keymap } from '@/utility/hotkey.js';
+import { isFocusable } from '@/utility/focus.js';
+import { getNodeOrNull } from '@/utility/get-dom-node-or-null.js';
 
 const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
 </script>
diff --git a/packages/frontend/src/components/MkModal.vue b/packages/frontend/src/components/MkModal.vue
index 19588003fa..8b3086d55e 100644
--- a/packages/frontend/src/components/MkModal.vue
+++ b/packages/frontend/src/components/MkModal.vue
@@ -43,13 +43,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { nextTick, normalizeClass, onMounted, onUnmounted, provide, watch, ref, shallowRef, computed } from 'vue';
+import type { Keymap } from '@/utility/hotkey.js';
 import * as os from '@/os.js';
-import { isTouchUsing } from '@/scripts/touch.js';
-import { defaultStore } from '@/store.js';
-import { deviceKind } from '@/scripts/device-kind.js';
-import type { Keymap } from '@/scripts/hotkey.js';
-import { focusTrap } from '@/scripts/focus-trap.js';
-import { focusParent } from '@/scripts/focus.js';
+import { isTouchUsing } from '@/utility/touch.js';
+import { deviceKind } from '@/utility/device-kind.js';
+import { focusTrap } from '@/utility/focus-trap.js';
+import { focusParent } from '@/utility/focus.js';
+import { prefer } from '@/preferences.js';
 
 function getFixedContainer(el: Element | null): Element | null {
 	if (el == null || el.tagName === 'BODY') return null;
@@ -106,7 +106,7 @@ const zIndex = os.claimZIndex(props.zPriority);
 const useSendAnime = ref(false);
 const type = computed<ModalTypes>(() => {
 	if (props.preferType === 'auto') {
-		if ((defaultStore.state.menuStyle === 'drawer') || (defaultStore.state.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) {
+		if ((prefer.s.menuStyle === 'drawer') || (prefer.s.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) {
 			return 'drawer';
 		} else {
 			return props.src != null ? 'popup' : 'dialog';
@@ -117,7 +117,7 @@ const type = computed<ModalTypes>(() => {
 });
 const isEnableBgTransparent = computed(() => props.transparentBg && (type.value === 'popup'));
 const transitionName = computed((() =>
-	defaultStore.state.animation
+	prefer.s.animation
 		? useSendAnime.value
 			? 'send'
 			: type.value === 'drawer'
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index fcd6da43c8..73ea96d54f 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	v-show="!isDeleted"
 	ref="rootEl"
 	v-hotkey="keymap"
-	:class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender || defaultStore.state.enableRenderingOptimization }]"
+	:class="[$style.root, { [$style.showActionsOnlyHover]: prefer.s.showNoteActionsOnlyHover, [$style.skipRender]: prefer.s.skipNoteRender || store.s.enableRenderingOptimization }]"
 	:tabindex="isDeleted ? '-1' : '0'"
 >
 	<div v-if="appearNote.reply && inReplyToCollapsed && !isRenote" :class="$style.collapsedInReplyTo">
@@ -57,7 +57,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkNoteHeader :note="appearNote" :mini="true" @click.stop/>
 			</div>
 		</div>
-		<div :class="[{ [$style.noteClickToOpen]: defaultStore.state.noteClickToOpen }]" @click.stop="defaultStore.state.noteClickToOpen ? noteClickToOpen(appearNote.id) : undefined">
+		<div :class="[{ [$style.noteClickToOpen]: store.s.noteClickToOpen }]" @click.stop="store.s.noteClickToOpen ? noteClickToOpen(appearNote.id) : undefined">
 			<div style="container-type: inline-size;">
 				<p v-if="appearNote.cw != null" :class="$style.cw">
 					<Mfm
@@ -137,17 +137,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<button v-else :class="$style.footerButton" class="_button" disabled>
 					<i class="ti ti-ban"></i>
 				</button>
-				<button v-if="defaultStore.state.enableFallbackReactButton && appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly' && !disableReactionsViewer" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop="like()">
+				<button v-if="store.s.enableFallbackReactButton && appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly' && !disableReactionsViewer" ref="likeButton" :class="$style.footerButton" class="_button" @click.stop="like()">
 					<i class="ti ti-heart"></i>
 				</button>
 				<button ref="reactButton" :class="$style.footerButton" class="_button" @click.stop="toggleReact()">
 					<i v-if="(appearNote.reactionAcceptance === 'likeOnly' || disableReactionsViewer) && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
 					<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
-					<i v-else-if="appearNote.reactionAcceptance === 'likeOnly' || disableReactionsViewer" class="ti ti-heart"></i>
+					<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
 					<i v-else class="ti ti-mood-plus"></i>
-					<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount || disableReactionsViewer) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
+					<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount || disableReactionsViewer) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
 				</button>
-				<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
+				<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" :class="$style.footerButton" class="_button" @mousedown.prevent="clip()">
 					<i class="ti ti-paperclip"></i>
 				</button>
 				<button ref="menuButton" :class="$style.footerButton" class="_button" @mousedown.prevent="showMenu()">
@@ -200,8 +200,8 @@ import { shouldCollapsed } from '@@/js/collapsed.js';
 import { host } from '@@/js/config.js';
 import type { Ref } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
-import type { Keymap } from '@/scripts/hotkey.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
+import type { Keymap } from '@/utility/hotkey.js';
 import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteHeader from '@/components/MkNoteHeader.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
@@ -212,33 +212,35 @@ import MkCwButton from '@/components/MkCwButton.vue';
 import MkPoll from '@/components/MkPoll.vue';
 import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
-import { pleaseLogin } from '@/scripts/please-login.js';
-import { checkWordMute } from '@/scripts/check-word-mute.js';
+import { pleaseLogin } from '@/utility/please-login.js';
+import { checkWordMute } from '@/utility/check-word-mute.js';
 import { notePage } from '@/filters/note.js';
 import { userPage } from '@/filters/user.js';
 import number from '@/filters/number.js';
 import * as os from '@/os.js';
-import * as sound from '@/scripts/sound.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { defaultStore, noteViewInterruptors } from '@/store.js';
-import { reactionPicker } from '@/scripts/reaction-picker.js';
-import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
+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 { i18n } from '@/i18n.js';
-import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
-import { useNoteCapture } from '@/scripts/use-note-capture.js';
-import { deepClone } from '@/scripts/clone.js';
-import { useTooltip } from '@/scripts/use-tooltip.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { getNoteSummary } from '@/scripts/get-note-summary.js';
+import { getAbuseNoteMenu, getCopyNoteLinkMenu, getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
+import { useNoteCapture } from '@/utility/use-note-capture.js';
+import { deepClone } from '@/utility/clone.js';
+import { useTooltip } from '@/utility/use-tooltip.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { getNoteSummary } from '@/utility/get-note-summary.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
-import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
+import { showMovedDialog } from '@/utility/show-moved-dialog.js';
 import { isEnabledUrlPreview } from '@/instance.js';
-import { focusPrev, focusNext } from '@/scripts/focus.js';
-import { getAppearNote } from '@/scripts/get-appear-note.js';
+import { focusPrev, focusNext } from '@/utility/focus.js';
+import { getAppearNote } from '@/utility/get-appear-note.js';
+import { prefer } from '@/preferences.js';
+import { getPluginHandlers } from '@/plugin.js';
 import { useRouter } from '@/router/supplier.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { spacingNote } from '@/scripts/autospacing.js';
+import { spacingNote } from '@/utility/autospacing.js';
+import { store } from '@/store.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -266,6 +268,7 @@ const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', nul
 const note = ref(deepClone(props.note));
 
 // plugin
+const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
 if (noteViewInterruptors.length > 0) {
 	onMounted(async () => {
 		let result: Misskey.entities.Note | null = deepClone(note.value);
@@ -304,23 +307,23 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
 const isDeleted = ref(false);
 const muted = ref(checkMute(appearNote.value, $i?.mutedWords));
 const hardMuted = ref(props.withHardMute && checkMute(appearNote.value, $i?.hardMutedWords, true));
-const showSoftWordMutedWord = computed(() => defaultStore.state.showSoftWordMutedWord);
+const showSoftWordMutedWord = computed(() => prefer.s.showSoftWordMutedWord);
 const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
 const translating = ref(false);
 const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || (appearNote.value.visibility === 'followers' && appearNote.value.userId === $i?.id));
 const renoteCollapsed = ref(
-	defaultStore.state.collapseRenotes && isRenote && (
+	prefer.s.collapseRenotes && isRenote && (
 		($i && ($i.id === note.value.userId || $i.id === appearNote.value.userId)) || // `||` must be `||`! See https://github.com/misskey-dev/misskey/issues/13131
 		(appearNote.value.myReaction != null)
 	),
 );
 
-const defaultLike = computed(() => defaultStore.state.like ?? '❤️');
+const defaultLike = computed(() => store.s.like ?? '❤️');
 
-const enableTranslateButton = ref(defaultStore.state.enableTranslateButton);
+const enableTranslateButton = ref(store.s.enableTranslateButton);
 
-const inReplyToCollapsed = ref(defaultStore.state.collapseNotesRepliedTo);
-const disableReactionsViewer = ref(defaultStore.reactiveState.disableReactionsViewer);
+const inReplyToCollapsed = ref(store.s.collapseNotesRepliedTo);
+const disableReactionsViewer = ref(store.s.disableReactionsViewer);
 
 const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
 	type: 'lookup',
@@ -371,7 +374,7 @@ const keymap = {
 	},
 	'c': () => {
 		if (renoteCollapsed.value) return;
-		if (!defaultStore.state.showClipButtonInNoteFooter) return;
+		if (!prefer.s.showClipButtonInNoteFooter) return;
 		clip();
 	},
 	'o': () => {
@@ -537,7 +540,7 @@ function react(): void {
 			reaction: defaultLike.value,
 		});
 		const el = reactButton.value;
-		if (el && defaultStore.state.animation) {
+		if (el && prefer.s.animation) {
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + (el.offsetWidth / 2);
 			const y = rect.top + (el.offsetHeight / 2);
@@ -548,7 +551,7 @@ function react(): void {
 	} else {
 		blur();
 		reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => {
-			if (defaultStore.state.confirmOnReact) {
+			if (prefer.s.confirmOnReact) {
 				const confirm = await os.confirm({
 					type: 'question',
 					text: i18n.tsx.reactAreYouSure({ emoji: reaction.replace('@.', '') }),
@@ -607,7 +610,7 @@ function onContextmenu(ev: MouseEvent): void {
 	if (ev.target && isLink(ev.target as HTMLElement)) return;
 	if (window.getSelection()?.toString() !== '') return;
 
-	if (defaultStore.state.useReactionPickerForContextMenu) {
+	if (prefer.s.useReactionPickerForContextMenu) {
 		ev.preventDefault();
 		react();
 	} else {
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index bc8e700bf3..ab89e11cfd 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -74,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<i class="ti ti-rocket-off"></i>
 					</span>
 				</div>
-				<MkInstanceTicker v-if="showTicker" :style="{ cursor: defaultStore.state.clickToShowInstanceTickerWindow ? 'pointer' : 'default' }" :instance="appearNote.user.instance" :host="appearNote.user.host"/>
+				<MkInstanceTicker v-if="showTicker" :style="{ cursor: store.s.clickToShowInstanceTickerWindow ? 'pointer' : 'default' }" :instance="appearNote.user.instance" :host="appearNote.user.host"/>
 			</div>
 		</header>
 		<div :class="$style.noteContent">
@@ -149,49 +149,49 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<MkReactionsViewer v-if="appearNote.reactionAcceptance !== 'likeOnly' && !disableReactionsViewer" ref="reactionsViewer" :note="appearNote"/>
 			<div :class="$style.footerButton">
-			<button class="_button" :class="$style.noteFooterButton" @click.stop="reply()">
-				<i class="ti ti-arrow-back-up"></i>
-				<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
-			</button>
-			<button
-				v-if="canRenote"
-				ref="renoteButton"
-				class="_button"
-				:class="$style.noteFooterButton"
-				@mousedown.prevent="renote()"
-			>
-				<i class="ti ti-repeat"></i>
-				<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
-			</button>
-			<button v-else class="_button" :class="$style.noteFooterButton" disabled>
-				<i class="ti ti-ban"></i>
-			</button>
-			<button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly' && !disableReactionsViewer" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()">
-				<i class="ti ti-heart"></i>
-			</button>
-			<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click.stop="toggleReact()">
-				<i v-if="(appearNote.reactionAcceptance === 'likeOnly' || disableReactionsViewer) && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
-				<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
-				<i v-else-if="appearNote.reactionAcceptance === 'likeOnly' || disableReactionsViewer" class="ti ti-heart"></i>
-				<i v-else class="ti ti-mood-plus"></i>
-				<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount || disableReactionsViewer) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
-			</button>
-			<button
-				v-if="appearNote.updatedAt" ref="historyMenuButton" class="_button" :class="[
-					$style.noteFooterButton,
-					$style.noteFooterButtonHistoryMenu,
-					showingNoteHistoryRef ? $style.active : undefined,
-				]" @mousedown="historyMenu()"
-			>
-				<i class="ti ti-history"></i>
-			</button>
-			<button v-if="defaultStore.state.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
-				<i class="ti ti-paperclip"></i>
-			</button>
-			<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
-				<i class="ti ti-dots"></i>
-			</button>
-		    </div>
+				<button class="_button" :class="$style.noteFooterButton" @click.stop="reply()">
+					<i class="ti ti-arrow-back-up"></i>
+					<p v-if="appearNote.repliesCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.repliesCount) }}</p>
+				</button>
+				<button
+					v-if="canRenote"
+					ref="renoteButton"
+					class="_button"
+					:class="$style.noteFooterButton"
+					@mousedown.prevent="renote()"
+				>
+					<i class="ti ti-repeat"></i>
+					<p v-if="appearNote.renoteCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.renoteCount) }}</p>
+				</button>
+				<button v-else class="_button" :class="$style.noteFooterButton" disabled>
+					<i class="ti ti-ban"></i>
+				</button>
+				<button v-if="appearNote.myReaction == null && appearNote.reactionAcceptance !== 'likeOnly' && !disableReactionsViewer" ref="likeButton" :class="$style.noteFooterButton" class="_button" @mousedown="like()">
+					<i class="ti ti-heart"></i>
+				</button>
+				<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click.stop="toggleReact()">
+					<i v-if="(appearNote.reactionAcceptance === 'likeOnly' || disableReactionsViewer) && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
+					<i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
+					<i v-else-if="appearNote.reactionAcceptance === 'likeOnly' || disableReactionsViewer" class="ti ti-heart"></i>
+					<i v-else class="ti ti-mood-plus"></i>
+					<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || prefer.s.showReactionsCount || disableReactionsViewer) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
+				</button>
+				<button
+					v-if="appearNote.updatedAt" ref="historyMenuButton" class="_button" :class="[
+						$style.noteFooterButton,
+						$style.noteFooterButtonHistoryMenu,
+						showingNoteHistoryRef ? $style.active : undefined,
+					]" @mousedown="historyMenu()"
+				>
+					<i class="ti ti-history"></i>
+				</button>
+				<button v-if="prefer.s.showClipButtonInNoteFooter" ref="clipButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="clip()">
+					<i class="ti ti-paperclip"></i>
+				</button>
+				<button ref="menuButton" class="_button" :class="$style.noteFooterButton" @mousedown.prevent="showMenu()">
+					<i class="ti ti-dots"></i>
+				</button>
+			</div>
 		</footer>
 	</article>
 	<div :class="$style.tabs">
@@ -253,6 +253,9 @@ import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import { isLink } from '@@/js/is-link.js';
 import { host } from '@@/js/config.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
+import type { Paging } from '@/components/MkPagination.vue';
+import type { Keymap } from '@/utility/hotkey.js';
 import MkNoteSub from '@/components/MkNoteSub.vue';
 import MkNoteSimple from '@/components/MkNoteSimple.vue';
 import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
@@ -263,36 +266,35 @@ import MkPoll from '@/components/MkPoll.vue';
 import MkUsersTooltip from '@/components/MkUsersTooltip.vue';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
 import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
-import { pleaseLogin } from '@/scripts/please-login.js';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
-import { checkWordMute } from '@/scripts/check-word-mute.js';
+import { pleaseLogin } from '@/utility/please-login.js';
+import { checkWordMute } from '@/utility/check-word-mute.js';
 import { userPage } from '@/filters/user.js';
 import { notePage } from '@/filters/note.js';
 import number from '@/filters/number.js';
 import * as os from '@/os.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import * as sound from '@/scripts/sound.js';
-import { defaultStore, noteViewInterruptors } from '@/store.js';
-import { reactionPicker } from '@/scripts/reaction-picker.js';
-import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
+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 { i18n } from '@/i18n.js';
-import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/scripts/get-note-menu.js';
-import { useNoteCapture } from '@/scripts/use-note-capture.js';
-import { deepClone } from '@/scripts/clone.js';
-import { useTooltip } from '@/scripts/use-tooltip.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { getNoteClipMenu, getNoteMenu, getRenoteMenu } from '@/utility/get-note-menu.js';
+import { useNoteCapture } from '@/utility/use-note-capture.js';
+import { deepClone } from '@/utility/clone.js';
+import { useTooltip } from '@/utility/use-tooltip.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
-import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
+import { showMovedDialog } from '@/utility/show-moved-dialog.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkPagination from '@/components/MkPagination.vue';
-import type { Paging } from '@/components/MkPagination.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import { isEnabledUrlPreview } from '@/instance.js';
-import { getAppearNote } from '@/scripts/get-appear-note.js';
-import type { Keymap } from '@/scripts/hotkey.js';
+import { getAppearNote } from '@/utility/get-appear-note.js';
+import { prefer } from '@/preferences.js';
+import { getPluginHandlers } from '@/plugin.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { spacingNote } from '@/scripts/autospacing.js';
+import { spacingNote } from '@/utility/autospacing.js';
+import { store } from '@/store.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
@@ -306,6 +308,7 @@ const inChannel = inject('inChannel', null);
 const note = ref(deepClone(props.note));
 
 // plugin
+const noteViewInterruptors = getPluginHandlers('note_view_interruptor');
 if (noteViewInterruptors.length > 0) {
 	onMounted(async () => {
 		let result: Misskey.entities.Note | null = deepClone(note.value);
@@ -344,11 +347,11 @@ const translation = ref<Misskey.entities.NotesTranslateResponse | null>(null);
 const translating = ref(false);
 const parsed = appearNote.value.text ? mfm.parse(appearNote.value.text) : null;
 const urls = parsed ? extractUrlFromMfm(parsed).filter((url) => appearNote.value.renote?.url !== url && appearNote.value.renote?.uri !== url) : null;
-const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.value.user.instance);
+const showTicker = (prefer.s.instanceTicker === 'always') || (prefer.s.instanceTicker === 'remote' && appearNote.value.user.instance);
 const conversation = ref<Misskey.entities.Note[]>([]);
 const replies = ref<Misskey.entities.Note[]>([]);
 const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
-const defaultLike = computed(() => defaultStore.state.like ?? '❤️');
+const defaultLike = computed(() => store.s.like ?? '❤️');
 
 type ShowingNoteHistoryState = {
 	createdAt: string | null;
@@ -357,9 +360,9 @@ type ShowingNoteHistoryState = {
 } | null;
 const showingNoteHistoryRef = ref<ShowingNoteHistoryState>(null);
 
-const disableReactionsViewer = ref(defaultStore.reactiveState.disableReactionsViewer);
+const disableReactionsViewer = ref(store.s.disableReactionsViewer);
 
-const enableTranslateButton = ref(defaultStore.state.enableTranslateButton);
+const enableTranslateButton = ref(store.s.enableTranslateButton);
 
 const pleaseLoginContext = computed<OpenOnRemoteOptions>(() => ({
 	type: 'lookup',
@@ -372,7 +375,7 @@ const keymap = {
 	'q': () => renote(),
 	'm': () => showMenu(),
 	'c': () => {
-		if (!defaultStore.state.showClipButtonInNoteFooter) return;
+		if (!prefer.s.showClipButtonInNoteFooter) return;
 		clip();
 	},
 	'o': () => galleryEl.value?.openGallery(),
@@ -528,7 +531,7 @@ function react(): void {
 			reaction: defaultLike.value,
 		});
 		const el = reactButton.value;
-		if (el && defaultStore.state.animation) {
+		if (el && prefer.s.animation) {
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + (el.offsetWidth / 2);
 			const y = rect.top + (el.offsetHeight / 2);
@@ -539,7 +542,7 @@ function react(): void {
 	} else {
 		blur();
 		reactionPicker.show(reactButton.value ?? null, note.value, async (reaction) => {
-			if (defaultStore.state.confirmOnReact) {
+			if (prefer.s.confirmOnReact) {
 				const confirm = await os.confirm({
 					type: 'question',
 					text: i18n.tsx.reactAreYouSure({ emoji: reaction.replace('@.', '') }),
@@ -583,7 +586,7 @@ function onContextmenu(ev: MouseEvent): void {
 	if (ev.target && isLink(ev.target as HTMLElement)) return;
 	if (window.getSelection()?.toString() !== '') return;
 
-	if (defaultStore.state.useReactionPickerForContextMenu) {
+	if (prefer.s.useReactionPickerForContextMenu) {
 		ev.preventDefault();
 		react();
 	} else {
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index 59b0e11987..0b564e98dc 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -52,8 +52,11 @@ import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import { notePage } from '@/filters/note.js';
 import { userPage } from '@/filters/user.js';
+<<<<<<< HEAD
 import { defaultStore } from '@/store.js';
 import MkInstanceTicker from '@/components/MkInstanceTicker.vue';
+=======
+>>>>>>> develop
 
 const isDetail = ref(false);
 const setDetail = (value) => {
diff --git a/packages/frontend/src/components/MkNoteMediaGrid.vue b/packages/frontend/src/components/MkNoteMediaGrid.vue
index e51ea5a2de..764d9f6a32 100644
--- a/packages/frontend/src/components/MkNoteMediaGrid.vue
+++ b/packages/frontend/src/components/MkNoteMediaGrid.vue
@@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template v-for="file in note.files">
 	<div
 		v-if="(((
-				(defaultStore.state.nsfw === 'force' || file.isSensitive) &&
-				defaultStore.state.nsfw !== 'ignore'
-			) || (defaultStore.state.dataSaver.media && file.type.startsWith('image/'))) &&
+			(prefer.s.nsfw === 'force' || file.isSensitive) &&
+			prefer.s.nsfw !== 'ignore'
+		) || (prefer.s.dataSaver.media && file.type.startsWith('image/'))) &&
 			!showingFiles.has(file.id)
 		)"
 		:class="[$style.filePreview, { [$style.square]: square }]"
@@ -18,15 +18,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkDriveFileThumbnail
 			:file="file"
 			fit="cover"
-			:highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia"
+			:highlightWhenSensitive="prefer.s.highlightSensitiveMedia"
 			:forceBlurhash="true"
 			:large="true"
 			:class="$style.file"
 		/>
 		<div :class="$style.sensitive">
 			<div>
-				<div v-if="file.isSensitive"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.dataSaver.media && file.size ? ` (${bytes(file.size)})` : '' }}</div>
-				<div v-else><i class="ti ti-photo"></i> {{ defaultStore.state.dataSaver.media && file.size ? bytes(file.size) : i18n.ts.image }}</div>
+				<div v-if="file.isSensitive"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ prefer.s.dataSaver.media && file.size ? ` (${bytes(file.size)})` : '' }}</div>
+				<div v-else><i class="ti ti-photo"></i> {{ prefer.s.dataSaver.media && file.size ? bytes(file.size) : i18n.ts.image }}</div>
 				<div>{{ i18n.ts.clickToShow }}</div>
 			</div>
 		</div>
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkDriveFileThumbnail
 			:file="file"
 			fit="cover"
-			:highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia"
+			:highlightWhenSensitive="prefer.s.highlightSensitiveMedia"
 			:large="true"
 			:class="$style.file"
 		/>
@@ -45,10 +45,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
+import * as Misskey from 'misskey-js';
 import { notePage } from '@/filters/note.js';
 import { i18n } from '@/i18n.js';
-import * as Misskey from 'misskey-js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import bytes from '@/filters/bytes.js';
 
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index e4bade309b..bb7347cd26 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -46,11 +46,11 @@ import MkNoteHeader from '@/components/MkNoteHeader.vue';
 import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
 import MkCwButton from '@/components/MkCwButton.vue';
 import { notePage } from '@/filters/note.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import { userPage } from '@/filters/user.js';
-import { checkWordMute } from '@/scripts/check-word-mute.js';
+import { checkWordMute } from '@/utility/check-word-mute.js';
 
 const props = withDefaults(defineProps<{
 	note: Misskey.entities.Note;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 80cb9a45bb..5d096cf92d 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -164,11 +164,11 @@ import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import MkButton from '@/components/MkButton.vue';
-import { getNoteSummary } from '@/scripts/get-note-summary.js';
+import { getNoteSummary } from '@/utility/get-note-summary.js';
 import { notePage } from '@/filters/note.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { signinRequired } from '@/account.js';
 import { infoImageUrl } from '@/instance.js';
 
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 2094557f97..f7a3538d32 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -26,17 +26,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
 import * as Misskey from 'misskey-js';
+import type { notificationTypes } from '@@/js/const.js';
 import MkPagination from '@/components/MkPagination.vue';
 import XNotification from '@/components/MkNotification.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkNote from '@/components/MkNote.vue';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import type { notificationTypes } from '@@/js/const.js';
 import { infoImageUrl } from '@/instance.js';
-import { defaultStore } from '@/store.js';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
-import { misskeyApi } from '@/scripts/misskey-api';
+import { misskeyApi } from '@/utility/misskey-api';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	excludeTypes?: typeof notificationTypes[number][];
@@ -44,7 +44,7 @@ const props = defineProps<{
 
 const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
 
-const pagination = computed(() => defaultStore.reactiveState.useGroupedNotifications.value ? {
+const pagination = computed(() => prefer.r.useGroupedNotifications.value ? {
 	endpoint: 'i/notifications-grouped' as const,
 	limit: 20,
 	params: computed(() => ({
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index c3fc1961eb..67b3165ade 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -32,19 +32,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
 import { url } from '@@/js/config.js';
 import { getScrollContainer } from '@@/js/scroll.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import type { PageMetadata } from '@/page.js';
 import RouterView from '@/components/global/RouterView.vue';
 import MkWindow from '@/components/MkWindow.vue';
-import { popout as _popout } from '@/scripts/popout.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { popout as _popout } from '@/utility/popout.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { i18n } from '@/i18n.js';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
 import { openingWindowsCount } from '@/os.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import { useRouterFactory } from '@/router/supplier.js';
 import { mainRouter } from '@/router/main.js';
 import { analytics } from '@/analytics.js';
+import { DI } from '@/di.js';
 
 const props = defineProps<{
 	initialPath: string;
@@ -119,7 +120,7 @@ windowRouter.addListener('change', ctx => {
 
 windowRouter.init();
 
-provide('router', windowRouter);
+provide(DI.router, windowRouter);
 provide('inAppSearchMarkerId', searchMarkerId);
 provideMetadataReceiver((metadataGetter) => {
 	const info = metadataGetter();
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index a3e765b61a..ca7fddddfa 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <Transition
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
 	mode="out-in"
 >
 	<MkLoading v-if="fetching"/>
@@ -44,15 +44,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts">
 import { computed, isRef, nextTick, onActivated, onBeforeMount, onBeforeUnmount, onDeactivated, ref, shallowRef, watch } from 'vue';
-import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js';
 import * as Misskey from 'misskey-js';
 import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
-import type { ComputedRef, Ref } from 'vue';
+import { onScrollTop, isTopVisible, getBodyScrollHeight, getScrollContainer, onScrollBottom, scrollToBottom, scroll, isBottomVisible } from '@@/js/scroll.js';
+import type { ComputedRef } from 'vue';
 import type { MisskeyEntity } from '@/types/date-separated-list.js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
 
 const SECOND_FETCH_LIMIT = 30;
 const TOLERANCE = 16;
@@ -141,7 +140,7 @@ const empty = computed(() => items.value.size === 0);
 const error = ref(false);
 const {
 	enableInfiniteScroll,
-} = defaultStore.reactiveState;
+} = prefer.r;
 
 const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value);
 const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body);
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 1b8a333f1a..2d29cf0cbd 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -31,11 +31,11 @@ import { computed, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { host } from '@@/js/config.js';
 import { useInterval } from '@@/js/use-interval.js';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
-import { sum } from '@/scripts/array.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
+import { sum } from '@/utility/array.js';
+import { pleaseLogin } from '@/utility/please-login.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkPollEditor.vue b/packages/frontend/src/components/MkPollEditor.vue
index 3726ddf822..22fe189a63 100644
--- a/packages/frontend/src/components/MkPollEditor.vue
+++ b/packages/frontend/src/components/MkPollEditor.vue
@@ -58,8 +58,8 @@ import MkInput from './MkInput.vue';
 import MkSelect from './MkSelect.vue';
 import MkSwitch from './MkSwitch.vue';
 import MkButton from './MkButton.vue';
-import { formatDateTimeString } from '@/scripts/format-time-string.js';
-import { addTime } from '@/scripts/time.js';
+import { formatDateTimeString } from '@/utility/format-time-string.js';
+import { addTime } from '@/utility/time.js';
 import { i18n } from '@/i18n.js';
 
 export type PollEditorModelValue = {
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index be2b101bf5..09bb4d7b3d 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -112,29 +112,31 @@ import { host, url } from '@@/js/config.js';
 import type { ShallowRef } from 'vue';
 import type { PostFormProps } from '@/types/post-form.js';
 import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
-import MkNoteSimple from '@/components/MkNoteSimple.vue';
 import MkNotePreview from '@/components/MkNotePreview.vue';
 import XPostFormAttaches from '@/components/MkPostFormAttaches.vue';
 import MkPollEditor from '@/components/MkPollEditor.vue';
-import { erase, unique } from '@/scripts/array.js';
-import { extractMentions } from '@/scripts/extract-mentions.js';
-import { formatTimeString } from '@/scripts/format-time-string.js';
-import { Autocomplete } from '@/scripts/autocomplete.js';
+import MkNoteSimple from '@/components/MkNoteSimple.vue';
+import { erase, unique } from '@/utility/array.js';
+import { extractMentions } from '@/utility/extract-mentions.js';
+import { formatTimeString } from '@/utility/format-time-string.js';
+import { Autocomplete } from '@/utility/autocomplete.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { selectFiles } from '@/scripts/select-file.js';
-import { defaultStore, notePostInterruptors, postFormActions } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { selectFiles } from '@/utility/select-file.js';
+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 { uploadFile } from '@/scripts/upload.js';
-import { deepClone } from '@/scripts/clone.js';
+import { uploadFile } from '@/utility/upload.js';
+import { deepClone } from '@/utility/clone.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import { miLocalStorage } from '@/local-storage.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { emojiPicker } from '@/scripts/emoji-picker.js';
-import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { emojiPicker } from '@/utility/emoji-picker.js';
+import { mfmFunctionPicker } from '@/utility/mfm-function-picker.js';
+import { prefer } from '@/preferences.js';
+import { getPluginHandlers } from '@/plugin.js';
 
 const $i = signinRequired();
 
@@ -175,19 +177,18 @@ const text = ref(props.initialText ?? '');
 const files = ref(props.initialFiles ?? []);
 const poll = ref<PollEditorModelValue | null>(null);
 const useCw = ref<boolean>(!!props.initialCw);
-const showPreview = ref(defaultStore.state.showPreview);
-watch(showPreview, () => defaultStore.set('showPreview', showPreview.value));
-const showAddMfmFunction = ref(defaultStore.state.enableQuickAddMfmFunction);
-watch(showAddMfmFunction, () => defaultStore.set('enableQuickAddMfmFunction', showAddMfmFunction.value));
+const showPreview = ref(store.s.showPreview);
+watch(showPreview, () => store.set('showPreview', showPreview.value));
+const showAddMfmFunction = ref(prefer.s.enableQuickAddMfmFunction);
+watch(showAddMfmFunction, () => prefer.commit('enableQuickAddMfmFunction', showAddMfmFunction.value));
 const cw = ref<string | null>(props.initialCw ?? null);
-const localOnly = ref(props.initialLocalOnly ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly));
-const visibility = ref(props.initialVisibility ?? (defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility));
+const localOnly = ref(props.initialLocalOnly ?? (prefer.s.rememberNoteVisibility ? store.s.localOnly : prefer.s.defaultNoteLocalOnly));
+const visibility = ref(props.initialVisibility ?? (prefer.s.rememberNoteVisibility ? store.s.visibility : prefer.s.defaultNoteVisibility));
 const visibleUsers = ref<Misskey.entities.UserDetailed[]>([]);
 if (props.initialVisibleUsers) {
 	props.initialVisibleUsers.forEach(u => pushVisibleUser(u));
 }
-const reactionAcceptance = ref(defaultStore.state.reactionAcceptance);
-const autocomplete = ref(null);
+const reactionAcceptance = ref(store.s.reactionAcceptance);
 const draghover = ref(false);
 const quoteId = ref<string | null>(null);
 const hasNotSpecifiedMentions = ref(false);
@@ -197,9 +198,10 @@ const showingOptions = ref(false);
 const textAreaReadOnly = ref(false);
 const justEndedComposition = ref(false);
 const renoteTargetNote: ShallowRef<PostFormProps['renote'] | null> = shallowRef(props.renote);
+const postFormActions = getPluginHandlers('post_form_action');
 
-const enableMFMCheatsheet = ref(defaultStore.state.enableMFMCheatsheet);
-const enableUndoClearPostForm = ref(defaultStore.state.enableUndoClearPostForm);
+const enableMFMCheatsheet = ref(prefer.s.enableMFMCheatsheet);
+const enableUndoClearPostForm = ref(prefer.s.enableUndoClearPostForm);
 
 const draftKey = computed((): string => {
 	let key = props.channel ? `channel:${props.channel.id}` : '';
@@ -265,60 +267,60 @@ const canPost = computed((): boolean => {
 		(!poll.value || poll.value.choices.length >= 2);
 });
 
-const withHashtags = computed(defaultStore.makeGetterSetter('postFormWithHashtags'));
-const hashtags = computed(defaultStore.makeGetterSetter('postFormHashtags'));
+const withHashtags = computed(store.makeGetterSetter('postFormWithHashtags'));
+const hashtags = computed(store.makeGetterSetter('postFormHashtags'));
 
 const textHistory = ref<string[]>([]);
 const currentHistoryIndex = ref(-1);
 const showTextManageButton = computed(() => text.value !== '' || currentHistoryIndex.value >= 0);
 const textManageButtonIcon = computed(() => {
-  if (currentHistoryIndex.value >= 0) return 'ti ti-arrow-back-up';
-  return text.value !== '' ? 'ti ti-trash' : '';
+	if (currentHistoryIndex.value >= 0) return 'ti ti-arrow-back-up';
+	return text.value !== '' ? 'ti ti-trash' : '';
 });
 let lastSaveTime = 0;
 const SAVE_INTERVAL = 300;
 
 function clearText() {
-  if (text.value !== '') {
-    saveToHistory();
-    text.value = '';
-    nextTick(() => textareaEl.value && autosize.update(textareaEl.value));
-  }
+	if (text.value !== '') {
+		saveToHistory();
+		text.value = '';
+		nextTick(() => textareaEl.value && autosize.update(textareaEl.value));
+	}
 }
 
 function saveToHistory() {
-  const now = Date.now();
-  if (
-    (now - lastSaveTime > SAVE_INTERVAL) &&
+	const now = Date.now();
+	if (
+		(now - lastSaveTime > SAVE_INTERVAL) &&
     (textHistory.value[currentHistoryIndex.value] !== text.value) &&
     (text.value.length > 0)
-  ) {
-    textHistory.value = textHistory.value.slice(0, currentHistoryIndex.value + 1);
-    textHistory.value.push(text.value);
-    currentHistoryIndex.value = textHistory.value.length - 1;
-    lastSaveTime = now;
+	) {
+		textHistory.value = textHistory.value.slice(0, currentHistoryIndex.value + 1);
+		textHistory.value.push(text.value);
+		currentHistoryIndex.value = textHistory.value.length - 1;
+		lastSaveTime = now;
 
-    if (textHistory.value.length > 50) {
-      textHistory.value = textHistory.value.slice(-50);
-      currentHistoryIndex.value = textHistory.value.length - 1;
-    }
-  }
+		if (textHistory.value.length > 50) {
+			textHistory.value = textHistory.value.slice(-50);
+			currentHistoryIndex.value = textHistory.value.length - 1;
+		}
+	}
 }
 
 function undoTextChange() {
-  if (currentHistoryIndex.value >= 0) {
-    text.value = textHistory.value[currentHistoryIndex.value];
-    currentHistoryIndex.value--;
-    nextTick(() => textareaEl.value && autosize.update(textareaEl.value));
-  }
+	if (currentHistoryIndex.value >= 0) {
+		text.value = textHistory.value[currentHistoryIndex.value];
+		currentHistoryIndex.value--;
+		nextTick(() => textareaEl.value && autosize.update(textareaEl.value));
+	}
 }
 
 function handleTextManageClick() {
-  if (currentHistoryIndex.value >= 0) {
-    undoTextChange();
-  } else {
-    clearText();
-  }
+	if (currentHistoryIndex.value >= 0) {
+		undoTextChange();
+	} else {
+		clearText();
+	}
 }
 
 watch(text, () => {
@@ -411,7 +413,7 @@ if (props.specified) {
 }
 
 // keep cw when reply
-if (defaultStore.state.keepCw && props.reply && props.reply.cw) {
+if (prefer.s.keepCw && props.reply && props.reply.cw) {
 	useCw.value = true;
 	cw.value = props.reply.cw;
 }
@@ -519,7 +521,7 @@ function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities
 function upload(file: File, name?: string): void {
 	if (props.mock) return;
 
-	uploadFile(file, defaultStore.state.uploadFolder, name).then(res => {
+	uploadFile(file, prefer.s.uploadFolder, name).then(res => {
 		files.value.push(res);
 	});
 }
@@ -540,8 +542,8 @@ function setVisibility() {
 	}, {
 		changeVisibility: v => {
 			visibility.value = v;
-			if (defaultStore.state.rememberNoteVisibility) {
-				defaultStore.set('visibility', visibility.value);
+			if (prefer.s.rememberNoteVisibility) {
+				store.set('visibility', visibility.value);
 			}
 		},
 		closed: () => dispose(),
@@ -588,8 +590,8 @@ async function toggleLocalOnly() {
 	}
 
 	localOnly.value = !localOnly.value;
-	if (defaultStore.state.rememberNoteVisibility) {
-		defaultStore.set('localOnly', localOnly.value);
+	if (prefer.s.rememberNoteVisibility) {
+		store.set('localOnly', localOnly.value);
 	}
 }
 
@@ -644,8 +646,8 @@ function onKeydown(ev: KeyboardEvent) {
 	if (enableUndoClearPostForm.value && !ev.ctrlKey && !ev.metaKey && !ev.altKey &&
         !justEndedComposition.value && !ev.isComposing &&
         !['Shift', 'Alt', 'Control', 'Meta', 'CapsLock', 'Tab'].includes(ev.key)) {
-      saveToHistory();
-    }
+		saveToHistory();
+	}
 
 	// justEndedComposition.value is for Safari, which keyDown occurs after compositionend.
 	// ev.isComposing is for another browsers.
@@ -669,6 +671,8 @@ function onCompositionEnd(ev: CompositionEvent) {
 	justEndedComposition.value = true;
 }
 
+const pastedFileName = 'yyyy-MM-dd HH-mm-ss [{{number}}]';
+
 async function onPaste(ev: ClipboardEvent) {
 	if (props.mock) return;
 	if (!ev.clipboardData) return;
@@ -679,7 +683,7 @@ async function onPaste(ev: ClipboardEvent) {
 			if (!file) continue;
 			const lio = file.name.lastIndexOf('.');
 			const ext = lio >= 0 ? file.name.slice(lio) : '';
-			const formatted = `${formatTimeString(new Date(file.lastModified), defaultStore.state.pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
+			const formatted = `${formatTimeString(new Date(file.lastModified), pastedFileName).replace(/{{number}}/g, `${i + 1}`)}${ext}`;
 			upload(file, formatted);
 		}
 	}
@@ -713,7 +717,7 @@ async function onPaste(ev: ClipboardEvent) {
 				return;
 			}
 
-			const fileName = formatTimeString(new Date(), defaultStore.state.pastedFileName).replace(/{{number}}/g, '0');
+			const fileName = formatTimeString(new Date(), pastedFileName).replace(/{{number}}/g, '0');
 			const file = new File([paste], `${fileName}.txt`, { type: 'text/plain' });
 			upload(file, `${fileName}.txt`);
 		});
@@ -817,18 +821,10 @@ function isAnnoying(text: string): boolean {
 }
 
 async function post(ev?: MouseEvent) {
-	if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
-		os.alert({
-			type: 'error',
-			text: i18n.ts.cwNotationRequired,
-		});
-		return;
-	}
-
 	if (ev) {
 		const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;
 
-		if (el && defaultStore.state.animation) {
+		if (el && prefer.s.animation) {
 			const rect = el.getBoundingClientRect();
 			const x = rect.left + (el.offsetWidth / 2);
 			const y = rect.top + (el.offsetHeight / 2);
@@ -898,6 +894,7 @@ async function post(ev?: MouseEvent) {
 	}
 
 	// plugin
+	const notePostInterruptors = getPluginHandlers('note_post_interruptor');
 	if (notePostInterruptors.length > 0) {
 		for (const interruptor of notePostInterruptors) {
 			try {
@@ -998,54 +995,54 @@ function insertMention() {
 }
 
 async function insertEmoji(ev: MouseEvent) {
-  textAreaReadOnly.value = true;
-  const target = ev.currentTarget ?? ev.target;
-  if (target == null) return;
+	textAreaReadOnly.value = true;
+	const target = ev.currentTarget ?? ev.target;
+	if (target == null) return;
 
-  // emojiPickerはダイアログが閉じずにtextareaとやりとりするので、
-  // focustrapをかけているとinsertTextAtCursorが効かない
-  // そのため、投稿フォームのテキストに直接注入する
-  // See: https://github.com/misskey-dev/misskey/pull/14282
-  //      https://github.com/misskey-dev/misskey/issues/14274
+	// emojiPickerはダイアログが閉じずにtextareaとやりとりするので、
+	// focustrapをかけているとinsertTextAtCursorが効かない
+	// そのため、投稿フォームのテキストに直接注入する
+	// See: https://github.com/misskey-dev/misskey/pull/14282
+	//      https://github.com/misskey-dev/misskey/issues/14274
 
-  let pos = textareaEl.value?.selectionStart ?? 0;
-  let posEnd = textareaEl.value?.selectionEnd ?? text.value.length;
+	let pos = textareaEl.value?.selectionStart ?? 0;
+	let posEnd = textareaEl.value?.selectionEnd ?? text.value.length;
 
-  const addSpacing = (before: string, after: string, emoji: string) => {
-    let result = emoji;
-    const needSpaceBefore = before.length > 0 && !before.endsWith(' ');
-    const needSpaceAfter = !after.startsWith(' ');
+	const addSpacing = (before: string, after: string, emoji: string) => {
+		let result = emoji;
+		const needSpaceBefore = before.length > 0 && !before.endsWith(' ');
+		const needSpaceAfter = !after.startsWith(' ');
 
-    if (needSpaceBefore) result = ' ' + result;
-    if (needSpaceAfter) result = result + ' ';
+		if (needSpaceBefore) result = ' ' + result;
+		if (needSpaceAfter) result = result + ' ';
 
-    return {
-      text: result,
-      addedSpaces: (needSpaceBefore ? 1 : 0) + (needSpaceAfter ? 1 : 0),
-    };
-  };
+		return {
+			text: result,
+			addedSpaces: (needSpaceBefore ? 1 : 0) + (needSpaceAfter ? 1 : 0),
+		};
+	};
 
-  emojiPicker.show(
-    target as HTMLElement,
-    (emoji) => {
-      const textBefore = text.value.substring(0, pos);
-      const textAfter = text.value.substring(posEnd);
+	emojiPicker.show(
+		target as HTMLElement,
+		(emoji) => {
+			const textBefore = text.value.substring(0, pos);
+			const textAfter = text.value.substring(posEnd);
 
-      const processed = defaultStore.state.emojiAutoSpacing
-        ? addSpacing(textBefore, textAfter, emoji)
-        : { text: emoji + ' ', addedSpaces: 1 };
+			const processed = prefer.s.emojiAutoSpacing
+				? addSpacing(textBefore, textAfter, emoji)
+				: { text: emoji + ' ', addedSpaces: 1 };
 
-      text.value = textBefore + processed.text + textAfter;
+			text.value = textBefore + processed.text + textAfter;
 
-      const newPos = pos + emoji.length + processed.addedSpaces;
-      pos = newPos;
-      posEnd = newPos;
-    },
-    () => {
-      textAreaReadOnly.value = false;
-      nextTick(() => focus());
-    },
-  );
+			const newPos = pos + emoji.length + processed.addedSpaces;
+			pos = newPos;
+			posEnd = newPos;
+		},
+		() => {
+			textAreaReadOnly.value = false;
+			nextTick(() => focus());
+		},
+	);
 }
 
 async function insertMfmFunction(ev: MouseEvent) {
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index c7774d50b2..3ce0c4957b 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -36,12 +36,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent, inject } from 'vue';
 import * as Misskey from 'misskey-js';
 import type { MenuItem } from '@/types/menu';
-import { defaultStore } from '@/store';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard';
+import { copyToClipboard } from '@/utility/copy-to-clipboard';
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
@@ -198,7 +198,7 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
 		action: () => { detachAndDeleteMedia(file); },
 	});
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
diff --git a/packages/frontend/src/components/MkPreferenceContainer.vue b/packages/frontend/src/components/MkPreferenceContainer.vue
new file mode 100644
index 0000000000..85fab462cd
--- /dev/null
+++ b/packages/frontend/src/components/MkPreferenceContainer.vue
@@ -0,0 +1,94 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root">
+	<div :class="$style.body">
+		<slot></slot>
+	</div>
+	<div :class="$style.menu">
+		<i v-if="isAccountOverrided" class="ti ti-user-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i>
+		<div :class="$style.buttons">
+			<button class="_button" style="color: var(--MI_THEME-fg)" @click="showMenu"><i class="ti ti-dots"></i></button>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { ref } from 'vue';
+import type { PREF_DEF } from '@/preferences/def.js';
+import * as os from '@/os.js';
+import { profileManager } from '@/preferences.js';
+
+const props = withDefaults(defineProps<{
+	k: keyof typeof PREF_DEF;
+}>(), {
+});
+
+const isAccountOverrided = ref(profileManager.isAccountOverrided(props.k));
+
+function showMenu(ev: MouseEvent) {
+	const i = window.setInterval(() => {
+		isAccountOverrided.value = profileManager.isAccountOverrided(props.k);
+	}, 100);
+	os.popupMenu(profileManager.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, {
+		onClosing: () => {
+			window.clearInterval(i);
+		},
+	});
+}
+</script>
+
+<style lang="scss" module>
+.root {
+	position: relative;
+	display: flex;
+
+	&:hover {
+		&::after {
+			content: '';
+			position: absolute;
+			top: -8px;
+			left: -8px;
+			width: calc(100% + 16px);
+			height: calc(100% + 16px);
+			border-radius: 8px;
+			background: light-dark(rgba(0, 0, 0, 0.02), rgba(255, 255, 255, 0.02));
+			pointer-events: none;
+		}
+
+		.menu {
+			.buttons {
+				opacity: 0.7;
+			}
+		}
+	}
+
+	.body {
+		flex: 1;
+	}
+
+	.menu {
+		display: flex;
+		gap: 8px;
+		align-items: center;
+		margin-left: 12px;
+		font-size: 12px;
+		padding-left: 8px;
+		border-left: solid 1px var(--MI_THEME-divider);
+
+		&:hover {
+			.buttons {
+				opacity: 1;
+			}
+		}
+
+		.buttons {
+			opacity: 0.3;
+		}
+	}
+}
+</style>
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index 4fb4c6fe56..af4e2a4fe0 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
 import { i18n } from '@/i18n.js';
 import { getScrollContainer } from '@@/js/scroll.js';
-import { isHorizontalSwipeSwiping } from '@/scripts/touch.js';
+import { isHorizontalSwipeSwiping } from '@/utility/touch.js';
 
 const SCROLL_STOP = 10;
 const MAX_PULL_DISTANCE = Infinity;
diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
index 5e42df4795..780f8bc6d0 100644
--- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue
+++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue
@@ -46,7 +46,7 @@ import { $i, getAccounts } from '@/account.js';
 import MkButton from '@/components/MkButton.vue';
 import { instance } from '@/instance.js';
 import { apiWithDialog, promiseDialog } from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 defineProps<{
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index 264b559222..8e5be38e03 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
-import { isTouchUsing } from '@/scripts/touch.js';
+import { isTouchUsing } from '@/utility/touch.js';
 import * as os from '@/os.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkReactionIcon.vue b/packages/frontend/src/components/MkReactionIcon.vue
index c0cbd8a65d..cb25580bc2 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 '@/scripts/use-tooltip.js';
+import { useTooltip } from '@/utility/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 d1dd103237..56bbf1b0b5 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -8,39 +8,42 @@ SPDX-License-Identifier: AGPL-3.0-only
 	ref="buttonEl"
 	v-ripple="canToggle"
 	class="_button"
-	:class="[$style.root, { [$style.reacted]: isReacted, [$style.canToggle]: canToggle, [$style.small]: defaultStore.state.reactionsDisplaySize === 'small', [$style.large]: defaultStore.state.reactionsDisplaySize === 'large' }]"
-	@click.stop="toggleReaction()"
+	:class="[$style.root, { [$style.reacted]: note.myReaction == reaction, [$style.canToggle]: canToggle, [$style.small]: prefer.s.reactionsDisplaySize === 'small', [$style.large]: prefer.s.reactionsDisplaySize === 'large' }]"
+	@click="toggleReaction()"
 	@contextmenu.prevent.stop="menu"
 >
-	<MkReactionIcon :class="defaultStore.state.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
+	<MkReactionIcon :class="prefer.s.limitWidthOfReaction ? $style.limitWidth : ''" :reaction="reaction" :emojiUrl="note.reactionEmojis[reaction.substring(1, reaction.length - 1)]"/>
 	<span :class="$style.count">{{ count }}</span>
 </button>
 </template>
 
 <script lang="ts" setup>
-import { computed, inject, onMounted, onBeforeMount, shallowRef, watch } from 'vue';
+import { computed, inject, onBeforeMount, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import { getUnicodeEmoji } from '@@/js/emojilist.js';
 import MkCustomEmojiDetailedDialog from './MkCustomEmojiDetailedDialog.vue';
 import XDetails from '@/components/MkReactionsViewer.details.vue';
 import MkReactionIcon from '@/components/MkReactionIcon.vue';
 import * as os from '@/os.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { useTooltip } from '@/scripts/use-tooltip.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
+import { useTooltip } from '@/utility/use-tooltip.js';
 import { $i } from '@/account.js';
 import MkReactionEffect from '@/components/MkReactionEffect.vue';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { defaultStore } from '@/store.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import { i18n } from '@/i18n.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 import { customEmojisMap } from '@/custom-emojis.js';
+import { prefer } from '@/preferences.js';
 
 const localEmojiSet = new Set(Array.from(customEmojisMap.keys()));
 const reactionCache = new Map<string, { hasNative: boolean; base: string }>();
 
 function getReactionInfo(reaction: string) {
 	if (reactionCache.has(reaction)) {
-		return reactionCache.get(reaction)!;
+		const cachedReaction = reactionCache.get(reaction);
+		if (cachedReaction) {
+			return cachedReaction;
+		}
 	}
 
 	let hasNative: boolean;
@@ -113,7 +116,7 @@ async function toggleReaction() {
 			noteId: props.note.id,
 		});
 	} else {
-		if (defaultStore.state.confirmOnReact) {
+		if (prefer.s.confirmOnReact) {
 			const confirm = await os.confirm({
 				type: 'question',
 				text: i18n.tsx.reactAreYouSure({ emoji: props.reaction.replace('@.', '') }),
@@ -177,7 +180,7 @@ async function menu(ev) {
 }
 
 function anime() {
-	if (document.hidden || !defaultStore.state.animation || buttonEl.value == null) return;
+	if (document.hidden || !prefer.s.animation || buttonEl.value == null) return;
 
 	const rect = buttonEl.value.getBoundingClientRect();
 	const x = rect.left + 16;
diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue
index 0fee9613d5..c3266c13e3 100644
--- a/packages/frontend/src/components/MkReactionsViewer.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.vue
@@ -5,11 +5,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <TransitionGroup
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_x_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_x_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_x_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_x_leaveTo : ''"
-	:moveClass="defaultStore.state.animation ? $style.transition_x_move : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''"
+	:moveClass="prefer.s.animation ? $style.transition_x_move : ''"
 	tag="div" :class="$style.root"
 >
 	<XReaction v-for="[reaction, count] in mergedReactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/>
@@ -21,30 +21,33 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { inject, watch, ref, computed, onBeforeMount } from 'vue';
 import XReaction from '@/components/MkReactionsViewer.reaction.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { customEmojisMap } from '@/custom-emojis.js';
 
 const localEmojiSet = new Set(Array.from(customEmojisMap.keys()));
 const emojiCache = new Map<string, boolean>();
 
 function hasLocalEmoji(reaction: string): boolean {
-  if (emojiCache.has(reaction)) return emojiCache.get(reaction)!;
-  
-  let result: boolean;
-  if (!reaction.includes(':')) {
-    result = true;
-  } else {
-    const emojiName = reaction.split('@')[0].split(':')[1];
-    result = localEmojiSet.has(emojiName);
-  }
-  
-  emojiCache.set(reaction, result);
-  return result;
+	if (emojiCache.has(reaction)) {
+		const cachedResult = emojiCache.get(reaction);
+		if (cachedResult !== undefined) return cachedResult;
+	}
+
+	let result: boolean;
+	if (!reaction.includes(':')) {
+		result = true;
+	} else {
+		const emojiName = reaction.split('@')[0].split(':')[1];
+		result = localEmojiSet.has(emojiName);
+	}
+
+	emojiCache.set(reaction, result);
+	return result;
 }
 
 function getBaseReaction(reaction: string): string {
-  if (!reaction.includes(':')) return reaction;
-  return `:${reaction.split('@')[0].split(':')[1]}:`;
+	if (!reaction.includes(':')) return reaction;
+	return `:${reaction.split('@')[0].split(':')[1]}:`;
 }
 
 const props = withDefaults(defineProps<{
@@ -66,27 +69,27 @@ const reactions = ref<[string, number][]>([]);
 const hasMoreReactions = ref(false);
 
 const mergedReactions = computed(() => {
-  const reactionMap = new Map();
-  
-  reactions.value.forEach(([reaction, count]) => {
-    if (!hasLocalEmoji(reaction)) {
-      if (reactionMap.has(reaction)) {
-        reactionMap.set(reaction, reactionMap.get(reaction) + count);
-      } else {
-        reactionMap.set(reaction, count);
-      }
-      return;
-    }
-    
-    const baseReaction = getBaseReaction(reaction);
-    if (reactionMap.has(baseReaction)) {
-      reactionMap.set(baseReaction, reactionMap.get(baseReaction) + count);
-    } else {
-      reactionMap.set(baseReaction, count);
-    }
-  });
+	const reactionMap = new Map();
 
-  return Array.from(reactionMap.entries());
+	reactions.value.forEach(([reaction, count]) => {
+		if (!hasLocalEmoji(reaction)) {
+			if (reactionMap.has(reaction)) {
+				reactionMap.set(reaction, reactionMap.get(reaction) + count);
+			} else {
+				reactionMap.set(reaction, count);
+			}
+			return;
+		}
+
+		const baseReaction = getBaseReaction(reaction);
+		if (reactionMap.has(baseReaction)) {
+			reactionMap.set(baseReaction, reactionMap.get(baseReaction) + count);
+		} else {
+			reactionMap.set(baseReaction, count);
+		}
+	});
+
+	return Array.from(reactionMap.entries());
 });
 
 if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.myReaction)) {
@@ -94,9 +97,9 @@ if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.m
 }
 
 onBeforeMount(() => {
-  Object.keys(props.note.reactions).forEach(reaction => {
-    hasLocalEmoji(reaction);
-  });
+	Object.keys(props.note.reactions).forEach(reaction => {
+		hasLocalEmoji(reaction);
+	});
 });
 
 function onMockToggleReaction(emoji: string, count: number) {
@@ -125,7 +128,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
 		...newReactions,
 		...Object.entries(newSource)
 			.sort(([, a], [, b]) => b - a)
-			.filter(([y], i) => i < maxNumber && !newReactionsNames.includes(y)),
+			.filter(([y], i) => i < maxNumber && !newReactionsNames.includes(y)) as [string, number][],
 	];
 
 	newReactions = newReactions.slice(0, props.maxNumber);
diff --git a/packages/frontend/src/components/MkRetentionHeatmap.vue b/packages/frontend/src/components/MkRetentionHeatmap.vue
index 64b573c4d3..c53bf98f67 100644
--- a/packages/frontend/src/components/MkRetentionHeatmap.vue
+++ b/packages/frontend/src/components/MkRetentionHeatmap.vue
@@ -15,11 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, nextTick, shallowRef, ref } from 'vue';
 import { Chart } from 'chart.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
@@ -75,7 +75,7 @@ async function renderChart() {
 
 	await nextTick();
 
-	const color = defaultStore.state.darkMode ? '#b4e900' : '#86b300';
+	const color = store.s.darkMode ? '#b4e900' : '#86b300';
 
 	const getYYYYMMDD = (date: Date) => {
 		const y = date.getFullYear().toString().padStart(2, '0');
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index d41793b0fa..9e03be3e7f 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -11,12 +11,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
 import tinycolor from 'tinycolor2';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { store } from '@/store.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 initChart();
 
@@ -42,7 +42,7 @@ const getDate = (ymd: string) => {
 onMounted(async () => {
 	let raw = await misskeyApi('retention', { });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
 	const color = accent.toHex();
diff --git a/packages/frontend/src/components/MkRoleSelectDialog.vue b/packages/frontend/src/components/MkRoleSelectDialog.vue
index 32f35ed5ad..229fd9d1ba 100644
--- a/packages/frontend/src/components/MkRoleSelectDialog.vue
+++ b/packages/frontend/src/components/MkRoleSelectDialog.vue
@@ -49,7 +49,7 @@ import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkRolePreview from '@/components/MkRolePreview.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import * as os from '@/os.js';
 import MkSpacer from '@/components/global/MkSpacer.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue
index e98ac9cfd2..aacd1eae2a 100644
--- a/packages/frontend/src/components/MkSignin.input.vue
+++ b/packages/frontend/src/components/MkSignin.input.vue
@@ -58,7 +58,7 @@ import { toUnicode } from 'punycode.js';
 
 import { query, extractDomain } from '@@/js/url.js';
 import { host as configHost } from '@@/js/config.js';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index ac2fba22f8..38186bd7b7 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -69,9 +69,9 @@ 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 '@/scripts/please-login.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
+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';
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index 676a336ec7..a970d1ce00 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
 import { shallowRef } from 'vue';
-import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
+import type { OpenOnRemoteOptions } from '@/utility/please-login.js';
 import MkSignin from '@/components/MkSignin.vue';
 import MkModal from '@/components/MkModal.vue';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 5d1cd5312f..ebb362fd6a 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -93,7 +93,7 @@ import MkTextarea from './MkTextarea.vue';
 import MkCaptcha from '@/components/MkCaptcha.vue';
 import type { Captcha } from '@/components/MkCaptcha.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { login } from '@/account.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index d8dec3aa2f..fddf3934bb 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -6,16 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div ref="rootEl" class="rrevdjwu" :class="{ grid }">
 	<MkInput
-		v-model="search"
+		v-if="searchIndex && searchIndex.length > 0"
+		v-model="searchQuery"
 		:placeholder="i18n.ts.search"
 		type="search"
 		style="margin-bottom: 16px;"
+		@input.passive="searchOnInput"
 		@keydown="searchOnKeyDown"
 	>
 		<template #prefix><i class="ti ti-search"></i></template>
 	</MkInput>
 
-	<template v-if="search == ''">
+	<template v-if="rawSearchQuery == ''">
 		<div v-for="group in def" class="group">
 			<div v-if="group.title" class="title">{{ group.title }}</div>
 
@@ -92,22 +94,27 @@ export type SuperMenuDef = {
 
 <script lang="ts" setup>
 import { useTemplateRef, ref, watch, nextTick } from 'vue';
-import type { SearchIndexItem } from '@/scripts/autogen/settings-search-index.js';
+import type { SearchIndexItem } from '@/utility/autogen/settings-search-index.js';
 import MkInput from '@/components/MkInput.vue';
 import { i18n } from '@/i18n.js';
 import { getScrollContainer } from '@@/js/scroll.js';
 import { useRouter } from '@/router/supplier.js';
+import { initIntlString, compareStringIncludes } from '@/utility/intl-string.js';
 
 const props = defineProps<{
 	def: SuperMenuDef[];
 	grid?: boolean;
-	searchIndex: SearchIndexItem[];
+	searchIndex?: SearchIndexItem[];
 }>();
 
+initIntlString();
+
 const router = useRouter();
 const rootEl = useTemplateRef('rootEl');
 
-const search = ref('');
+const searchQuery = ref('');
+const rawSearchQuery = ref('');
+
 const searchSelectedIndex = ref<null | number>(null);
 const searchResult = ref<{
 	id: string;
@@ -118,7 +125,11 @@ const searchResult = ref<{
 	parentLabels: string[];
 }[]>([]);
 
-watch(search, (value) => {
+watch(searchQuery, (value) => {
+	rawSearchQuery.value = value;
+});
+
+watch(rawSearchQuery, (value) => {
 	searchResult.value = [];
 	searchSelectedIndex.value = null;
 
@@ -128,14 +139,15 @@ watch(search, (value) => {
 
 	const dive = (items: SearchIndexItem[], parents: SearchIndexItem[] = []) => {
 		for (const item of items) {
-			const matched =
-				item.label.includes(value.toLowerCase()) ||
-				item.keywords.some((x) => x.toLowerCase().includes(value.toLowerCase()));
+			const matched = (
+				compareStringIncludes(item.label, value) ||
+				item.keywords.some((x) => compareStringIncludes(x, value))
+			);
 
 			if (matched) {
 				searchResult.value.push({
 					id: item.id,
-					path: item.path ?? parents.find((x) => x.path != null)?.path,
+					path: item.path ?? parents.find((x) => x.path != null)?.path ?? '/', // never gets `/`
 					label: item.label,
 					parentLabels: parents.map((x) => x.label).toReversed(),
 					icon: item.icon ?? parents.find((x) => x.icon != null)?.icon,
@@ -149,9 +161,16 @@ watch(search, (value) => {
 		}
 	};
 
-	dive(props.searchIndex);
+	if (props.searchIndex) {
+		dive(props.searchIndex);
+	}
 });
 
+function searchOnInput(ev: InputEvent) {
+	searchSelectedIndex.value = null;
+	rawSearchQuery.value = (ev.target as HTMLInputElement).value;
+}
+
 function searchOnKeyDown(ev: KeyboardEvent) {
 	if (ev.isComposing) return;
 
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index 7e92726dcb..2e1a96c326 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -103,7 +103,7 @@ import type {
 } from '@/components/MkSystemWebhookEditor.impl.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index 3e8588018c..eb47a8b858 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -40,8 +40,8 @@ import { onMounted, onUnmounted, nextTick, ref, watch, computed, toRefs, shallow
 import { debounce } from 'throttle-debounce';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { Autocomplete } from '@/scripts/autocomplete.js';
-import type { SuggestionType } from '@/scripts/autocomplete.js';
+import { Autocomplete } from '@/utility/autocomplete.js';
+import type { SuggestionType } from '@/utility/autocomplete.js';
 
 const props = defineProps<{
 	modelValue: string | null;
diff --git a/packages/frontend/src/components/MkThemePreview.vue b/packages/frontend/src/components/MkThemePreview.vue
new file mode 100644
index 0000000000..5b180b3680
--- /dev/null
+++ b/packages/frontend/src/components/MkThemePreview.vue
@@ -0,0 +1,96 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<svg
+	version="1.1"
+	viewBox="0 0 203.2 152.4"
+	xmlns="http://www.w3.org/2000/svg"
+	xmlns:xlink="http://www.w3.org/1999/xlink"
+>
+	<g fill-rule="evenodd">
+		<rect width="203.2" height="152.4" :fill="themeVariables.bg" stroke-width=".26458" />
+		<rect width="65.498" height="152.4" :fill="themeVariables.panel" stroke-width=".26458" />
+		<rect x="65.498" width="137.7" height="40.892" :fill="themeVariables.acrylicBg" stroke-width=".265" />
+		<path transform="scale(.26458)" d="m439.77 247.19c-43.673 0-78.832 35.157-78.832 78.83v249.98h407.06v-328.81z" :fill="themeVariables.panel" />
+	</g>
+	<circle cx="32.749" cy="83.054" r="21.132" :fill="themeVariables.accentedBg" stroke-dasharray="0.319256, 0.319256" stroke-width=".15963" style="paint-order:stroke fill markers" />
+	<circle cx="136.67" cy="106.76" r="23.876" :fill="themeVariables.fg" fill-opacity="0.5" stroke-dasharray="0.352425, 0.352425" stroke-width=".17621" style="paint-order:stroke fill markers" />
+	<g :fill="themeVariables.fg" fill-rule="evenodd" stroke-width=".26458">
+		<rect x="171.27" y="87.815" width="48.576" height="6.8747" ry="3.4373"/>
+		<rect x="171.27" y="105.09" width="48.576" height="6.875" ry="3.4375"/>
+		<rect x="171.27" y="121.28" width="48.576" height="6.875" ry="3.4375"/>
+		<rect x="171.27" y="137.47" width="48.576" height="6.875" ry="3.4375"/>
+	</g>
+	<path d="m65.498 40.892h137.7" :stroke="themeVariables.divider" stroke-width="0.75" />
+	<g transform="matrix(.60823 0 0 .60823 25.45 75.755)" fill="none" :stroke="themeVariables.accent" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
+		<path d="m0 0h24v24h-24z" fill="none" stroke="none" />
+		<path d="m5 12h-2l9-9 9 9h-2" />
+		<path d="m5 12v7a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-7" />
+		<path d="m9 21v-6a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v6" />
+	</g>
+	<g transform="matrix(.61621 0 0 .61621 25.354 117.92)" fill="none" :stroke="themeVariables.fg" stroke-linecap="round" stroke-linejoin="round" stroke-width="2">
+		<path d="m0 0h24v24h-24z" fill="none" stroke="none" />
+		<path d="m10 5a2 2 0 1 1 4 0 7 7 0 0 1 4 6v3a4 4 0 0 0 2 3h-16a4 4 0 0 0 2-3v-3a7 7 0 0 1 4-6" />
+		<path d="m9 17v1a3 3 0 0 0 6 0v-1" />
+	</g>
+	<image x="20.948" y="18.388" width="23.602" height="23.602" image-rendering="optimizeSpeed" preserveAspectRatio="xMidYMid meet" v-bind="{ 'xlink:href': instance.iconUrl || '/favicon.ico' }" />
+</svg>
+</template>
+
+<script setup lang="ts">
+import { ref, watch } from 'vue';
+import { instance } from '@/instance.js';
+import { compile } from '@/theme.js';
+import type { Theme } from '@/theme.js';
+import { deepClone } from '@/utility/clone.js';
+import lightTheme from '@@/themes/_light.json5';
+import darkTheme from '@@/themes/_dark.json5';
+
+const props = defineProps<{
+	theme: Theme;
+}>();
+
+const themeVariables = ref<{
+	bg: string;
+	acrylicBg: string;
+	panel: string;
+	fg: string;
+	divider: string;
+	accent: string;
+	accentedBg: string;
+}>({
+	bg: 'var(--MI_THEME-bg)',
+	acrylicBg: 'var(--MI_THEME-acrylicBg)',
+	panel: 'var(--MI_THEME-panel)',
+	fg: 'var(--MI_THEME-fg)',
+	divider: 'var(--MI_THEME-divider)',
+	accent: 'var(--MI_THEME-accent)',
+	accentedBg: 'var(--MI_THEME-accentedBg)',
+});
+
+watch(() => props.theme, (theme) => {
+	if (theme == null) return;
+
+	const _theme = deepClone(theme);
+
+	if (_theme?.base != null) {
+		const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
+		if (base) _theme.props = Object.assign({}, base.props, _theme.props);
+	}
+
+	const compiled = compile(_theme);
+
+	themeVariables.value = {
+		bg: compiled.bg ?? 'var(--MI_THEME-bg)',
+		acrylicBg: compiled.acrylicBg ?? 'var(--MI_THEME-acrylicBg)',
+		panel: compiled.panel ?? 'var(--MI_THEME-panel)',
+		fg: compiled.fg ?? 'var(--MI_THEME-fg)',
+		divider: compiled.divider ?? 'var(--MI_THEME-divider)',
+		accent: compiled.accent ?? 'var(--MI_THEME-accent)',
+		accentedBg: compiled.accentedBg ?? 'var(--MI_THEME-accentedBg)',
+	};
+}, { immediate: true });
+</script>
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index bcb522e334..7014239c39 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		v-if="paginationQuery"
 		ref="tlComponent"
 		:pagination="paginationQuery"
-		:noGap="!defaultStore.state.showGapBetweenNotesInTimeline"
+		:noGap="!prefer.s.showGapBetweenNotesInTimeline"
 		@queue="emit('queue', $event)"
 		@status="prComponent?.setDisabled($event)"
 	/>
@@ -24,10 +24,10 @@ import type { Paging } from '@/components/MkPagination.vue';
 import MkNotes from '@/components/MkNotes.vue';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import { useStream } from '@/stream.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 import { $i } from '@/account.js';
 import { instance } from '@/instance.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	src: BasicTimelineType | 'mentions' | 'directs' | 'list' | 'antenna' | 'channel' | 'role';
@@ -245,7 +245,7 @@ function updatePaginationQuery(untilDate?: Date) {
 }
 
 function refreshEndpointAndChannel() {
-	if (!defaultStore.state.disableStreamingTimeline) {
+	if (!prefer.s.disableStreamingTimeline) {
 		disconnectChannel();
 		connectChannel();
 	}
diff --git a/packages/frontend/src/components/MkToast.vue b/packages/frontend/src/components/MkToast.vue
index e256640649..ac795e312c 100644
--- a/packages/frontend/src/components/MkToast.vue
+++ b/packages/frontend/src/components/MkToast.vue
@@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div>
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_toast_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_toast_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_toast_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_toast_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_toast_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_toast_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_toast_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_toast_leaveTo : ''"
 		appear @afterLeave="emit('closed')"
 	>
 		<div v-if="showing" class="_acrylic" :class="$style.root" :style="{ zIndex }">
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 defineProps<{
 	message: string;
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index 73aef68964..31ecb15ab8 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -79,8 +79,8 @@ const adminPermissions = Misskey.permissions.filter(p => p.startsWith('read:admi
 
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 const name = ref(props.initialName);
-const permissionSwitches = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
-const permissionSwitchesForAdmin = ref(<Record<(typeof Misskey.permissions)[number], boolean>>{});
+const permissionSwitches = ref({} as Record<(typeof Misskey.permissions)[number], boolean>);
+const permissionSwitchesForAdmin = ref({} as Record<(typeof Misskey.permissions)[number], boolean>);
 
 if (props.initialPermissions) {
 	for (const kind of props.initialPermissions) {
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index 10365d29b1..8031abd563 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <Transition
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_tooltip_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_tooltip_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_tooltip_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_tooltip_leaveTo : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_tooltip_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_tooltip_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_tooltip_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_tooltip_leaveTo : ''"
 	appear @afterLeave="emit('closed')"
 >
 	<div v-show="showing" ref="el" :class="$style.root" class="_acrylic _shadow" :style="{ zIndex, maxWidth: maxWidth + 'px' }">
@@ -25,8 +25,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { nextTick, onMounted, onUnmounted, shallowRef } from 'vue';
 import * as os from '@/os.js';
-import { calcPopupPosition } from '@/scripts/popup-position.js';
-import { defaultStore } from '@/store.js';
+import { calcPopupPosition } from '@/utility/popup-position.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	showing: boolean;
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 11d7c8dc4d..5908db33c4 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -159,7 +159,7 @@ import MkAnimBg from '@/components/MkAnimBg.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { host } from '@@/js/config.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import * as os from '@/os.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index e874cffa96..1f311f41d9 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -9,19 +9,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.title"><MkSparkle>{{ i18n.ts.misskeyUpdated }}</MkSparkle></div>
 		<div :class="$style.version">✨{{ version }}🚀</div>
 		<MkButton full @click="whatIsNew">{{ i18n.ts.whatIsNew }}</MkButton>
-		<MkButton :class="$style.gotIt" primary full @click="modal?.close()">{{ i18n.ts.gotIt }}</MkButton>
+		<MkButton :class="$style.gotIt" primary full @click.stop="modal?.close()">{{ i18n.ts.gotIt }}</MkButton>
 	</div>
 </MkModal>
 </template>
 
 <script lang="ts" setup>
 import { onMounted, shallowRef } from 'vue';
+import { version } from '@@/js/config.js';
 import MkModal from '@/components/MkModal.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkSparkle from '@/components/MkSparkle.vue';
-import { version } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { confetti } from '@/scripts/confetti.js';
+import { confetti } from '@/utility/confetti.js';
 import { useRouter } from '@/router/supplier';
 
 const modal = shallowRef<InstanceType<typeof MkModal>>();
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 7265e438d0..fa422687c0 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -34,7 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			sandbox="allow-popups allow-popups-to-escape-sandbox allow-scripts allow-same-origin"
 			scrolling="no"
 			:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
-			:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"
+			:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${store.s.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"
 		></iframe>
 	</div>
 	<div :class="$style.action">
@@ -53,7 +53,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 <div v-else>
 	<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
-		<div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="defaultStore.state.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`">
+		<div v-if="thumbnail && !sensitive" :class="$style.thumbnail" :style="prefer.s.dataSaver.urlPreview ? '' : `background-image: url('${thumbnail}')`">
 		</div>
 		<article :class="$style.body">
 			<header :class="$style.header">
@@ -105,10 +105,11 @@ import type { summaly } from '@misskey-dev/summaly';
 import { misskeyApi } from '@/scripts/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import MkButton from '@/components/MkButton.vue';
-import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
-import { defaultStore } from '@/store.js';
+import { transformPlayerUrl } from '@/utility/player-url-transform.js';
+import { store } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 type SummalyResult = Awaited<ReturnType<typeof summaly>>;
 
diff --git a/packages/frontend/src/components/MkUrlPreviewPopup.vue b/packages/frontend/src/components/MkUrlPreviewPopup.vue
index e972973dba..fd36d6a82b 100644
--- a/packages/frontend/src/components/MkUrlPreviewPopup.vue
+++ b/packages/frontend/src/components/MkUrlPreviewPopup.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
-	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')">
+	<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')">
 		<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url" :showActions="false"/>
 	</Transition>
 </div>
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, ref } from 'vue';
 import MkUrlPreview from '@/components/MkUrlPreview.vue';
 import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	showing: boolean;
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 34991fa0dd..34e86444ad 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -56,7 +56,7 @@ import MkModalWindow from '@/components/MkModalWindow.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue
index 7a2e878931..dde2efd8ee 100644
--- a/packages/frontend/src/components/MkUserCardMini.vue
+++ b/packages/frontend/src/components/MkUserCardMini.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { onMounted, ref } from 'vue';
 import MkMiniChart from '@/components/MkMiniChart.vue';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { acct } from '@/filters/user.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index 0164515a8a..eb189b446b 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div class="_panel" :class="$style.root">
-	<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div>
+	<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''"></div>
 	<MkAvatar :class="$style.avatar" :user="user" indicator/>
 	<div :class="$style.title">
 		<MkA :class="$style.name" :to="userPage(user)"><MkUserName :user="user" :nowrap="false"/></MkA>
@@ -40,9 +40,9 @@ import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
-import { defaultStore } from '@/store.js';
+import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
+import { prefer } from '@/preferences.js';
 
 defineProps<{
 	user: Misskey.entities.UserDetailed;
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index b8470870a3..1d0ff1b6ce 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -5,19 +5,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <Transition
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_popup_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_popup_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_popup_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_popup_leaveTo : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_popup_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_popup_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_popup_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_popup_leaveTo : ''"
 	appear @afterLeave="emit('closed')"
 >
 	<div v-if="showing" :class="$style.root" class="_popup _shadow" :style="{ zIndex, top: top + 'px', left: left + 'px' }" @mouseover="() => { emit('mouseover'); }" @mouseleave="() => { emit('mouseleave'); }">
 		<div v-if="user != null">
-			<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''">
-				<span v-if="$i && $i.id != user.id && user.isFollowed && user.isFollowing" :class="$style.followed">{{ i18n.ts.mutuals }}</span>
-				<span v-else-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
-				<span v-else-if="$i && $i.id != user.id && user.isFollowing" :class="$style.followed">{{ i18n.ts.following }}</span>
-				<span v-if="user.isLocked && $i && $i.id != user.id && !user.isFollowing" :title="i18n.ts.isLocked" :class="$style.locked"><i class="ph-lock ph-bold ph-lg"></i></span>
+			<div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${prefer.s.disableShowingAnimatedImages ? getStaticImageUrl(user.bannerUrl) : user.bannerUrl})` : ''">
+				<span v-if="$i && $i.id != user.id && user.isFollowed" :class="$style.followed">{{ i18n.ts.followsYou }}</span>
 			</div>
 			<svg viewBox="0 0 128 128" :class="$style.avatarBack">
 				<g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)">
@@ -63,14 +60,14 @@ import * as Misskey from 'misskey-js';
 import MkFollowButton from '@/components/MkFollowButton.vue';
 import { userPage } from '@/filters/user.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { getUserMenu } from '@/scripts/get-user-menu.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { getUserMenu } from '@/utility/get-user-menu.js';
 import number from '@/filters/number.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { $i } from '@/account.js';
-import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
 
 const props = defineProps<{
 	showing: boolean;
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index 1e93d9dbea..e5c6df267b 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -67,8 +67,8 @@ import { host as currentHost, hostname } from '@@/js/config.js';
 import MkInput from '@/components/MkInput.vue';
 import FormSplit from '@/components/form/split.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import { instance } from '@/instance.js';
@@ -128,10 +128,10 @@ async function ok() {
 	dialogEl.value?.close();
 
 	// 最近使ったユーザー更新
-	let recents = defaultStore.state.recentlyUsedUsers;
+	let recents = store.s.recentlyUsedUsers;
 	recents = recents.filter(x => x !== selected.value?.id);
 	recents.unshift(selected.value.id);
-	defaultStore.set('recentlyUsedUsers', recents.splice(0, 16));
+	store.set('recentlyUsedUsers', recents.splice(0, 16));
 }
 
 function cancel() {
@@ -141,7 +141,7 @@ function cancel() {
 
 onMounted(() => {
 	misskeyApi('users/show', {
-		userIds: defaultStore.state.recentlyUsedUsers,
+		userIds: store.s.recentlyUsedUsers,
 	}).then(foundUsers => {
 		let _users = foundUsers;
 		_users = _users.filter((u) => {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
index 62e5d1da8a..629bc30667 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Privacy.vue
@@ -49,7 +49,7 @@ import { i18n } from '@/i18n.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const isLocked = ref(false);
 const hideOnlineStatus = ref(false);
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 7cb48f6afb..14acfd3f89 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -37,7 +37,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import FormSlot from '@/components/form/slot.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { chooseFileFromPc } from '@/scripts/select-file.js';
+import { chooseFileFromPc } from '@/utility/select-file.js';
 import * as os from '@/os.js';
 import { signinRequired } from '@/account.js';
 
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue
index 004edab630..4accc6183b 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.User.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue
@@ -29,7 +29,7 @@ import * as Misskey from 'misskey-js';
 import { ref } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const props = defineProps<{
 	user: Misskey.entities.UserDetailed;
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index b7261129ef..eb3a69217e 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -139,7 +139,7 @@ import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { host } from '@@/js/config.js';
 import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import * as os from '@/os.js';
 
 const emit = defineEmits<{
@@ -149,10 +149,10 @@ const emit = defineEmits<{
 const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
 
 // eslint-disable-next-line vue/no-setup-props-reactivity-loss
-const page = ref(defaultStore.state.accountSetupWizard);
+const page = ref(store.s.accountSetupWizard);
 
 watch(page, () => {
-	defaultStore.set('accountSetupWizard', page.value);
+	store.set('accountSetupWizard', page.value);
 });
 
 async function close(skip: boolean) {
@@ -165,11 +165,11 @@ async function close(skip: boolean) {
 	}
 
 	dialog.value?.close();
-	defaultStore.set('accountSetupWizard', -1);
+	store.set('accountSetupWizard', -1);
 }
 
 function setupComplete() {
-	defaultStore.set('accountSetupWizard', -1);
+	store.set('accountSetupWizard', -1);
 	dialog.value?.close();
 }
 
@@ -194,7 +194,7 @@ async function later(later: boolean) {
 	}
 
 	dialog.value?.close();
-	defaultStore.set('accountSetupWizard', 0);
+	store.set('accountSetupWizard', 0);
 }
 </script>
 
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index d098dad9a1..872d4201bb 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -17,11 +17,11 @@ import { onMounted, shallowRef, ref, nextTick } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
 import tinycolor from 'tinycolor2';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
@@ -59,7 +59,7 @@ async function renderChart() {
 
 	await nextTick();
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const computedStyle = getComputedStyle(document.documentElement);
 	const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 4f98e3ca8f..650221145b 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -65,7 +65,7 @@ import MkTimeline from '@/components/MkTimeline.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { instanceName } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import MkNumber from '@/components/MkNumber.vue';
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 2953f656d4..4b3c728ee4 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -5,10 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <Transition
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_window_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_window_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_window_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_window_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_window_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_window_leaveTo : ''"
 	appear
 	@afterLeave="emit('closed')"
 >
@@ -55,10 +55,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
-import contains from '@/scripts/contains.js';
+import contains from '@/utility/contains.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 type WindowButton = {
 	title: string;
diff --git a/packages/frontend/src/components/MkYouTubePlayer.vue b/packages/frontend/src/components/MkYouTubePlayer.vue
index 1122976436..ab62a5113d 100644
--- a/packages/frontend/src/components/MkYouTubePlayer.vue
+++ b/packages/frontend/src/components/MkYouTubePlayer.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</template>
 
 	<div class="poamfof">
-		<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
+		<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
 			<div v-if="player.url && (player.url.startsWith('http://') || player.url.startsWith('https://'))" class="player">
 				<iframe v-if="!fetching" :src="transformPlayerUrl(player.url)" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>
 			</div>
@@ -25,10 +25,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
-import MkWindow from '@/components/MkWindow.vue';
 import { versatileLang } from '@@/js/intl-const.js';
-import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
-import { defaultStore } from '@/store.js';
+import MkWindow from '@/components/MkWindow.vue';
+import { transformPlayerUrl } from '@/utility/player-url-transform.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	url: string;
diff --git a/packages/frontend/src/components/global/MkA.stories.impl.ts b/packages/frontend/src/components/global/MkA.stories.impl.ts
index 1ccf105dbb..deb2b8a52b 100644
--- a/packages/frontend/src/components/global/MkA.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkA.stories.impl.ts
@@ -7,7 +7,7 @@
 import { expect, userEvent, within } from '@storybook/test';
 import type { StoryObj } from '@storybook/vue3';
 import MkA from './MkA.vue';
-import { tick } from '@/scripts/test-utils.js';
+import { tick } from '@/utility/test-utils.js';
 export const Default = {
 	render(args) {
 		return {
diff --git a/packages/frontend/src/components/global/MkA.vue b/packages/frontend/src/components/global/MkA.vue
index 8eacf16d6d..173f6a849f 100644
--- a/packages/frontend/src/components/global/MkA.vue
+++ b/packages/frontend/src/components/global/MkA.vue
@@ -17,7 +17,7 @@ export type MkABehavior = 'window' | 'browser' | null;
 import { computed, inject, shallowRef } from 'vue';
 import { url } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 08a78c8d81..c196519c15 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -45,9 +45,10 @@ import { url as local, host } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import MkButton from '@/components/MkButton.vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import * as os from '@/os.js';
 import { $i } from '@/account.js';
+import { prefer } from '@/preferences.js';
 
 type Ad = (typeof instance)['ads'][number];
 
@@ -66,7 +67,7 @@ const choseAd = (): Ad | null => {
 		return props.specify;
 	}
 
-	const allAds = instance.ads.map(ad => defaultStore.state.mutedAds.includes(ad.id) ? {
+	const allAds = instance.ads.map(ad => store.s.mutedAds.includes(ad.id) ? {
 		...ad,
 		ratio: 0,
 	} : ad);
@@ -107,12 +108,12 @@ const chosen = ref(choseAd());
 
 const self = computed(() => chosen.value?.url.startsWith(local));
 
-const shouldHide = ref(!defaultStore.state.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
+const shouldHide = ref(!prefer.s.forceShowAds && $i && $i.policies.canHideAds && (props.specify == null));
 
 function reduceFrequency(): void {
 	if (chosen.value == null) return;
-	if (defaultStore.state.mutedAds.includes(chosen.value.id)) return;
-	defaultStore.push('mutedAds', chosen.value.id);
+	if (store.s.mutedAds.includes(chosen.value.id)) return;
+	store.push('mutedAds', chosen.value.id);
 	os.success();
 	chosen.value = choseAd();
 	showMenu.value = false;
diff --git a/packages/frontend/src/components/global/MkAvatar.vue b/packages/frontend/src/components/global/MkAvatar.vue
index 28cd1771d4..971ab18235 100644
--- a/packages/frontend/src/components/global/MkAvatar.vue
+++ b/packages/frontend/src/components/global/MkAvatar.vue
@@ -45,14 +45,13 @@ import * as Misskey from 'misskey-js';
 import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurhash.js';
 import MkImgWithBlurhash from '../MkImgWithBlurhash.vue';
 import MkA from './MkA.vue';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import { acct, userPage } from '@/filters/user.js';
 import MkUserOnlineIndicator from '@/components/MkUserOnlineIndicator.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
-const animation = ref(defaultStore.state.animation);
-const squareAvatars = ref(defaultStore.state.squareAvatars);
-const useBlurEffect = ref(defaultStore.state.useBlurEffect);
+const animation = ref(prefer.s.animation);
+const squareAvatars = ref(prefer.s.squareAvatars);
 
 const props = withDefaults(defineProps<{
 	user: Misskey.entities.User;
@@ -75,7 +74,7 @@ const emit = defineEmits<{
 	(ev: 'click', v: MouseEvent): void;
 }>();
 
-const showDecoration = props.forceShowDecoration || defaultStore.state.showAvatarDecorations;
+const showDecoration = props.forceShowDecoration || prefer.s.showAvatarDecorations;
 
 const bound = computed(() => props.link
 	? { to: userPage(props.user), target: props.target }
@@ -83,7 +82,7 @@ const bound = computed(() => props.link
 
 const url = computed(() => {
 	if (props.user.avatarUrl == null) return null;
-	if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl);
+	if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(props.user.avatarUrl);
 	return props.user.avatarUrl;
 });
 
@@ -93,7 +92,7 @@ function onClick(ev: MouseEvent): void {
 }
 
 function getDecorationUrl(decoration: Omit<Misskey.entities.UserDetailed['avatarDecorations'][number], 'id'>) {
-	if (defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar) return getStaticImageUrl(decoration.url);
+	if (prefer.s.disableShowingAnimatedImages || prefer.s.dataSaver.avatar) return getStaticImageUrl(decoration.url);
 	return decoration.url;
 }
 
diff --git a/packages/frontend/src/components/global/MkCustomEmoji.vue b/packages/frontend/src/components/global/MkCustomEmoji.vue
index 216cdc19e5..271db7963e 100644
--- a/packages/frontend/src/components/global/MkCustomEmoji.vue
+++ b/packages/frontend/src/components/global/MkCustomEmoji.vue
@@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, inject, ref } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
-import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
-import { defaultStore } from '@/store.js';
+import { getProxiedImageUrl, getStaticImageUrl } from '@/utility/media-proxy.js';
 import { customEmojisMap } from '@/custom-emojis.js';
 import * as os from '@/os.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import * as sound from '@/scripts/sound.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
+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 { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	name: string;
@@ -77,7 +77,7 @@ const url = computed(() => {
 				false,
 				true,
 			);
-	return defaultStore.reactiveState.disableShowingAnimatedImages.value
+	return prefer.s.disableShowingAnimatedImages
 		? getStaticImageUrl(proxied)
 		: proxied;
 });
diff --git a/packages/frontend/src/components/global/MkEmoji.vue b/packages/frontend/src/components/global/MkEmoji.vue
index 8144ef02dc..9572bc6f42 100644
--- a/packages/frontend/src/components/global/MkEmoji.vue
+++ b/packages/frontend/src/components/global/MkEmoji.vue
@@ -12,12 +12,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, inject } from 'vue';
 import { colorizeEmoji, getEmojiName } from '@@/js/emojilist.js';
 import { char2fluentEmojiFilePath, char2twemojiFilePath } from '@@/js/emoji-base.js';
-import { defaultStore } from '@/store.js';
-import * as os from '@/os.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import * as sound from '@/scripts/sound.js';
-import { i18n } from '@/i18n.js';
 import type { MenuItem } from '@/types/menu.js';
+import * as os from '@/os.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import * as sound from '@/utility/sound.js';
+import { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	emoji: string;
@@ -27,9 +27,9 @@ const props = defineProps<{
 
 const react = inject<((name: string) => void) | null>('react', null);
 
-const char2path = defaultStore.state.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
+const char2path = prefer.s.emojiStyle === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 
-const useOsNativeEmojis = computed(() => defaultStore.state.emojiStyle === 'native');
+const useOsNativeEmojis = computed(() => prefer.s.emojiStyle === 'native');
 const url = computed(() => char2path(props.emoji));
 const colorizedNativeEmoji = computed(() => colorizeEmoji(props.emoji));
 
diff --git a/packages/frontend/src/components/global/MkError.vue b/packages/frontend/src/components/global/MkError.vue
index c594cc752b..b07e0775a3 100644
--- a/packages/frontend/src/components/global/MkError.vue
+++ b/packages/frontend/src/components/global/MkError.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear>
+<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
 	<div :class="$style.root">
 		<img :class="$style.img" :src="serverErrorImageUrl" class="_ghost"/>
 		<p :class="$style.text"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</p>
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { serverErrorImageUrl } from '@/instance.js';
 
 const emit = defineEmits<{
diff --git a/packages/frontend/src/components/global/MkFooterSpacer.vue b/packages/frontend/src/components/global/MkFooterSpacer.vue
index 1a75855fa1..45b415a7f6 100644
--- a/packages/frontend/src/components/global/MkFooterSpacer.vue
+++ b/packages/frontend/src/components/global/MkFooterSpacer.vue
@@ -4,11 +4,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div :class="[$style.spacer, defaultStore.reactiveState.darkMode.value ? $style.dark : $style.light]"></div>
+<div :class="[$style.spacer, store.r.darkMode.value ? $style.dark : $style.light]"></div>
 </template>
 
 <script lang="ts" setup>
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts
index 845a56377d..3fff2060e9 100644
--- a/packages/frontend/src/components/global/MkMfm.ts
+++ b/packages/frontend/src/components/global/MkMfm.ts
@@ -3,11 +3,12 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { h, provide } from 'vue';
-import type { VNode, SetupContext } from 'vue';
+import { h } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
 import { host } from '@@/js/config.js';
+import type { VNode, SetupContext } from 'vue';
+import type { MkABehavior } from '@/components/global/MkA.vue';
 import MkUrl from '@/components/global/MkUrl.vue';
 import MkTime from '@/components/global/MkTime.vue';
 import MkLink from '@/components/MkLink.vue';
@@ -19,8 +20,7 @@ import MkCodeInline from '@/components/MkCodeInline.vue';
 import MkGoogle from '@/components/MkGoogle.vue';
 import MkSparkle from '@/components/MkSparkle.vue';
 import MkA from '@/components/global/MkA.vue';
-import type { MkABehavior } from '@/components/global/MkA.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 function safeParseFloat(str: unknown): number | null {
 	if (typeof str !== 'string' || str === '') return null;
@@ -81,7 +81,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 		return c.match(/^[0-9a-f]{3,6}$/i) ? c : null;
 	};
 
-	const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
+	const useAnim = prefer.s.advancedMfm && prefer.s.animatedMfm;
 
 	/**
 	 * Gen Vue Elements from MFM AST
@@ -188,17 +188,17 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 					}
 					case 'x2': {
 						return h('span', {
-							class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
+							class: prefer.s.advancedMfm ? 'mfm-x2' : '',
 						}, genEl(token.children, scale * 2));
 					}
 					case 'x3': {
 						return h('span', {
-							class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
+							class: prefer.s.advancedMfm ? 'mfm-x3' : '',
 						}, genEl(token.children, scale * 3));
 					}
 					case 'x4': {
 						return h('span', {
-							class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
+							class: prefer.s.advancedMfm ? 'mfm-x4' : '',
 						}, genEl(token.children, scale * 4));
 					}
 					case 'font': {
@@ -241,14 +241,14 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
 						break;
 					}
 					case 'position': {
-						if (!defaultStore.state.advancedMfm) break;
+						if (!prefer.s.advancedMfm) break;
 						const x = safeParseFloat(token.props.args.x) ?? 0;
 						const y = safeParseFloat(token.props.args.y) ?? 0;
 						style = `transform: translateX(${x}em) translateY(${y}em);`;
 						break;
 					}
 					case 'scale': {
-						if (!defaultStore.state.advancedMfm) {
+						if (!prefer.s.advancedMfm) {
 							style = '';
 							break;
 						}
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index e48fa67671..8a5e556293 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -7,14 +7,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 <div ref="el" :class="$style.tabs" @wheel="onTabWheel">
 	<div :class="$style.tabsInner">
 		<button
-			v-for="t in tabs" :key="t.key" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
-			class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: defaultStore.reactiveState.animation.value }]"
+			v-for="t in tabs" :ref="(el) => tabRefs[t.key] = (el as HTMLElement)" v-tooltip.noDelay="t.title"
+			class="_button" :class="[$style.tab, { [$style.active]: t.key != null && t.key === props.tab, [$style.animate]: prefer.s.animation }]"
 			@mousedown="(ev) => onTabMousedown(t, ev)" @click="(ev) => onTabClick(t, ev)"
 		>
 			<div :class="$style.tabInner">
 				<i v-if="t.icon" :class="[$style.tabIcon, t.icon]"></i>
 				<div
-					v-if="!t.iconOnly || (!defaultStore.reactiveState.animation.value && t.key === tab)"
+					v-if="!t.iconOnly || (!prefer.s.animation && t.key === tab)"
 					:class="$style.tabTitle"
 				>
 					{{ t.title }}
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 	<div
 		ref="tabHighlightEl"
-		:class="[$style.tabHighlight, { [$style.animate]: defaultStore.reactiveState.animation.value }]"
+		:class="[$style.tabHighlight, { [$style.animate]: prefer.s.animation }]"
 	></div>
 </div>
 </template>
@@ -41,20 +41,20 @@ export type Tab = {
 	onClick?: (ev: MouseEvent) => void;
 } & (
 	| {
-			iconOnly?: false;
-			title: string;
-			icon?: string;
-		}
+		iconOnly?: false;
+		title: string;
+		icon?: string;
+	}
 	| {
-			iconOnly: true;
-			icon: string;
-		}
+		iconOnly: true;
+		icon: string;
+	}
 );
 </script>
 
 <script lang="ts" setup>
 import { nextTick, onMounted, onUnmounted, shallowRef, watch } from 'vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	tabs?: Tab[];
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index 1070c0c83b..728e37cf51 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -43,14 +43,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, onUnmounted, ref, inject, shallowRef, computed } from 'vue';
 import tinycolor from 'tinycolor2';
+import { scrollToTop } from '@@/js/scroll.js';
 import XTabs from './MkPageHeader.tabs.vue';
 import type { Tab } from './MkPageHeader.tabs.vue';
-import { scrollToTop } from '@@/js/scroll.js';
-import { globalEvents } from '@/events.js';
-import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
-import { $i, openAccountMenu as openAccountMenu_ } from '@/account.js';
 import type { PageHeaderItem } from '@/types/page-header.js';
-import type { PageMetadata } from '@/scripts/page-metadata.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';
 
 const props = withDefaults(defineProps<{
 	overridePageMetadata?: PageMetadata;
@@ -114,7 +114,7 @@ let ro: ResizeObserver | null;
 
 onMounted(() => {
 	calcBg();
-	globalEvents.on('themeChanged', calcBg);
+	globalEvents.on('themeChanging', calcBg);
 
 	if (el.value && el.value.parentElement) {
 		narrow.value = el.value.parentElement.offsetWidth < 500;
@@ -128,7 +128,7 @@ onMounted(() => {
 });
 
 onUnmounted(() => {
-	globalEvents.off('themeChanged', calcBg);
+	globalEvents.off('themeChanging', calcBg);
 	if (ro) ro.disconnect();
 });
 </script>
diff --git a/packages/frontend/src/components/global/MkSpacer.vue b/packages/frontend/src/components/global/MkSpacer.vue
index db01c10eb0..6080bad9cd 100644
--- a/packages/frontend/src/components/global/MkSpacer.vue
+++ b/packages/frontend/src/components/global/MkSpacer.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { inject } from 'vue';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { deviceKind } from '@/utility/device-kind.js';
 
 const props = withDefaults(defineProps<{
 	contentMax?: number | null;
diff --git a/packages/frontend/src/components/global/MkUrl.vue b/packages/frontend/src/components/global/MkUrl.vue
index 351447b463..08dd85b728 100644
--- a/packages/frontend/src/components/global/MkUrl.vue
+++ b/packages/frontend/src/components/global/MkUrl.vue
@@ -30,7 +30,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 '@/scripts/use-tooltip.js';
+import { useTooltip } from '@/utility/use-tooltip.js';
 import { isEnabledUrlPreview } from '@/instance.js';
 import type { MkABehavior } from '@/components/global/MkA.vue';
 
diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 3ab3d10a40..25a29a4ae7 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <KeepAlive
-	:max="defaultStore.state.numberOfPageCache"
+	:max="prefer.s.numberOfPageCache"
 	:exclude="pageCacheController"
 >
 	<Suspense :timeout="0">
@@ -21,23 +21,24 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { inject, onBeforeUnmount, provide, ref, shallowRef, computed, nextTick } from 'vue';
 import type { IRouter, Resolved, RouteDef } from '@/nirax.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { globalEvents } from '@/events.js';
 import MkLoadingPage from '@/pages/_loading_.vue';
+import { DI } from '@/di.js';
 
 const props = defineProps<{
 	router?: IRouter;
 	nested?: boolean;
 }>();
 
-const router = props.router ?? inject('router');
+const router = props.router ?? inject(DI.router);
 
 if (router == null) {
 	throw new Error('no router provided');
 }
 
-const currentDepth = inject('routerCurrentDepth', 0);
-provide('routerCurrentDepth', currentDepth + 1);
+const currentDepth = inject(DI.routerCurrentDepth, 0);
+provide(DI.routerCurrentDepth, currentDepth + 1);
 
 function resolveNested(current: Resolved, d = 0): Resolved | null {
 	if (!props.nested) return current;
diff --git a/packages/frontend/src/components/global/SearchMarker.vue b/packages/frontend/src/components/global/SearchMarker.vue
index c5ec626cf4..66a78cb7fd 100644
--- a/packages/frontend/src/components/global/SearchMarker.vue
+++ b/packages/frontend/src/components/global/SearchMarker.vue
@@ -36,7 +36,7 @@ const rootEl = useTemplateRef('root');
 const rootElMutationObserver = new MutationObserver(() => {
 	checkChildren();
 });
-const injectedSearchMarkerId = inject<Ref<string | null>>('inAppSearchMarkerId');
+const injectedSearchMarkerId = inject<Ref<string | null> | null>('inAppSearchMarkerId', null);
 const searchMarkerId = computed(() => injectedSearchMarkerId?.value ?? window.location.hash.slice(1));
 const highlighted = ref(props.markerId === searchMarkerId.value);
 
diff --git a/packages/frontend/src/components/grid/MkDataCell.vue b/packages/frontend/src/components/grid/MkDataCell.vue
index c2dc05efe6..f813bcb73f 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 '@/scripts/use-tooltip.js';
+import { useTooltip } from '@/utility/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/components/grid/MkGrid.vue b/packages/frontend/src/components/grid/MkGrid.vue
index c89e23c135..94f4f3dab1 100644
--- a/packages/frontend/src/components/grid/MkGrid.vue
+++ b/packages/frontend/src/components/grid/MkGrid.vue
@@ -66,7 +66,7 @@ import {
 import * as os from '@/os.js';
 import { createColumn } from '@/components/grid/column.js';
 import { createRow, defaultGridRowSetting, resetRow } from '@/components/grid/row.js';
-import { handleKeyEvent } from '@/scripts/key-event.js';
+import { handleKeyEvent } from '@/utility/key-event.js';
 
 import type { DataSource, GridSetting, GridState, Size } from '@/components/grid/grid.js';
 import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
diff --git a/packages/frontend/src/components/grid/grid-utils.ts b/packages/frontend/src/components/grid/grid-utils.ts
index 4f48af194c..9e5402354e 100644
--- a/packages/frontend/src/components/grid/grid-utils.ts
+++ b/packages/frontend/src/components/grid/grid-utils.ts
@@ -10,7 +10,7 @@ import { CELL_ADDRESS_NONE } from '@/components/grid/cell.js';
 import type { CellAddress, CellValue, GridCell } from '@/components/grid/cell.js';
 import type { GridRow } from '@/components/grid/row.js';
 import type { GridContext } from '@/components/grid/grid-event.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import type { GridColumn, GridColumnSetting } from '@/components/grid/column.js';
 
 export function isCellElement(elem: HTMLElement): boolean {
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
index 84436e7adb..df26874c17 100644
--- a/packages/frontend/src/components/page/page.note.vue
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -15,7 +15,7 @@ import { onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkNote from '@/components/MkNote.vue';
 import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const props = defineProps<{
 	block: Misskey.entities.PageBlock,
diff --git a/packages/frontend/src/components/page/page.text.vue b/packages/frontend/src/components/page/page.text.vue
index e0c7956f6e..7702e250e4 100644
--- a/packages/frontend/src/components/page/page.text.vue
+++ b/packages/frontend/src/components/page/page.text.vue
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent } from 'vue';
 import * as mfm from 'mfm-js';
 import * as Misskey from 'misskey-js';
-import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
+import { extractUrlFromMfm } from '@/utility/extract-url-from-mfm.js';
 import { isEnabledUrlPreview } from '@/instance.js';
 
 const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts
index 95241fb8e8..c226b711a1 100644
--- a/packages/frontend/src/custom-emojis.ts
+++ b/packages/frontend/src/custom-emojis.ts
@@ -5,8 +5,8 @@
 
 import { shallowRef, computed, markRaw, watch } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { get, set } from '@/scripts/idb-proxy.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
+import { get, set } from '@/utility/idb-proxy.js';
 
 const CACHE_EXPIRE_TIME = 12 * 60 * 60 * 1000;
 const BATCH_SIZE = 1000;
diff --git a/packages/frontend/src/deck.ts b/packages/frontend/src/deck.ts
new file mode 100644
index 0000000000..f158db2f85
--- /dev/null
+++ b/packages/frontend/src/deck.ts
@@ -0,0 +1,306 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { throttle } from 'throttle-debounce';
+import { notificationTypes } from 'misskey-js';
+import type { BasicTimelineType } from '@/timelines.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { deepClone } from '@/utility/clone.js';
+import { store } from '@/store.js';
+
+type ColumnWidget = {
+	name: string;
+	id: string;
+	data: Record<string, any>;
+};
+
+export const columnTypes = [
+	'main',
+	'widgets',
+	'notifications',
+	'tl',
+	'antenna',
+	'list',
+	'channel',
+	'mentions',
+	'direct',
+	'roleTimeline',
+] as const;
+
+export type ColumnType = typeof columnTypes[number];
+
+export type Column = {
+	id: string;
+	type: ColumnType;
+	name: string | null;
+	width: number;
+	widgets?: ColumnWidget[];
+	active?: boolean;
+	flexible?: boolean;
+	antennaId?: string;
+	listId?: string;
+	channelId?: string;
+	roleId?: string;
+	excludeTypes?: typeof notificationTypes[number][];
+	tl?: BasicTimelineType;
+	withRenotes?: boolean;
+	withReplies?: boolean;
+	withSensitive?: boolean;
+	onlyFiles?: boolean;
+	soundSetting?: SoundStore;
+};
+
+export const loadDeck = async () => {
+	let deck;
+
+	try {
+		deck = await misskeyApi('i/registry/get', {
+			scope: ['client', 'deck', 'profiles'],
+			key: store.s['deck.profile'],
+		});
+	} catch (err) {
+		if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') {
+			// 後方互換性のため
+			if (store.s['deck.profile'] === 'default') {
+				saveDeck();
+				return;
+			}
+
+			store.set('deck.columns', []);
+			store.set('deck.layout', []);
+			return;
+		}
+		throw err;
+	}
+
+	store.set('deck.columns', deck.columns);
+	store.set('deck.layout', deck.layout);
+};
+
+export async function forceSaveDeck() {
+	await misskeyApi('i/registry/set', {
+		scope: ['client', 'deck', 'profiles'],
+		key: store.s['deck.profile'],
+		value: {
+			columns: store.r['deck.columns'].value,
+			layout: store.r['deck.layout'].value,
+		},
+	});
+}
+
+// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
+export const saveDeck = throttle(1000, () => {
+	forceSaveDeck();
+});
+
+export async function getProfiles(): Promise<string[]> {
+	return await misskeyApi('i/registry/keys', {
+		scope: ['client', 'deck', 'profiles'],
+	});
+}
+
+export async function deleteProfile(key: string): Promise<void> {
+	return await misskeyApi('i/registry/remove', {
+		scope: ['client', 'deck', 'profiles'],
+		key: key,
+	});
+}
+
+export function addColumn(column: Column) {
+	if (column.name === undefined) column.name = null;
+	store.push('deck.columns', column);
+	store.push('deck.layout', [column.id]);
+	saveDeck();
+}
+
+export function removeColumn(id: Column['id']) {
+	store.set('deck.columns', store.s['deck.columns'].filter(c => c.id !== id));
+	store.set('deck.layout', store.s['deck.layout']
+		.map(ids => ids.filter(_id => _id !== id))
+		.filter(ids => ids.length > 0));
+	saveDeck();
+}
+
+export function swapColumn(a: Column['id'], b: Column['id']) {
+	const aX = store.s['deck.layout'].findIndex(ids => ids.indexOf(a) !== -1);
+	const aY = store.s['deck.layout'][aX].findIndex(id => id === a);
+	const bX = store.s['deck.layout'].findIndex(ids => ids.indexOf(b) !== -1);
+	const bY = store.s['deck.layout'][bX].findIndex(id => id === b);
+	const layout = deepClone(store.s['deck.layout']);
+	layout[aX][aY] = b;
+	layout[bX][bY] = a;
+	store.set('deck.layout', layout);
+	saveDeck();
+}
+
+export function swapLeftColumn(id: Column['id']) {
+	const layout = deepClone(store.s['deck.layout']);
+	store.s['deck.layout'].some((ids, i) => {
+		if (ids.includes(id)) {
+			const left = store.s['deck.layout'][i - 1];
+			if (left) {
+				layout[i - 1] = store.s['deck.layout'][i];
+				layout[i] = left;
+				store.set('deck.layout', layout);
+			}
+			return true;
+		}
+		return false;
+	});
+	saveDeck();
+}
+
+export function swapRightColumn(id: Column['id']) {
+	const layout = deepClone(store.s['deck.layout']);
+	store.s['deck.layout'].some((ids, i) => {
+		if (ids.includes(id)) {
+			const right = store.s['deck.layout'][i + 1];
+			if (right) {
+				layout[i + 1] = store.s['deck.layout'][i];
+				layout[i] = right;
+				store.set('deck.layout', layout);
+			}
+			return true;
+		}
+		return false;
+	});
+	saveDeck();
+}
+
+export function swapUpColumn(id: Column['id']) {
+	const layout = deepClone(store.s['deck.layout']);
+	const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id));
+	const ids = deepClone(store.s['deck.layout'][idsIndex]);
+	ids.some((x, i) => {
+		if (x === id) {
+			const up = ids[i - 1];
+			if (up) {
+				ids[i - 1] = id;
+				ids[i] = up;
+
+				layout[idsIndex] = ids;
+				store.set('deck.layout', layout);
+			}
+			return true;
+		}
+		return false;
+	});
+	saveDeck();
+}
+
+export function swapDownColumn(id: Column['id']) {
+	const layout = deepClone(store.s['deck.layout']);
+	const idsIndex = store.s['deck.layout'].findIndex(ids => ids.includes(id));
+	const ids = deepClone(store.s['deck.layout'][idsIndex]);
+	ids.some((x, i) => {
+		if (x === id) {
+			const down = ids[i + 1];
+			if (down) {
+				ids[i + 1] = id;
+				ids[i] = down;
+
+				layout[idsIndex] = ids;
+				store.set('deck.layout', layout);
+			}
+			return true;
+		}
+		return false;
+	});
+	saveDeck();
+}
+
+export function stackLeftColumn(id: Column['id']) {
+	let layout = deepClone(store.s['deck.layout']);
+	const i = store.s['deck.layout'].findIndex(ids => ids.includes(id));
+	layout = layout.map(ids => ids.filter(_id => _id !== id));
+	layout[i - 1].push(id);
+	layout = layout.filter(ids => ids.length > 0);
+	store.set('deck.layout', layout);
+	saveDeck();
+}
+
+export function popRightColumn(id: Column['id']) {
+	let layout = deepClone(store.s['deck.layout']);
+	const i = store.s['deck.layout'].findIndex(ids => ids.includes(id));
+	const affected = layout[i];
+	layout = layout.map(ids => ids.filter(_id => _id !== id));
+	layout.splice(i + 1, 0, [id]);
+	layout = layout.filter(ids => ids.length > 0);
+	store.set('deck.layout', layout);
+
+	const columns = deepClone(store.s['deck.columns']);
+	for (const column of columns) {
+		if (affected.includes(column.id)) {
+			column.active = true;
+		}
+	}
+	store.set('deck.columns', columns);
+
+	saveDeck();
+}
+
+export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
+	const columns = deepClone(store.s['deck.columns']);
+	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
+	const column = deepClone(store.s['deck.columns'][columnIndex]);
+	if (column == null) return;
+	if (column.widgets == null) column.widgets = [];
+	column.widgets.unshift(widget);
+	columns[columnIndex] = column;
+	store.set('deck.columns', columns);
+	saveDeck();
+}
+
+export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
+	const columns = deepClone(store.s['deck.columns']);
+	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
+	const column = deepClone(store.s['deck.columns'][columnIndex]);
+	if (column == null) return;
+	if (column.widgets == null) column.widgets = [];
+	column.widgets = column.widgets.filter(w => w.id !== widget.id);
+	columns[columnIndex] = column;
+	store.set('deck.columns', columns);
+	saveDeck();
+}
+
+export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
+	const columns = deepClone(store.s['deck.columns']);
+	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
+	const column = deepClone(store.s['deck.columns'][columnIndex]);
+	if (column == null) return;
+	column.widgets = widgets;
+	columns[columnIndex] = column;
+	store.set('deck.columns', columns);
+	saveDeck();
+}
+
+export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
+	const columns = deepClone(store.s['deck.columns']);
+	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
+	const column = deepClone(store.s['deck.columns'][columnIndex]);
+	if (column == null) return;
+	if (column.widgets == null) column.widgets = [];
+	column.widgets = column.widgets.map(w => w.id === widgetId ? {
+		...w,
+		data: widgetData,
+	} : w);
+	columns[columnIndex] = column;
+	store.set('deck.columns', columns);
+	saveDeck();
+}
+
+export function updateColumn(id: Column['id'], column: Partial<Column>) {
+	const columns = deepClone(store.s['deck.columns']);
+	const columnIndex = store.s['deck.columns'].findIndex(c => c.id === id);
+	const currentColumn = deepClone(store.s['deck.columns'][columnIndex]);
+	if (currentColumn == null) return;
+	for (const [k, v] of Object.entries(column)) {
+		currentColumn[k] = v;
+	}
+	columns[columnIndex] = currentColumn;
+	store.set('deck.columns', columns);
+	saveDeck();
+}
diff --git a/packages/frontend/src/di.ts b/packages/frontend/src/di.ts
new file mode 100644
index 0000000000..538f85df2d
--- /dev/null
+++ b/packages/frontend/src/di.ts
@@ -0,0 +1,12 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { InjectionKey, Ref } from 'vue';
+import type { IRouter } from '@/nirax.js';
+
+export const DI = {
+	routerCurrentDepth: Symbol() as InjectionKey<number>,
+	router: Symbol() as InjectionKey<IRouter>,
+};
diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts
index 98e3d91b29..a68cd1b18b 100644
--- a/packages/frontend/src/directives/adaptive-bg.ts
+++ b/packages/frontend/src/directives/adaptive-bg.ts
@@ -4,7 +4,7 @@
  */
 
 import type { Directive } from 'vue';
-import { getBgColor } from '@/scripts/get-bg-color.js';
+import { getBgColor } from '@/utility/get-bg-color.js';
 
 export default {
 	mounted(src, binding, vn) {
diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts
index 4037076cae..8072a1ffd9 100644
--- a/packages/frontend/src/directives/adaptive-border.ts
+++ b/packages/frontend/src/directives/adaptive-border.ts
@@ -4,18 +4,33 @@
  */
 
 import type { Directive } from 'vue';
-import { getBgColor } from '@/scripts/get-bg-color.js';
+import { getBgColor } from '@/utility/get-bg-color.js';
+import { globalEvents } from '@/events.js';
+
+const handlerMap = new WeakMap<any, any>();
 
 export default {
 	mounted(src, binding, vn) {
-		const parentBg = getBgColor(src.parentElement) ?? 'transparent';
+		function calc() {
+			const parentBg = getBgColor(src.parentElement) ?? 'transparent';
 
-		const myBg = window.getComputedStyle(src).backgroundColor;
+			const myBg = window.getComputedStyle(src).backgroundColor;
 
-		if (parentBg === myBg) {
-			src.style.borderColor = 'var(--MI_THEME-divider)';
-		} else {
-			src.style.borderColor = myBg;
+			if (parentBg === myBg) {
+				src.style.borderColor = 'var(--MI_THEME-divider)';
+			} else {
+				src.style.borderColor = myBg;
+			}
 		}
+
+		handlerMap.set(src, calc);
+
+		calc();
+
+		globalEvents.on('themeChanged', calc);
+	},
+
+	unmounted(src, binding, vn) {
+		globalEvents.off('themeChanged', handlerMap.get(src));
 	},
 } as Directive;
diff --git a/packages/frontend/src/directives/click-anime.ts b/packages/frontend/src/directives/click-anime.ts
index 60242837f2..c34f351fb3 100644
--- a/packages/frontend/src/directives/click-anime.ts
+++ b/packages/frontend/src/directives/click-anime.ts
@@ -4,11 +4,11 @@
  */
 
 import type { Directive } from 'vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 export default {
 	mounted(el: HTMLElement, binding, vn) {
-		if (!defaultStore.state.animation) return;
+		if (!prefer.s.animation) return;
 
 		const target = el.children[0];
 
diff --git a/packages/frontend/src/directives/hotkey.ts b/packages/frontend/src/directives/hotkey.ts
index ec00652381..75e022e98f 100644
--- a/packages/frontend/src/directives/hotkey.ts
+++ b/packages/frontend/src/directives/hotkey.ts
@@ -4,7 +4,7 @@
  */
 
 import type { Directive } from 'vue';
-import { makeHotkey } from '@/scripts/hotkey.js';
+import { makeHotkey } from '@/utility/hotkey.js';
 
 export default {
 	mounted(el, binding) {
diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts
index 19fd374861..17916fb6d3 100644
--- a/packages/frontend/src/directives/panel.ts
+++ b/packages/frontend/src/directives/panel.ts
@@ -4,7 +4,7 @@
  */
 
 import type { Directive } from 'vue';
-import { getBgColor } from '@/scripts/get-bg-color.js';
+import { getBgColor } from '@/utility/get-bg-color.js';
 
 export default {
 	mounted(src, binding, vn) {
diff --git a/packages/frontend/src/directives/ripple.ts b/packages/frontend/src/directives/ripple.ts
index 99845c57c3..614cd37011 100644
--- a/packages/frontend/src/directives/ripple.ts
+++ b/packages/frontend/src/directives/ripple.ts
@@ -4,14 +4,14 @@
  */
 
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { popup } from '@/os.js';
 
 export default {
 	mounted(el, binding, vn) {
 		// 明示的に false であればバインドしない
 		if (binding.value === false) return;
-		if (!defaultStore.state.animation) return;
+		if (!prefer.s.animation) return;
 
 		el.addEventListener('click', () => {
 			const rect = el.getBoundingClientRect();
diff --git a/packages/frontend/src/directives/tooltip.ts b/packages/frontend/src/directives/tooltip.ts
index 6bfe6ac31d..068186dfa0 100644
--- a/packages/frontend/src/directives/tooltip.ts
+++ b/packages/frontend/src/directives/tooltip.ts
@@ -8,7 +8,7 @@
 
 import { defineAsyncComponent, ref } from 'vue';
 import type { Directive } from 'vue';
-import { isTouchUsing } from '@/scripts/touch.js';
+import { isTouchUsing } from '@/utility/touch.js';
 import { popup, alert } from '@/os.js';
 
 const start = isTouchUsing ? 'touchstart' : 'mouseenter';
diff --git a/packages/frontend/src/events.ts b/packages/frontend/src/events.ts
index d476aec04a..a74018223c 100644
--- a/packages/frontend/src/events.ts
+++ b/packages/frontend/src/events.ts
@@ -7,6 +7,7 @@ import { EventEmitter } from 'eventemitter3';
 import * as Misskey from 'misskey-js';
 
 export const globalEvents = new EventEmitter<{
+	themeChanging: () => void;
 	themeChanged: () => void;
 	clientNotification: (notification: Misskey.entities.Notification) => void;
 	requestClearPageCache: () => void;
diff --git a/packages/frontend/src/instance.ts b/packages/frontend/src/instance.ts
index 66859c30f4..bbc962c945 100644
--- a/packages/frontend/src/instance.ts
+++ b/packages/frontend/src/instance.ts
@@ -5,8 +5,7 @@
 
 import { computed, reactive } from 'vue';
 import * as Misskey from 'misskey-js';
-import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERROR_IMAGE_URL } from '@@/js/const.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { miLocalStorage } from '@/local-storage.js';
 
 // TODO: 他のタブと永続化されたstateを同期
diff --git a/packages/frontend/src/local-storage.ts b/packages/frontend/src/local-storage.ts
index 2115e46482..804bd6bee8 100644
--- a/packages/frontend/src/local-storage.ts
+++ b/packages/frontend/src/local-storage.ts
@@ -19,7 +19,6 @@ export type Keys = (
 	'drafts' |
 	'hashtags' |
 	'wallpaper' |
-	'theme' |
 	'colorScheme' |
 	'useSystemFont' |
 	'defaultFontFace' |
@@ -29,13 +28,17 @@ export type Keys = (
 	'locale' |
 	'localeVersion' |
 	'theme' |
+	'themeId' |
 	'customCss' |
 	'message_drafts' |
 	'scratchpad' |
 	'debug' |
+	'preferences' |
+	'latestPreferencesUpdate' |
+	'hidePreferencesRestoreSuggestion' |
 	`miux:${string}` |
 	`ui:folder:${string}` |
-	`themes:${string}` |
+	`themes:${string}` | // DEPRECATED
 	`aiscript:${string}` |
 	'lastEmojisFetchedAt' | // DEPRECATED, stored in indexeddb (13.9.0~)
 	'emojis' | // DEPRECATED, stored in indexeddb (13.9.0~);
diff --git a/packages/frontend/src/navbar.ts b/packages/frontend/src/navbar.ts
index 3e40645840..11e0554d63 100644
--- a/packages/frontend/src/navbar.ts
+++ b/packages/frontend/src/navbar.ts
@@ -5,14 +5,14 @@
 
 import { computed, reactive } from 'vue';
 import { ui } from '@@/js/config.js';
-import { clearCache } from './scripts/clear-cache.js';
+import { clearCache } from './utility/clear-cache.js';
 import { $i } from '@/account.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { openInstanceMenu, openToolsMenu } from '@/ui/_common_/common.js';
-import { lookup } from '@/scripts/lookup.js';
+import { lookup } from '@/utility/lookup.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 
 export const navbarItemDef = reactive({
 	notifications: {
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index 6e48366092..a009a3f3d1 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -6,15 +6,15 @@
 // TODO: なんでもかんでもos.tsに突っ込むのやめたいのでよしなに分割する
 
 import { markRaw, ref, defineAsyncComponent, nextTick } from 'vue';
-import type { Component, Ref } from 'vue';
 import { EventEmitter } from 'eventemitter3';
 import * as Misskey from 'misskey-js';
+import type { Component, Ref } from 'vue';
 import type { ComponentProps as CP } from 'vue-component-type-helpers';
-import type { Form, GetFormResultType } from '@/scripts/form.js';
+import type { Form, GetFormResultType } from '@/utility/form.js';
 import type { MenuItem } from '@/types/menu.js';
 import type { PostFormProps } from '@/types/post-form.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 import MkPostFormDialog from '@/components/MkPostFormDialog.vue';
 import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
@@ -25,11 +25,11 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
 import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
 import MkPopupMenu from '@/components/MkPopupMenu.vue';
 import MkContextMenu from '@/components/MkContextMenu.vue';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
-import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
-import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
-import { focusParent } from '@/scripts/focus.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { pleaseLogin } from '@/utility/please-login.js';
+import { showMovedDialog } from '@/utility/show-moved-dialog.js';
+import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
+import { focusParent } from '@/utility/focus.js';
 
 export const openingWindowsCount = ref(0);
 
@@ -626,7 +626,7 @@ export async function selectRole(params: {
 }): Promise<
 	{ canceled: true; result: undefined; } |
 	{ canceled: false; result: Misskey.entities.Role[] }
-> {
+	> {
 	return new Promise((resolve) => {
 		popup(defineAsyncComponent(() => import('@/components/MkRoleSelectDialog.vue')), params, {
 			done: roles => {
@@ -699,8 +699,8 @@ export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | n
 
 export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
 	if (
-		defaultStore.state.contextMenu === 'native' ||
-		(defaultStore.state.contextMenu === 'appWithShift' && !ev.shiftKey)
+		prefer.s.contextMenu === 'native' ||
+		(prefer.s.contextMenu === 'appWithShift' && !ev.shiftKey)
 	) {
 		return Promise.resolve();
 	}
diff --git a/packages/frontend/src/scripts/page-metadata.ts b/packages/frontend/src/page.ts
similarity index 95%
rename from packages/frontend/src/scripts/page-metadata.ts
rename to packages/frontend/src/page.ts
index 671751147c..f3ec09a16f 100644
--- a/packages/frontend/src/scripts/page-metadata.ts
+++ b/packages/frontend/src/page.ts
@@ -35,7 +35,7 @@ const getMetadata = (): Ref<PageMetadata | null> | undefined => {
 	return inject<Ref<PageMetadata | null>>(METADATA_KEY);
 };
 
-export const definePageMetadata = (maybeRefOrGetterMetadata: MaybeRefOrGetter<PageMetadata>): void => {
+export const definePage = (maybeRefOrGetterMetadata: MaybeRefOrGetter<PageMetadata>): void => {
 	const metadataRef = ref(toValue(maybeRefOrGetterMetadata));
 	const metadataGetter = () => metadataRef.value;
 	const receiver = getReceiver();
diff --git a/packages/frontend/src/pages/_error_.vue b/packages/frontend/src/pages/_error_.vue
index f09a8e4285..066980db1f 100644
--- a/packages/frontend/src/pages/_error_.vue
+++ b/packages/frontend/src/pages/_error_.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <MkLoading v-if="!loaded"/>
-<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" appear>
+<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" appear>
 	<div v-show="loaded" :class="$style.root">
 		<img :src="serverErrorImageUrl" class="_ghost" :class="$style.img"/>
 		<div class="_gaps">
@@ -27,15 +27,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
+import { version } from '@@/js/config.js';
 import MkButton from '@/components/MkButton.vue';
 import MkLink from '@/components/MkLink.vue';
-import { version } from '@@/js/config.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { serverErrorImageUrl } from '@/instance.js';
 
 const props = withDefaults(defineProps<{
@@ -67,7 +67,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.error,
 	icon: 'ti ti-alert-triangle',
 }));
diff --git a/packages/frontend/src/pages/about-misskey.vue b/packages/frontend/src/pages/about-misskey.vue
index e5c55962c0..f1eb23dfdc 100644
--- a/packages/frontend/src/pages/about-misskey.vue
+++ b/packages/frontend/src/pages/about-misskey.vue
@@ -142,13 +142,13 @@ import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { physics } from '@/scripts/physics.js';
+import { physics } from '@/utility/physics.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import * as os from '@/os.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
+import { definePage } from '@/page.js';
+import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
 import { $i } from '@/account.js';
 
 const patronsWithIcon = [{
@@ -408,7 +408,7 @@ const easterEggEngine = ref<{ stop: () => void } | null>(null);
 const containerEl = shallowRef<HTMLElement>();
 
 function iconLoaded() {
-	const emojis = defaultStore.state.reactions;
+	const emojis = store.s.reactions;
 	const containerWidth = containerEl.value.offsetWidth;
 	for (let i = 0; i < 32; i++) {
 		easterEggEmojis.value.push({
@@ -452,7 +452,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.aboutMisskey,
 	icon: null,
 }));
diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue
index e5e57c05c4..0b9eee7d49 100644
--- a/packages/frontend/src/pages/about.overview.vue
+++ b/packages/frontend/src/pages/about.overview.vue
@@ -130,7 +130,7 @@ import { host, version } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import number from '@/filters/number.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import FormSplit from '@/components/form/split.vue';
diff --git a/packages/frontend/src/pages/about.vue b/packages/frontend/src/pages/about.vue
index ef0fd39ffe..d6833a3d97 100644
--- a/packages/frontend/src/pages/about.vue
+++ b/packages/frontend/src/pages/about.vue
@@ -27,8 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, defineAsyncComponent, ref, watch } from 'vue';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { definePage } from '@/page.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
 const XOverview = defineAsyncComponent(() => import('@/pages/about.overview.vue'));
@@ -81,7 +81,7 @@ const headerTabs = computed(() => {
 	return items;
 });
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.instanceInfo,
 	icon: 'ti ti-info-circle',
 }));
diff --git a/packages/frontend/src/pages/achievements.vue b/packages/frontend/src/pages/achievements.vue
index 77ab473ea2..53ce75f9bf 100644
--- a/packages/frontend/src/pages/achievements.vue
+++ b/packages/frontend/src/pages/achievements.vue
@@ -16,9 +16,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
 import MkAchievements from '@/components/MkAchievements.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { $i } from '@/account.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 
 let timer: number | null;
 
@@ -48,7 +48,7 @@ onDeactivated(() => {
 	}
 });
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.achievements,
 	icon: 'ti ti-medal',
 }));
diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue
index 7ccb996fff..0af28e94fa 100644
--- a/packages/frontend/src/pages/admin-file.vue
+++ b/packages/frontend/src/pages/admin-file.vue
@@ -83,9 +83,9 @@ import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import bytes from '@/filters/bytes.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { iAmAdmin, iAmModerator } from '@/account.js';
 
 const tab = ref('overview');
@@ -161,7 +161,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-code',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: file.value ? `${i18n.ts.file}: ${file.value.name}` : i18n.ts.file,
 	icon: 'ti ti-file',
 }));
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 2aae19b3e8..30d99ab608 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -231,9 +231,9 @@ import FormSuspense from '@/components/form/suspense.vue';
 import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { acct } from '@/filters/user.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { iAmAdmin, $i, iAmModerator } from '@/account.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
@@ -549,7 +549,7 @@ const headerTabs = computed(() => isSystem.value ? [{
 	icon: 'ti ti-code',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: user.value ? acct(user.value) : i18n.ts.userInfo,
 	icon: 'ti ti-user-exclamation',
 }));
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index 4762ef3f97..6c47e6397f 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -71,7 +71,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 import { rolesCache } from '@/cache.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue
index 9b1bf51f58..1382cad9a4 100644
--- a/packages/frontend/src/pages/admin/_header_.vue
+++ b/packages/frontend/src/pages/admin/_header_.vue
@@ -35,11 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, onMounted, onUnmounted, ref, shallowRef, watch, nextTick } from 'vue';
 import tinycolor from 'tinycolor2';
-import { popupMenu } from '@/os.js';
 import { scrollToTop } from '@@/js/scroll.js';
+import { popupMenu } from '@/os.js';
 import MkButton from '@/components/MkButton.vue';
 import { globalEvents } from '@/events.js';
-import { injectReactiveMetadata } from '@/scripts/page-metadata.js';
+import { injectReactiveMetadata } from '@/page.js';
 
 type Tab = {
 	key?: string | null;
@@ -127,7 +127,7 @@ const calcBg = () => {
 
 onMounted(() => {
 	calcBg();
-	globalEvents.on('themeChanged', calcBg);
+	globalEvents.on('themeChanging', calcBg);
 
 	watch(() => [props.tab, props.tabs], () => {
 		nextTick(() => {
@@ -147,7 +147,7 @@ onMounted(() => {
 });
 
 onUnmounted(() => {
-	globalEvents.off('themeChanged', calcBg);
+	globalEvents.off('themeChanging', calcBg);
 });
 </script>
 
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
index 5f683c7a1d..5b049e5bad 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
@@ -77,7 +77,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n.js';
 import MkInput from '@/components/MkInput.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkSelect from '@/components/MkSelect.vue';
 import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
 import type { MkSystemWebhookResult } from '@/components/MkSystemWebhookEditor.impl.js';
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
index f5249261be..ee87fae606 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.vue
@@ -49,7 +49,7 @@ import { entities } from 'misskey-js';
 import { computed, defineAsyncComponent, onMounted, ref } from 'vue';
 import XRecipient from './notification-recipient.item.vue';
 import XHeader from '@/pages/admin/_header_.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkButton from '@/components/MkButton.vue';
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index 22173bb888..e4119bb62d 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
 			</div>
 
-			<MkInfo v-if="!defaultStore.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
+			<MkInfo v-if="!store.r.abusesTutorial.value" closable @close="closeTutorial()">
 				{{ i18n.ts._abuseUserReport.resolveTutorial }}
 			</MkInfo>
 
@@ -65,10 +65,10 @@ import MkSelect from '@/components/MkSelect.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import XAbuseReport from '@/components/MkAbuseReport.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 const reports = shallowRef<InstanceType<typeof MkPagination>>();
 
@@ -93,14 +93,14 @@ function resolved(reportId) {
 }
 
 function closeTutorial() {
-	defaultStore.set('abusesTutorial', false);
+	store.set('abusesTutorial', false);
 }
 
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.abuseReports,
 	icon: 'ti ti-exclamation-circle',
 }));
diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue
index 0d67359e47..ebc3d23296 100644
--- a/packages/frontend/src/pages/admin/ads.vue
+++ b/packages/frontend/src/pages/admin/ads.vue
@@ -96,9 +96,9 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const ads = ref<Misskey.entities.Ad[]>([]);
 
@@ -255,7 +255,7 @@ const headerActions = computed(() => [{
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.ads,
 	icon: 'ti ti-ad',
 }));
diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue
index e420586017..f6b331455f 100644
--- a/packages/frontend/src/pages/admin/announcements.vue
+++ b/packages/frontend/src/pages/admin/announcements.vue
@@ -94,9 +94,9 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 
@@ -199,7 +199,7 @@ const headerActions = computed(() => [{
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.announcements,
 	icon: 'ti ti-speakerphone',
 }));
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 2d314c822d..59b8435595 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -160,10 +160,10 @@ import MkRadios from '@/components/MkRadios.vue';
 import MkInput from '@/components/MkInput.vue';
 import FormSlot from '@/components/form/slot.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { useForm } from '@/scripts/use-form.js';
+import { useForm } from '@/utility/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/branding.vue b/packages/frontend/src/pages/admin/branding.vue
index 95f82c1f24..0ac45914e8 100644
--- a/packages/frontend/src/pages/admin/branding.vue
+++ b/packages/frontend/src/pages/admin/branding.vue
@@ -111,10 +111,10 @@ import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { instance, fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 import MkColorInput from '@/components/MkColorInput.vue';
 import { host } from '@@/js/config.js';
@@ -175,7 +175,7 @@ function save() {
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.branding,
 	icon: 'ti ti-paint',
 }));
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
index 06d13cda75..260177c894 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue
@@ -85,9 +85,9 @@ import MkGrid from '@/components/grid/MkGrid.vue';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 import { validators } from '@/components/grid/cell-validators.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkPagingButtons from '@/components/MkPagingButtons.vue';
-import { selectFile } from '@/scripts/select-file.js';
+import { selectFile } from '@/utility/select-file.js';
 import { copyGridDataToClipboard, removeDataFromGrid } from '@/components/grid/grid-utils.js';
 import { useLoading } from '@/components/hook/useLoading.js';
 
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
index d6ee8ea49c..666e3c95ac 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.register.vue
@@ -78,7 +78,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 /* eslint-disable @typescript-eslint/no-non-null-assertion */
 import * as Misskey from 'misskey-js';
 import { onMounted, ref, useCssModule } from 'vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
+import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
+import type { DroppedFile } from '@/utility/file-drop.js';
+import type { GridSetting } from '@/components/grid/grid.js';
+import type { GridRow } from '@/components/grid/row.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import {
 	emptyStrToEmptyArray,
 	emptyStrToNull,
@@ -88,22 +93,17 @@ import MkGrid from '@/components/grid/MkGrid.vue';
 import { i18n } from '@/i18n.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { defaultStore } from '@/store.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { validators } from '@/components/grid/cell-validators.js';
-import { chooseFileFromDrive, chooseFileFromPc } from '@/scripts/select-file.js';
-import { uploadFile } from '@/scripts/upload.js';
-import { extractDroppedItems, flattenDroppedFiles } from '@/scripts/file-drop.js';
+import { chooseFileFromDrive, chooseFileFromPc } from '@/utility/select-file.js';
+import { uploadFile } from '@/utility/upload.js';
+import { extractDroppedItems, flattenDroppedFiles } from '@/utility/file-drop.js';
 import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
 import { copyGridDataToClipboard } from '@/components/grid/grid-utils.js';
 
-import type { RequestLogItem } from '@/pages/admin/custom-emojis-manager.impl.js';
-import type { GridCellValidationEvent, GridCellValueChangeEvent, GridEvent } from '@/components/grid/grid-event.js';
-import type { DroppedFile } from '@/scripts/file-drop.js';
-import type { GridSetting } from '@/components/grid/grid.js';
-import type { GridRow } from '@/components/grid/row.js';
+import { prefer } from '@/preferences.js';
 
 const MAXIMUM_EMOJI_REGISTER_COUNT = 100;
 
@@ -244,8 +244,8 @@ function setupGrid(): GridSetting {
 
 const uploadFolders = ref<FolderItem[]>([]);
 const gridItems = ref<GridItem[]>([]);
-const selectedFolderId = ref(defaultStore.state.uploadFolder);
-const keepOriginalUploading = ref(defaultStore.state.keepOriginalUploading);
+const selectedFolderId = ref(prefer.s.uploadFolder);
+const keepOriginalUploading = ref(prefer.s.keepOriginalUploading);
 const directoryToCategory = ref<boolean>(false);
 const registerButtonDisabled = ref<boolean>(false);
 const requestLogs = ref<RequestLogItem[]>([]);
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
index 609d445d79..c868a700f1 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager.remote.vue
@@ -143,7 +143,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, onMounted, ref, useCssModule } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -152,7 +152,7 @@ import { emptyStrToUndefined, gridSortOrderKeys } from '@/pages/admin/custom-emo
 import MkFolder from '@/components/MkFolder.vue';
 import XRegisterLogs from '@/pages/admin/custom-emojis-manager.logs.vue';
 import * as os from '@/os.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import MkPagingButtons from '@/components/MkPagingButtons.vue';
 import MkSortOrderEditor from '@/components/MkSortOrderEditor.vue';
 import { useLoading } from '@/components/hook/useLoading.js';
diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
index fb930064ff..7667206fa8 100644
--- a/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
+++ b/packages/frontend/src/pages/admin/custom-emojis-manager2.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script setup lang="ts">
 import { computed, ref } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import XGridLocalComponent from '@/pages/admin/custom-emojis-manager.local.vue';
 import XGridRemoteComponent from '@/pages/admin/custom-emojis-manager.remote.vue';
 import MkPageHeader from '@/components/global/MkPageHeader.vue';
@@ -36,7 +36,7 @@ const headerTabs = computed(() => [{
 	title: i18n.ts.remote,
 }]);
 
-definePageMetadata(computed(() => ({
+definePage(computed(() => ({
 	title: i18n.ts.customEmojis,
 	icon: 'ti ti-icons',
 	needWideArea: true,
diff --git a/packages/frontend/src/pages/admin/database.vue b/packages/frontend/src/pages/admin/database.vue
index e092efd92c..1d8803d8c1 100644
--- a/packages/frontend/src/pages/admin/database.vue
+++ b/packages/frontend/src/pages/admin/database.vue
@@ -21,11 +21,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import bytes from '@/filters/bytes.js';
 import number from '@/filters/number.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const databasePromiseFactory = () => misskeyApi('admin/get-table-stats').then(res => Object.entries(res).sort((a, b) => b[1].size - a[1].size));
 
@@ -33,7 +33,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.database,
 	icon: 'ti ti-database',
 }));
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index 5b60e67dac..ab584ba9da 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -73,10 +73,10 @@ import FormSuspense from '@/components/form/suspense.vue';
 import FormSplit from '@/components/form/split.vue';
 import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance, instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 
 const enableEmail = ref<boolean>(false);
@@ -130,7 +130,7 @@ function save() {
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.emailServer,
 	icon: 'ti ti-mail',
 }));
diff --git a/packages/frontend/src/pages/admin/external-services.vue b/packages/frontend/src/pages/admin/external-services.vue
index a312ecce12..a6557114dc 100644
--- a/packages/frontend/src/pages/admin/external-services.vue
+++ b/packages/frontend/src/pages/admin/external-services.vue
@@ -49,10 +49,10 @@ import MkButton from '@/components/MkButton.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkFolder from '@/components/MkFolder.vue';
 
 const deeplAuthKey = ref<string>('');
@@ -88,7 +88,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.externalServices,
 	icon: 'ti ti-link',
 }));
diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue
index e7b9fd8621..7f6424225b 100644
--- a/packages/frontend/src/pages/admin/federation.vue
+++ b/packages/frontend/src/pages/admin/federation.vue
@@ -67,7 +67,7 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
 import FormSplit from '@/components/form/split.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const host = ref('');
 const state = ref('federating');
@@ -112,7 +112,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.federation,
 	icon: 'ti ti-whirl',
 }));
diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue
index 4cc859227f..e15724c2a7 100644
--- a/packages/frontend/src/pages/admin/files.vue
+++ b/packages/frontend/src/pages/admin/files.vue
@@ -42,9 +42,9 @@ import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
 import * as os from '@/os.js';
-import { lookupFile } from '@/scripts/admin-lookup.js';
+import { lookupFile } from '@/utility/admin-lookup.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const origin = ref('local');
 const type = ref<string | null>(null);
@@ -85,7 +85,7 @@ const headerActions = computed(() => [{
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.files,
 	icon: 'ti ti-cloud',
 }));
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index 310bbbea95..54d0778950 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -38,12 +38,12 @@ import MkSuperMenu from '@/components/MkSuperMenu.vue';
 import type { SuperMenuDef } from '@/components/MkSuperMenu.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import { instance } from '@/instance.js';
-import { lookup } from '@/scripts/lookup.js';
+import { lookup } from '@/utility/lookup.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { lookupUser, lookupUserByEmail, lookupFile } from '@/scripts/admin-lookup.js';
-import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { lookupUser, lookupUserByEmail, lookupFile } from '@/utility/admin-lookup.js';
+import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
+import type { PageMetadata } from '@/page.js';
 import { useRouter } from '@/router/supplier.js';
 
 const isEmpty = (x: string | null) => x == null || x === '';
@@ -333,7 +333,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => INFO.value);
+definePage(() => INFO.value);
 
 defineExpose({
 	header: {
diff --git a/packages/frontend/src/pages/admin/invites.vue b/packages/frontend/src/pages/admin/invites.vue
index 5189e12899..069ed6e1f3 100644
--- a/packages/frontend/src/pages/admin/invites.vue
+++ b/packages/frontend/src/pages/admin/invites.vue
@@ -59,7 +59,7 @@ import { computed, ref, shallowRef } from 'vue';
 import XHeader from './_header_.vue';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSelect from '@/components/MkSelect.vue';
@@ -68,7 +68,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import type { Paging } from '@/components/MkPagination.vue';
 import MkInviteCode from '@/components/MkInviteCode.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const pagingComponent = shallowRef<InstanceType<typeof MkPagination>>();
 
@@ -114,7 +114,7 @@ function deleted(id: string) {
 const headerActions = computed(() => []);
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.invite,
 	icon: 'ti ti-user-plus',
 }));
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index 5c195fa36f..9dea53a292 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -138,10 +138,10 @@ import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 import FormLink from '@/components/form/link.vue';
 import MkFolder from '@/components/MkFolder.vue';
@@ -273,7 +273,7 @@ function save_mediaSilencedHosts() {
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.moderation,
 	icon: 'ti ti-shield',
 }));
diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue
index c9eaf07531..ae7022a1c9 100644
--- a/packages/frontend/src/pages/admin/modlog.vue
+++ b/packages/frontend/src/pages/admin/modlog.vue
@@ -38,7 +38,7 @@ import MkSelect from '@/components/MkSelect.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 
 const logs = shallowRef<InstanceType<typeof MkPagination>>();
@@ -59,7 +59,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.moderationLogs,
 	icon: 'ti ti-list-search',
 }));
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index d5a664934c..da96eb4881 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -90,10 +90,10 @@ import MkInput from '@/components/MkInput.vue';
 import FormSuspense from '@/components/form/suspense.vue';
 import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 
 const useObjectStorage = ref<boolean>(false);
@@ -149,7 +149,7 @@ function save() {
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.objectStorage,
 	icon: 'ti ti-cloud',
 }));
diff --git a/packages/frontend/src/pages/admin/overview.active-users.vue b/packages/frontend/src/pages/admin/overview.active-users.vue
index 79dd6fd5fd..7cada9b5be 100644
--- a/packages/frontend/src/pages/admin/overview.active-users.vue
+++ b/packages/frontend/src/pages/admin/overview.active-users.vue
@@ -16,11 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, shallowRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
@@ -54,7 +54,7 @@ async function renderChart() {
 
 	const raw = await misskeyApi('charts/active-users', { limit: chartLimit, span: 'day' });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const colorRead = '#3498db';
 	const colorWrite = '#2ecc71';
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue
index 570fcddc07..bbfe60d205 100644
--- a/packages/frontend/src/pages/admin/overview.ap-requests.vue
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue
@@ -24,12 +24,12 @@ import { onMounted, shallowRef, ref } from 'vue';
 import { Chart } from 'chart.js';
 import gradient from 'chartjs-plugin-gradient';
 import isChromatic from 'chromatic';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { defaultStore } from '@/store.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { store } from '@/store.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
@@ -68,7 +68,7 @@ onMounted(async () => {
 
 	const raw = await misskeyApi('charts/ap-request', { limit: chartLimit, span: 'day' });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 	const succColor = '#87e000';
 	const failColor = '#ff4400';
 
diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue
index 7d80f8c2e3..362f0974e0 100644
--- a/packages/frontend/src/pages/admin/overview.federation.vue
+++ b/packages/frontend/src/pages/admin/overview.federation.vue
@@ -50,11 +50,11 @@ import { onMounted, ref } from 'vue';
 import XPie from './overview.pie.vue';
 import type { InstanceForPie } from './overview.pie.vue';
 import * as os from '@/os.js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+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 '@/scripts/use-chart-tooltip.js';
+import { useChartTooltip } from '@/utility/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.instances.vue b/packages/frontend/src/pages/admin/overview.instances.vue
index 292e2e1dbc..c8291459fe 100644
--- a/packages/frontend/src/pages/admin/overview.instances.vue
+++ b/packages/frontend/src/pages/admin/overview.instances.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div>
-	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
+	<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
 		<MkLoading v-if="fetching"/>
 		<div v-else :class="$style.instances">
 			<MkA v-for="(instance, i) in instances" :key="instance.id" v-tooltip.mfm.noDelay="`${instance.name}\n${instance.host}\n${instance.softwareName} ${instance.softwareVersion}`" :to="`/instance-info/${instance.host}`" :class="$style.instance">
@@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
 import * as Misskey from 'misskey-js';
 import { useInterval } from '@@/js/use-interval.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkInstanceCardMini from '@/components/MkInstanceCardMini.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const instances = ref<Misskey.entities.FederationInstance[]>([]);
 const fetching = ref(true);
diff --git a/packages/frontend/src/pages/admin/overview.moderators.vue b/packages/frontend/src/pages/admin/overview.moderators.vue
index f0691534c8..fb2c5ea13c 100644
--- a/packages/frontend/src/pages/admin/overview.moderators.vue
+++ b/packages/frontend/src/pages/admin/overview.moderators.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div>
-	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
+	<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
 		<MkLoading v-if="fetching"/>
 		<div v-else :class="$style.root" class="_panel">
 			<MkA v-for="user in moderators" :key="user.id" class="user" :to="`/admin/user/${user.id}`">
@@ -18,9 +18,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
 import * as Misskey from 'misskey-js';
-import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { prefer } from '@/preferences.js';
 
 const moderators = ref<Misskey.entities.UserDetailed[] | null>(null);
 const fetching = ref(true);
diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue
index a21ec6c464..792565dd96 100644
--- a/packages/frontend/src/pages/admin/overview.pie.vue
+++ b/packages/frontend/src/pages/admin/overview.pie.vue
@@ -10,8 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { initChart } from '@/utility/init-chart.js';
 
 export type InstanceForPie = {
 	name: string,
diff --git a/packages/frontend/src/pages/admin/overview.queue.chart.vue b/packages/frontend/src/pages/admin/overview.queue.chart.vue
index 2efc17c888..708768a1e4 100644
--- a/packages/frontend/src/pages/admin/overview.queue.chart.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.chart.vue
@@ -10,11 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { store } from '@/store.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
@@ -67,7 +67,7 @@ const color =
 	'?' as never;
 
 onMounted(() => {
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	chartInstance = new Chart(chartEl.value, {
 		type: 'line',
diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue
index 222e9f4673..fd8145b308 100644
--- a/packages/frontend/src/pages/admin/overview.stats.vue
+++ b/packages/frontend/src/pages/admin/overview.stats.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div>
-	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
+	<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
 		<MkLoading v-if="fetching"/>
 		<div v-else :class="$style.root">
 			<div class="item _panel users">
@@ -63,12 +63,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import MkNumberDiff from '@/components/MkNumberDiff.vue';
 import MkNumber from '@/components/MkNumber.vue';
 import { i18n } from '@/i18n.js';
 import { customEmojis } from '@/custom-emojis.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const stats = ref<Misskey.entities.StatsResponse | null>(null);
 const usersComparedToThePrevDay = ref<number>();
diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue
index 8c9d7a8197..6a39f4561f 100644
--- a/packages/frontend/src/pages/admin/overview.users.vue
+++ b/packages/frontend/src/pages/admin/overview.users.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <div :class="$style.root">
-	<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" mode="out-in">
+	<Transition :name="prefer.s.animation ? '_transition_zoom' : ''" mode="out-in">
 		<MkLoading v-if="fetching"/>
 		<div v-else class="users">
 			<MkA v-for="(user, i) in newUsers" :key="user.id" :to="`/admin/user/${user.id}`" class="user">
@@ -18,11 +18,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref } from 'vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
 import * as Misskey from 'misskey-js';
 import { useInterval } from '@@/js/use-interval.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const newUsers = ref<Misskey.entities.UserDetailed[] | null>(null);
 const fetching = ref(true);
diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue
index 1de4dc0dc8..c23662572a 100644
--- a/packages/frontend/src/pages/admin/overview.vue
+++ b/packages/frontend/src/pages/admin/overview.vue
@@ -79,10 +79,10 @@ import XModerators from './overview.moderators.vue';
 import XHeatmap from './overview.heatmap.vue';
 import type { InstanceForPie } from './overview.pie.vue';
 import * as os from '@/os.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 
 const rootEl = shallowRef<HTMLElement>();
@@ -184,7 +184,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.dashboard,
 	icon: 'ti ti-dashboard',
 }));
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue
index 12338f0bf9..8fa2c61613 100644
--- a/packages/frontend/src/pages/admin/performance.vue
+++ b/packages/frontend/src/pages/admin/performance.vue
@@ -111,15 +111,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, computed } from 'vue';
 import XHeader from './_header_.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 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 '@/scripts/use-form.js';
+import { useForm } from '@/utility/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 
 const meta = await misskeyApi('admin/meta');
@@ -202,7 +202,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.other,
 	icon: 'ti ti-adjustments',
 }));
diff --git a/packages/frontend/src/pages/admin/queue.chart.chart.vue b/packages/frontend/src/pages/admin/queue.chart.chart.vue
index cc18898172..071d4b2f51 100644
--- a/packages/frontend/src/pages/admin/queue.chart.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.chart.vue
@@ -10,11 +10,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, shallowRef } from 'vue';
 import { Chart } from 'chart.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { alpha } from '@/scripts/color.js';
-import { initChart } from '@/scripts/init-chart.js';
+import { store } from '@/store.js';
+import { useChartTooltip } from '@/utility/use-chart-tooltip.js';
+import { chartVLine } from '@/utility/chart-vline.js';
+import { alpha } from '@/utility/color.js';
+import { initChart } from '@/utility/init-chart.js';
 
 initChart();
 
@@ -67,7 +67,7 @@ const color =
 	'?' as never;
 
 onMounted(() => {
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	chartInstance = new Chart(chartEl.value, {
 		type: 'line',
diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue
index 7c171ba0e1..607a974d20 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -53,7 +53,7 @@ import * as Misskey from 'misskey-js';
 import XChart from './queue.chart.chart.vue';
 import type { ApQueueDomain } from '@/pages/admin/queue.vue';
 import number from '@/filters/number.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 import MkFolder from '@/components/MkFolder.vue';
diff --git a/packages/frontend/src/pages/admin/queue.vue b/packages/frontend/src/pages/admin/queue.vue
index 1a26c00ddb..65d728e776 100644
--- a/packages/frontend/src/pages/admin/queue.vue
+++ b/packages/frontend/src/pages/admin/queue.vue
@@ -23,7 +23,7 @@ import XHeader from './_header_.vue';
 import * as os from '@/os.js';
 import * as config from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 
 export type ApQueueDomain = 'deliver' | 'inbox';
@@ -71,7 +71,7 @@ const headerTabs = computed(() => [{
 	title: 'Inbox',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.jobQueue,
 	icon: 'ti ti-clock-play',
 }));
diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue
index 17e99e6593..a6280e7075 100644
--- a/packages/frontend/src/pages/admin/relays.vue
+++ b/packages/frontend/src/pages/admin/relays.vue
@@ -29,9 +29,9 @@ import * as Misskey from 'misskey-js';
 import XHeader from './_header_.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const relays = ref<Misskey.entities.AdminRelaysListResponse>([]);
 
@@ -84,7 +84,7 @@ const headerActions = computed(() => [{
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.relays,
 	icon: 'ti ti-planet',
 }));
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index 2b4006c3f7..129fabf489 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -28,9 +28,9 @@ import { v4 as uuid } from 'uuid';
 import XHeader from './_header_.vue';
 import XEditor from './roles.editor.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 import { rolesCache } from '@/cache.js';
 import { useRouter } from '@/router/supplier.js';
@@ -87,7 +87,7 @@ async function save() {
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: role.value ? `${i18n.ts._role.edit}: ${role.value.name}` : i18n.ts._role.new,
 	icon: 'ti ti-badge',
 }));
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index c0a997da84..68f60771f2 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -750,7 +750,7 @@ import MkRange from '@/components/MkRange.vue';
 import FormSlot from '@/components/form/slot.vue';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 
 const emit = defineEmits<{
 	(ev: 'update:modelValue', v: any): void;
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index 1c237a69b4..43c3446b73 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -67,9 +67,9 @@ import XHeader from './_header_.vue';
 import XEditor from './roles.editor.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkInfo from '@/components/MkInfo.vue';
@@ -170,7 +170,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: `${i18n.ts.role}: ${role.name}`,
 	icon: 'ti ti-badge',
 }));
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index bed7d8db8f..e2727c3fc1 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -306,9 +306,9 @@ import MkButton from '@/components/MkButton.vue';
 import MkRange from '@/components/MkRange.vue';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { instance, fetchInstance } from '@/instance.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import { useRouter } from '@/router/supplier.js';
@@ -354,7 +354,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.roles,
 	icon: 'ti ti-badges',
 }));
diff --git a/packages/frontend/src/pages/admin/security.vue b/packages/frontend/src/pages/admin/security.vue
index 975a4a1265..85dcec6b2e 100644
--- a/packages/frontend/src/pages/admin/security.vue
+++ b/packages/frontend/src/pages/admin/security.vue
@@ -131,11 +131,11 @@ import MkRange from '@/components/MkRange.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { useForm } from '@/scripts/use-form.js';
+import { definePage } from '@/page.js';
+import { useForm } from '@/utility/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 
 const meta = await misskeyApi('admin/meta');
@@ -206,7 +206,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.security,
 	icon: 'ti ti-lock',
 }));
diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue
index cd50f92143..b8722d4112 100644
--- a/packages/frontend/src/pages/admin/server-rules.vue
+++ b/packages/frontend/src/pages/admin/server-rules.vue
@@ -46,7 +46,7 @@ import XHeader from './_header_.vue';
 import * as os from '@/os.js';
 import { fetchInstance, instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 
@@ -67,7 +67,7 @@ const remove = (index: number): void => {
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.serverRules,
 	icon: 'ti ti-checkbox',
 }));
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index aed593fc54..8c89664671 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -266,14 +266,14 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { fetchInstance, instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+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 '@/scripts/use-form.js';
+import { useForm } from '@/utility/use-form.js';
 import MkFormFooter from '@/components/MkFormFooter.vue';
 import MkRadios from '@/components/MkRadios.vue';
 
@@ -391,7 +391,7 @@ const proxyAccountForm = useForm({
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.general,
 	icon: 'ti ti-settings',
 }));
diff --git a/packages/frontend/src/pages/admin/system-webhook.vue b/packages/frontend/src/pages/admin/system-webhook.vue
index c59abda24a..d8eb9b92ee 100644
--- a/packages/frontend/src/pages/admin/system-webhook.vue
+++ b/packages/frontend/src/pages/admin/system-webhook.vue
@@ -30,11 +30,11 @@ import { computed, onMounted, ref } from 'vue';
 import { entities } from 'misskey-js';
 import XItem from './system-webhook.item.vue';
 import FormSection from '@/components/form/section.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import XHeader from '@/pages/admin/_header_.vue';
 import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { showSystemWebhookEditorDialog } from '@/components/MkSystemWebhookEditor.impl.js';
 import * as os from '@/os.js';
 
@@ -82,7 +82,7 @@ onMounted(async () => {
 	await fetchWebhooks();
 });
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'SystemWebhook',
 	icon: 'ti ti-webhook',
 }));
diff --git a/packages/frontend/src/pages/admin/users.vue b/packages/frontend/src/pages/admin/users.vue
index da4e1816bd..5fd4e0aba3 100644
--- a/packages/frontend/src/pages/admin/users.vue
+++ b/packages/frontend/src/pages/admin/users.vue
@@ -69,9 +69,9 @@ import MkInput from '@/components/MkInput.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import * as os from '@/os.js';
-import { lookupUser } from '@/scripts/admin-lookup.js';
+import { lookupUser } from '@/utility/admin-lookup.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import { dateString } from '@/filters/date.js';
 
@@ -170,7 +170,7 @@ watchEffect(() => {
 	}));
 });
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.users,
 	icon: 'ti ti-users',
 }));
diff --git a/packages/frontend/src/pages/ads.vue b/packages/frontend/src/pages/ads.vue
index b31807f9f5..bf60aa5c3f 100644
--- a/packages/frontend/src/pages/ads.vue
+++ b/packages/frontend/src/pages/ads.vue
@@ -16,11 +16,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.ads,
 	icon: 'ti ti-ad',
 }));
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue
index 56c10fb292..977bbe0b47 100644
--- a/packages/frontend/src/pages/announcement.vue
+++ b/packages/frontend/src/pages/announcement.vue
@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="800">
 		<Transition
-			:enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
-			:leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
-			:enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
-			:leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
+			:enterActiveClass="prefer.s.animation ? $style.fadeEnterActive : ''"
+			:leaveActiveClass="prefer.s.animation ? $style.fadeLeaveActive : ''"
+			:enterFromClass="prefer.s.animation ? $style.fadeEnterFrom : ''"
+			:leaveToClass="prefer.s.animation ? $style.fadeLeaveTo : ''"
 			mode="out-in"
 		>
 			<div v-if="announcement" :key="announcement.id" class="_panel" :class="$style.announcement">
@@ -52,11 +52,11 @@ import { ref, computed, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { $i, updateAccountPartial } from '@/account.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	announcementId: string;
@@ -102,7 +102,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: announcement.value ? announcement.value.title : i18n.ts.announcements,
 	icon: 'ti ti-speakerphone',
 }));
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index 75c0fd98dc..13f28d9b35 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -53,9 +53,9 @@ import MkButton from '@/components/MkButton.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { $i, updateAccountPartial } from '@/account.js';
 
 const paginationCurrent = {
@@ -111,7 +111,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-point',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.announcements,
 	icon: 'ti ti-speakerphone',
 }));
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index b89e1e4c53..4991db82d5 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -29,8 +29,8 @@ import * as Misskey from 'misskey-js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { scroll } from '@@/js/scroll.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
@@ -89,7 +89,7 @@ const headerActions = computed(() => antenna.value ? [{
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: antenna.value ? antenna.value.name : i18n.ts.antennas,
 	icon: 'ti ti-antenna',
 }));
diff --git a/packages/frontend/src/pages/api-console.vue b/packages/frontend/src/pages/api-console.vue
index 9bddb0a9d2..22b3eda61a 100644
--- a/packages/frontend/src/pages/api-console.vue
+++ b/packages/frontend/src/pages/api-console.vue
@@ -41,8 +41,8 @@ import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 
 const body = ref('{}');
 const endpoint = ref('');
@@ -87,7 +87,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'API console',
 	icon: 'ti ti-terminal-2',
 }));
diff --git a/packages/frontend/src/pages/auth.form.vue b/packages/frontend/src/pages/auth.form.vue
index f4fb2ef4d5..1917293c06 100644
--- a/packages/frontend/src/pages/auth.form.vue
+++ b/packages/frontend/src/pages/auth.form.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/auth.vue b/packages/frontend/src/pages/auth.vue
index 4170b4f73e..8b0fde4a25 100644
--- a/packages/frontend/src/pages/auth.vue
+++ b/packages/frontend/src/pages/auth.vue
@@ -46,9 +46,9 @@ import { onMounted, ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import XForm from './auth.form.vue';
 import MkSignin from '@/components/MkSignin.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { $i, login } from '@/account.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
@@ -97,7 +97,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts._auth.shareAccessTitle,
 	icon: 'ti ti-apps',
 }));
diff --git a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
index a834f1c5fd..a3c5a36614 100644
--- a/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
+++ b/packages/frontend/src/pages/avatar-decoration-edit-dialog.vue
@@ -68,7 +68,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRolePreview from '@/components/MkRolePreview.vue';
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index a5cafb1678..eb1015b19e 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -30,9 +30,9 @@ import { ref, computed, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
 import { signinRequired } from '@/account.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const $i = signinRequired();
 
@@ -86,7 +86,7 @@ const headerActions = computed(() => [{
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.avatarDecorations,
 	icon: 'ti ti-sparkles',
 }));
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index 6d8274a55c..80cefe12c3 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -74,10 +74,10 @@ import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkColorInput from '@/components/MkColorInput.vue';
-import { selectFile } from '@/scripts/select-file.js';
+import { selectFile } from '@/utility/select-file.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
@@ -202,7 +202,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: props.channelId ? i18n.ts._channel.edit : i18n.ts._channel.create,
 	icon: 'ti ti-device-tv',
 }));
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 4a91165d50..a774aa6e44 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -37,7 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<MkInfo v-if="channel.isArchived" warn>{{ i18n.ts.thisChannelArchived }}</MkInfo>
 
 				<!-- スマホ・タブレットの場合、キーボードが表示されると投稿が見づらくなるので、デスクトップ場合のみ自動でフォーカスを当てる -->
-				<MkPostForm v-if="$i && defaultStore.reactiveState.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
+				<MkPostForm v-if="$i && prefer.r.showFixedPostFormInChannel.value" :channel="channel" class="post-form _panel" fixed :autofocus="deviceKind === 'desktop'"/>
 
 				<MkTimeline :key="channelId" src="channel" :channel="channelId" @before="before" @after="after" @note="miLocalStorage.setItemAsJson(`channelLastReadedAt:${channel.id}`, Date.now())"/>
 			</div>
@@ -75,29 +75,29 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
+import type { PageHeaderItem } from '@/types/page-header.js';
 import MkPostForm from '@/components/MkPostForm.vue';
 import MkTimeline from '@/components/MkTimeline.vue';
 import XChannelFollowButton from '@/components/MkChannelFollowButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { $i, iAmModerator } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { definePage } from '@/page.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import MkNotes from '@/components/MkNotes.vue';
-import { url } from '@@/js/config.js';
 import { favoritedChannelsCache } from '@/cache.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import MkNote from '@/components/MkNote.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import type { PageHeaderItem } from '@/types/page-header.js';
-import { isSupportShare } from '@/scripts/navigator.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { notesSearchAvailable } from '@/scripts/check-permissions.js';
+import { isSupportShare } from '@/utility/navigator.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { notesSearchAvailable } from '@/utility/check-permissions.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { useRouter } from '@/router/supplier.js';
 
@@ -265,7 +265,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-search',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: channel.value ? channel.value.name : i18n.ts.channel,
 	icon: 'ti ti-device-tv',
 }));
diff --git a/packages/frontend/src/pages/channels.vue b/packages/frontend/src/pages/channels.vue
index 6830c1ace4..071f5a048b 100644
--- a/packages/frontend/src/pages/channels.vue
+++ b/packages/frontend/src/pages/channels.vue
@@ -69,7 +69,7 @@ import MkRadios from '@/components/MkRadios.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
@@ -161,7 +161,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-edit',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.channel,
 	icon: 'ti ti-device-tv',
 }));
diff --git a/packages/frontend/src/pages/clicker.vue b/packages/frontend/src/pages/clicker.vue
index 9e9b5e8688..fcc1d1d5db 100644
--- a/packages/frontend/src/pages/clicker.vue
+++ b/packages/frontend/src/pages/clicker.vue
@@ -14,9 +14,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import MkClickerGame from '@/components/MkClickerGame.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: '🍪👈',
 	icon: 'ti ti-cookie',
 }));
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 224bc51599..9765ebf216 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -39,13 +39,13 @@ import MkNotes from '@/components/MkNotes.vue';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 import { clipsCache } from '@/cache.js';
-import { isSupportShare } from '@/scripts/navigator.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { genEmbedCode } from '@/scripts/get-embed-code.js';
+import { isSupportShare } from '@/utility/navigator.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { genEmbedCode } from '@/utility/get-embed-code.js';
 import { assertServerContext, serverContext } from '@/server-context.js';
 
 // contextは非ログイン状態の情報しかないためログイン時は利用できない
@@ -193,7 +193,7 @@ const headerActions = computed(() => clip.value && isOwned.value ? [{
 	},
 }] : null);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: clip.value ? clip.value.name : i18n.ts.clip,
 	icon: 'ti ti-paperclip',
 }));
diff --git a/packages/frontend/src/pages/contact.vue b/packages/frontend/src/pages/contact.vue
index 1f2bee5a77..d99c392759 100644
--- a/packages/frontend/src/pages/contact.vue
+++ b/packages/frontend/src/pages/contact.vue
@@ -37,11 +37,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkLink from '@/components/MkLink.vue';
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.inquiry,
 	icon: 'ti ti-help-circle',
 }));
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 82c6d8df4e..35a46e4dd9 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -81,12 +81,12 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkRemoteEmojiEditDialog from '@/components/MkRemoteEmojiEditDialog.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import FormSplit from '@/components/form/split.vue';
-import { selectFile } from '@/scripts/select-file.js';
+import { selectFile } from '@/utility/select-file.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { getProxiedImageUrl } from '@/utility/media-proxy.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const emojisPaginationComponent = shallowRef<InstanceType<typeof MkPagination>>();
 
@@ -326,7 +326,7 @@ const headerTabs = computed(() => [{
 	title: i18n.ts.remote,
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.customEmojis,
 	icon: 'ti ti-icons',
 }));
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index dfcc82c77b..3f78935b0a 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -85,7 +85,7 @@ import bytes from '@/filters/bytes.js';
 import { infoImageUrl } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
diff --git a/packages/frontend/src/pages/drive.file.vue b/packages/frontend/src/pages/drive.file.vue
index 5711ec8b3a..ecc1117da9 100644
--- a/packages/frontend/src/pages/drive.file.vue
+++ b/packages/frontend/src/pages/drive.file.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, ref, defineAsyncComponent } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
 const props = defineProps<{
@@ -48,7 +48,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-pencil',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts._fileViewer.title,
 	icon: 'ti ti-file',
 }));
diff --git a/packages/frontend/src/pages/drive.vue b/packages/frontend/src/pages/drive.vue
index 25e140f67f..c5813a4523 100644
--- a/packages/frontend/src/pages/drive.vue
+++ b/packages/frontend/src/pages/drive.vue
@@ -14,7 +14,7 @@ import { computed, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XDrive from '@/components/MkDrive.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const folder = ref<Misskey.entities.DriveFolder | null>(null);
 
@@ -22,7 +22,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: folder.value ? folder.value.name : i18n.ts.drive,
 	icon: 'ti ti-cloud',
 	hideHeader: true,
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index 10099e6291..f760aca8ae 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -56,7 +56,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 
 			<div ref="containerEl" :class="[$style.gameContainer, { [$style.gameOver]: isGameOver && !replaying }]" @contextmenu.stop.prevent @click.stop.prevent="onClick" @touchmove.stop.prevent="onTouchmove" @touchend="onTouchend" @mousemove="onMousemove">
-				<img v-if="defaultStore.state.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
+				<img v-if="store.s.darkMode" src="/client-assets/drop-and-fusion/frame-dark.svg" :class="$style.mainFrameImg"/>
 				<img v-else src="/client-assets/drop-and-fusion/frame-light.svg" :class="$style.mainFrameImg"/>
 				<canvas ref="canvasEl" :class="$style.canvas"/>
 				<Transition
@@ -195,23 +195,24 @@ import { computed, onDeactivated, onMounted, onUnmounted, ref, shallowRef, watch
 import * as Matter from 'matter-js';
 import * as Misskey from 'misskey-js';
 import { DropAndFusionGame } from 'misskey-bubble-game';
+import { useInterval } from '@@/js/use-interval.js';
+import { apiUrl } from '@@/js/config.js';
 import type { Mono } from 'misskey-bubble-game';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import * as os from '@/os.js';
 import MkNumber from '@/components/MkNumber.vue';
 import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
 import MkButton from '@/components/MkButton.vue';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { defaultStore } from '@/store.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { store } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { useInterval } from '@@/js/use-interval.js';
-import { apiUrl } from '@@/js/config.js';
 import { $i } from '@/account.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 import MkRange from '@/components/MkRange.vue';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { prefer } from '@/preferences.js';
 
 type FrontendMonoDefinition = {
 	id: string;
@@ -586,8 +587,8 @@ const showConfig = ref(false);
 const replaying = ref(false);
 const replayPlaybackRate = ref(1);
 const currentFrame = ref(0);
-const bgmVolume = ref(defaultStore.state.dropAndFusion.bgmVolume);
-const sfxVolume = ref(defaultStore.state.dropAndFusion.sfxVolume);
+const bgmVolume = ref(prefer.s['game.dropAndFusion'].bgmVolume);
+const sfxVolume = ref(prefer.s['game.dropAndFusion'].sfxVolume);
 
 watch(replayPlaybackRate, (newValue) => {
 	game.replayPlaybackRate = newValue;
@@ -623,7 +624,7 @@ function loadMonoTextures() {
 		if (renderer.textures[mono.img]) return;
 
 		let src = mono.img;
-		// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+
 		if (monoTextureUrls[mono.img]) {
 			src = monoTextureUrls[mono.img];
 			// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
@@ -649,7 +650,6 @@ function loadMonoTextures() {
 function getTextureImageUrl(mono: Mono) {
 	const def = monoDefinitions.value.find(x => x.id === mono.id)!;
 
-	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 	if (monoTextureUrls[def.img]) {
 		return monoTextureUrls[def.img];
 
@@ -853,13 +853,13 @@ function exportLog() {
 }
 
 function updateSettings<
-	K extends keyof typeof defaultStore.state.dropAndFusion,
-	V extends typeof defaultStore.state.dropAndFusion[K],
+	K extends keyof typeof prefer.s['game.dropAndFusion'],
+	V extends typeof prefer.s['game.dropAndFusion'][K],
 >(key: K, value: V) {
 	const changes: { [P in K]?: V } = {};
 	changes[key] = value;
-	defaultStore.set('dropAndFusion', {
-		...defaultStore.state.dropAndFusion,
+	prefer.commit('game.dropAndFusion', {
+		...prefer.s['game.dropAndFusion'],
 		...changes,
 	});
 }
@@ -909,8 +909,8 @@ function getGameImageDriveFile() {
 				formData.append('name', `bubble-game-${Date.now()}.png`);
 				formData.append('isSensitive', 'false');
 				formData.append('i', $i.token);
-				if (defaultStore.state.uploadFolder) {
-					formData.append('folderId', defaultStore.state.uploadFolder);
+				if (prefer.s.uploadFolder) {
+					formData.append('folderId', prefer.s.uploadFolder);
 				}
 
 				window.fetch(apiUrl + '/drive/files/create', {
@@ -1229,7 +1229,7 @@ onDeactivated(() => {
 	bgmNodes?.soundSource.stop();
 });
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.bubbleGame,
 	icon: 'ti ti-apple',
 }));
diff --git a/packages/frontend/src/pages/drop-and-fusion.vue b/packages/frontend/src/pages/drop-and-fusion.vue
index 54352c9b0d..7f571a7c36 100644
--- a/packages/frontend/src/pages/drop-and-fusion.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.vue
@@ -88,12 +88,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue';
 import XGame from './drop-and-fusion.game.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 
 const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space'>('normal');
 const gameStarted = ref(false);
@@ -121,7 +121,7 @@ function onGameEnd() {
 	gameStarted.value = false;
 }
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.bubbleGame,
 	icon: 'ti ti-device-gamepad',
 }));
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index da3315cff5..581198c89d 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -87,11 +87,11 @@ import MkInput from '@/components/MkInput.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { customEmojiCategories } from '@/custom-emojis.js';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { selectFile } from '@/scripts/select-file.js';
+import { selectFile } from '@/utility/select-file.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index 979d50966e..35a240b9ba 100644
--- a/packages/frontend/src/pages/emojis.emoji.vue
+++ b/packages/frontend/src/pages/emojis.emoji.vue
@@ -18,8 +18,8 @@ import * as Misskey from 'misskey-js';
 import { defineAsyncComponent } from 'vue';
 import type { MenuItem } from '@/types/menu.js';
 import * as os from '@/os.js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+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';
diff --git a/packages/frontend/src/pages/explore.roles.vue b/packages/frontend/src/pages/explore.roles.vue
index 389cd23ad2..ffefeb9618 100644
--- a/packages/frontend/src/pages/explore.roles.vue
+++ b/packages/frontend/src/pages/explore.roles.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const roles = ref<Misskey.entities.Role[] | null>(null);
 
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index 56ae08b322..c112045a21 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -68,7 +68,7 @@ import * as Misskey from 'misskey-js';
 import MkUserList from '@/components/MkUserList.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkTab from '@/components/MkTab.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/pages/explore.vue b/packages/frontend/src/pages/explore.vue
index b1a8183d9b..d24ebe6aae 100644
--- a/packages/frontend/src/pages/explore.vue
+++ b/packages/frontend/src/pages/explore.vue
@@ -27,7 +27,7 @@ import XUsers from './explore.users.vue';
 import XRoles from './explore.roles.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
@@ -60,7 +60,7 @@ const headerTabs = computed(() => [{
 	title: i18n.ts.roles,
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.explore,
 	icon: 'ti ti-hash',
 }));
diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue
index 6716566101..548ed828f1 100644
--- a/packages/frontend/src/pages/favorites.vue
+++ b/packages/frontend/src/pages/favorites.vue
@@ -30,7 +30,7 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkNote from '@/components/MkNote.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { infoImageUrl } from '@/instance.js';
 
 const pagination = {
@@ -38,7 +38,7 @@ const pagination = {
 	limit: 10,
 };
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.favorites,
 	icon: 'ti ti-star',
 }));
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index d84ec4873b..b04974b7dc 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -46,9 +46,9 @@ import * as Misskey from 'misskey-js';
 import { AISCRIPT_VERSION } from '@syuilo/aiscript';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import MkInput from '@/components/MkInput.vue';
@@ -461,7 +461,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: flash.value ? `${i18n.ts._play.edit}: ${flash.value.title}` : i18n.ts._play.new,
 }));
 </script>
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index 2b85489706..3cd7c46c1e 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -46,7 +46,7 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
@@ -91,7 +91,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-heart',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'Play',
 	icon: 'ti ti-player-play',
 }));
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 6294a3f4a2..6bce6689d4 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -7,9 +7,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <MkStickyContainer>
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="700">
-		<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
+		<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
 			<div v-if="flash" :key="flash.id">
-				<Transition :name="defaultStore.state.animation ? 'zoom' : ''" mode="out-in">
+				<Transition :name="prefer.s.animation ? 'zoom' : ''" mode="out-in">
 					<div v-if="started" :class="$style.started">
 						<div class="main _panel">
 							<MkAsUi v-if="root" :component="root" :components="components"/>
@@ -67,23 +67,23 @@ import * as Misskey from 'misskey-js';
 import { Interpreter, Parser, values } from '@syuilo/aiscript';
 import { url } from '@@/js/config.js';
 import type { Ref } from 'vue';
-import type { AsUiComponent, AsUiRoot } from '@/scripts/aiscript/ui.js';
+import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
 import type { MenuItem } from '@/types/menu.js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkAsUi from '@/components/MkAsUi.vue';
-import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { registerAsUiLib } from '@/aiscript/ui.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkCode from '@/components/MkCode.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { $i } from '@/account.js';
-import { isSupportShare } from '@/scripts/navigator.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
+import { isSupportShare } from '@/utility/navigator.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { pleaseLogin } from '@/utility/please-login.js';
 
 const props = defineProps<{
 	id: string;
@@ -304,7 +304,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: flash.value ? flash.value.title : 'Play',
 	...flash.value ? {
 		avatar: flash.value.user,
diff --git a/packages/frontend/src/pages/follow-requests.vue b/packages/frontend/src/pages/follow-requests.vue
index 863ae018ba..bd48b882d2 100644
--- a/packages/frontend/src/pages/follow-requests.vue
+++ b/packages/frontend/src/pages/follow-requests.vue
@@ -52,7 +52,7 @@ import MkButton from '@/components/MkButton.vue';
 import { userPage, acct } from '@/filters/user.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { infoImageUrl } from '@/instance.js';
 import { $i } from '@/account.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
@@ -105,7 +105,7 @@ const headerTabs = computed(() => [
 
 const tab = ref($i?.isLocked ? 'list' : 'sent');
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.followRequests,
 	icon: 'ti ti-user-plus',
 }));
diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue
index 70f8b2c31d..9cd59d0aa5 100644
--- a/packages/frontend/src/pages/gallery/edit.vue
+++ b/packages/frontend/src/pages/gallery/edit.vue
@@ -45,10 +45,10 @@ import MkInput from '@/components/MkInput.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import FormSuspense from '@/components/form/suspense.vue';
-import { selectFiles } from '@/scripts/select-file.js';
+import { selectFiles } from '@/utility/select-file.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
@@ -122,7 +122,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: props.postId ? i18n.ts.edit : i18n.ts.postToGallery,
 	icon: 'ti ti-pencil',
 }));
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index f396fd2c0c..14b3f7bf3c 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -52,7 +52,7 @@ import MkFoldableSection from '@/components/MkFoldableSection.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
@@ -119,7 +119,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-edit',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.gallery,
 	icon: 'ti ti-icons',
 }));
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index feb4c60611..56ddb820cf 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="1000" :marginMin="16" :marginMax="32">
 		<div class="_root">
-			<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
+			<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
 				<div v-if="post" class="rkxwuolj">
 					<div class="files">
 						<div v-for="file in post.files" :key="file.id" class="file">
@@ -65,22 +65,22 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, ref, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
+import type { MenuItem } from '@/types/menu.js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkGalleryPostPreview from '@/components/MkGalleryPostPreview.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
-import { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { defaultStore } from '@/store.js';
+import { definePage } from '@/page.js';
+import { prefer } from '@/preferences.js';
 import { $i } from '@/account.js';
-import { isSupportShare } from '@/scripts/navigator.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { isSupportShare } from '@/utility/navigator.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { useRouter } from '@/router/supplier.js';
-import type { MenuItem } from '@/types/menu.js';
 
 const router = useRouter();
 
@@ -208,7 +208,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: post.value ? post.value.title : i18n.ts.gallery,
 	...post.value ? {
 		avatar: post.value.user,
diff --git a/packages/frontend/src/pages/games.vue b/packages/frontend/src/pages/games.vue
index 998b8be0f3..48adead383 100644
--- a/packages/frontend/src/pages/games.vue
+++ b/packages/frontend/src/pages/games.vue
@@ -25,9 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'Misskey Games',
 	icon: 'ti ti-device-gamepad',
 }));
diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue
index 58f8b865bb..2bd923e6d9 100644
--- a/packages/frontend/src/pages/install-extensions.vue
+++ b/packages/frontend/src/pages/install-extensions.vue
@@ -45,21 +45,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, computed, onActivated, onDeactivated, nextTick } from 'vue';
+import type { Extension } from '@/components/MkExtensionInstaller.vue';
+import type { AiScriptPluginMeta } from '@/plugin.js';
 import MkLoading from '@/components/global/MkLoading.vue';
 import MkExtensionInstaller from '@/components/MkExtensionInstaller.vue';
-import type { Extension } from '@/components/MkExtensionInstaller.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkUrl from '@/components/global/MkUrl.vue';
 import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js';
-import type { AiScriptPluginMeta } from '@/scripts/install-plugin.js';
-import { parseThemeCode, installTheme } from '@/scripts/install-theme.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { parsePluginMeta, installPlugin } from '@/plugin.js';
+import { parseThemeCode, installTheme } from '@/theme.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const uiPhase = ref<'fetching' | 'confirm' | 'error'>('fetching');
 const errorKV = ref<{
@@ -244,7 +244,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts._externalResourceInstaller.title,
 	icon: 'ti ti-download',
 }));
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index 35d1c7dcd1..eddeb4aba9 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -146,16 +146,16 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import number from '@/filters/number.js';
 import { iAmModerator, iAmAdmin } from '@/account.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import type { Paging } from '@/components/MkPagination.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 import { dateString } from '@/filters/date.js';
 import MkTextarea from '@/components/MkTextarea.vue';
 
@@ -299,7 +299,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-code',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: props.host,
 	icon: 'ti ti-server',
 }));
diff --git a/packages/frontend/src/pages/invite.vue b/packages/frontend/src/pages/invite.vue
index 49c0d931c5..8369927d85 100644
--- a/packages/frontend/src/pages/invite.vue
+++ b/packages/frontend/src/pages/invite.vue
@@ -38,12 +38,12 @@ import { computed, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkButton from '@/components/MkButton.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import type { Paging } from '@/components/MkPagination.vue';
 import MkInviteCode from '@/components/MkInviteCode.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { serverErrorImageUrl, instance } from '@/instance.js';
 import { $i } from '@/account.js';
 
@@ -92,7 +92,7 @@ async function update() {
 
 update();
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.invite,
 	icon: 'ti ti-user-plus',
 }));
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 0ff1854154..524db0da0b 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -37,12 +37,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { watch, computed, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import MkButton from '@/components/MkButton.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { serverErrorImageUrl } from '@/instance.js';
 
 const props = defineProps<{
@@ -101,7 +101,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: list.value ? list.value.name : i18n.ts.lists,
 	icon: 'ti ti-list',
 }));
diff --git a/packages/frontend/src/pages/lookup.vue b/packages/frontend/src/pages/lookup.vue
index 6f10c69640..e92ee0a4cc 100644
--- a/packages/frontend/src/pages/lookup.vue
+++ b/packages/frontend/src/pages/lookup.vue
@@ -22,9 +22,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { mainRouter } from '@/router/main.js';
 import MkButton from '@/components/MkButton.vue';
 
@@ -90,7 +90,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata({
+definePage({
 	title: i18n.ts.lookup,
 	icon: 'ti ti-world-search',
 });
diff --git a/packages/frontend/src/pages/miauth.vue b/packages/frontend/src/pages/miauth.vue
index ab060587c5..79437f3f17 100644
--- a/packages/frontend/src/pages/miauth.vue
+++ b/packages/frontend/src/pages/miauth.vue
@@ -36,8 +36,8 @@ import MkAnimBg from '@/components/MkAnimBg.vue';
 import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
 
 import { i18n } from '@/i18n.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 
 const props = defineProps<{
 	session: string;
@@ -77,7 +77,7 @@ function onDeny() {
 	authRoot.value?.showUI('denied');
 }
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'MiAuth',
 	icon: 'ti ti-apps',
 }));
diff --git a/packages/frontend/src/pages/my-antennas/create.vue b/packages/frontend/src/pages/my-antennas/create.vue
index 2b8518747f..42d8b7be4c 100644
--- a/packages/frontend/src/pages/my-antennas/create.vue
+++ b/packages/frontend/src/pages/my-antennas/create.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { antennasCache } from '@/cache.js';
 import { useRouter } from '@/router/supplier.js';
 import MkAntennaEditor from '@/components/MkAntennaEditor.vue';
@@ -29,7 +29,7 @@ function onAntennaCreated() {
 const headerActions = computed(() => []);
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.createAntenna,
 	icon: 'ti ti-antenna',
 }));
diff --git a/packages/frontend/src/pages/my-antennas/edit.vue b/packages/frontend/src/pages/my-antennas/edit.vue
index 9f927cd1a0..acd368b5e2 100644
--- a/packages/frontend/src/pages/my-antennas/edit.vue
+++ b/packages/frontend/src/pages/my-antennas/edit.vue
@@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkAntennaEditor from '@/components/MkAntennaEditor.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { antennasCache } from '@/cache.js';
 import { useRouter } from '@/router/supplier.js';
 
@@ -41,7 +41,7 @@ misskeyApi('antennas/show', { antennaId: props.antennaId }).then((antennaRespons
 const headerActions = computed(() => []);
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.editAntenna,
 	icon: 'ti ti-antenna',
 }));
diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue
index f387740728..03f4940e09 100644
--- a/packages/frontend/src/pages/my-antennas/index.vue
+++ b/packages/frontend/src/pages/my-antennas/index.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onActivated, computed } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { antennasCache } from '@/cache.js';
 import { infoImageUrl } from '@/instance.js';
 
@@ -55,7 +55,7 @@ const headerActions = computed(() => [{
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.manageAntennas,
 	icon: 'ti ti-antenna',
 }));
diff --git a/packages/frontend/src/pages/my-clips/index.vue b/packages/frontend/src/pages/my-clips/index.vue
index acf37a9a2f..247720d9ac 100644
--- a/packages/frontend/src/pages/my-clips/index.vue
+++ b/packages/frontend/src/pages/my-clips/index.vue
@@ -30,9 +30,9 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkClipPreview from '@/components/MkClipPreview.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { clipsCache } from '@/cache.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
@@ -100,7 +100,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-heart',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.clip,
 	icon: 'ti ti-paperclip',
 }));
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index 6cbcca73c2..0bc9b3f3c2 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -34,7 +34,7 @@ import MkButton from '@/components/MkButton.vue';
 import MkAvatars from '@/components/MkAvatars.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { userListsCache } from '@/cache.js';
 import { infoImageUrl } from '@/instance.js';
 import { signinRequired } from '@/account.js';
@@ -71,7 +71,7 @@ const headerActions = computed(() => [{
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.manageLists,
 	icon: 'ti ti-list',
 }));
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 69e404bd85..fdee890cfd 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -57,8 +57,8 @@ import { computed, ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { userPage } from '@/filters/user.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
@@ -67,15 +67,15 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkInput from '@/components/MkInput.vue';
 import { userListsCache } from '@/cache.js';
 import { signinRequired } from '@/account.js';
-import { defaultStore } from '@/store.js';
 import MkPagination from '@/components/MkPagination.vue';
 import { mainRouter } from '@/router/main.js';
+import { prefer } from '@/preferences.js';
 
 const $i = signinRequired();
 
 const {
 	enableInfiniteScroll,
-} = defaultStore.reactiveState;
+} = prefer.r;
 
 const props = defineProps<{
 	listId: string;
@@ -191,7 +191,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: list.value ? list.value.name : i18n.ts.lists,
 	icon: 'ti ti-list',
 }));
diff --git a/packages/frontend/src/pages/not-found.vue b/packages/frontend/src/pages/not-found.vue
index 6a2d01b6fa..3bba1159e9 100644
--- a/packages/frontend/src/pages/not-found.vue
+++ b/packages/frontend/src/pages/not-found.vue
@@ -15,8 +15,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
+import { definePage } from '@/page.js';
+import { pleaseLogin } from '@/utility/please-login.js';
 import { notFoundImageUrl } from '@/instance.js';
 
 const props = defineProps<{
@@ -31,7 +31,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.notFound,
 	icon: 'ti ti-alert-triangle',
 }));
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index 0791c1343b..6f53cba806 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="800">
 		<div>
-			<Transition :name="defaultStore.state.animation ? 'fade' : ''" mode="out-in">
+			<Transition :name="prefer.s.animation ? 'fade' : ''" mode="out-in">
 				<div v-if="note">
 					<div v-if="showNext" class="_margin">
 						<MkNotes class="" :pagination="showNext === 'channel' ? nextChannelPagination : nextUserPagination" :noGap="true" :disableAutoLoad="true"/>
@@ -56,14 +56,14 @@ import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
 import MkNotes from '@/components/MkNotes.vue';
 import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
 import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { dateString } from '@/filters/date.js';
 import MkClipPreview from '@/components/MkClipPreview.vue';
-import { defaultStore } from '@/store.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
-import { getAppearNote } from '@/scripts/get-appear-note.js';
+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';
 
@@ -165,7 +165,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.note,
 	...note.value ? {
 		subtitle: dateString(note.value.createdAt),
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index 46ee501c76..6c276d3fb3 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -29,7 +29,7 @@ import MkNotes from '@/components/MkNotes.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { notificationTypes } from '@@/js/const.js';
 
 const tab = ref('all');
@@ -94,7 +94,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-mail',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.notifications,
 	icon: 'ti ti-bell',
 }));
diff --git a/packages/frontend/src/pages/oauth.vue b/packages/frontend/src/pages/oauth.vue
index 860c884d13..e8b88b4bc9 100644
--- a/packages/frontend/src/pages/oauth.vue
+++ b/packages/frontend/src/pages/oauth.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import * as Misskey from 'misskey-js';
 import MkAnimBg from '@/components/MkAnimBg.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkAuthConfirm from '@/components/MkAuthConfirm.vue';
 
 const transactionIdMeta = document.querySelector<HTMLMetaElement>('meta[name="misskey:oauth:transaction-id"]');
@@ -75,7 +75,7 @@ function onDeny(token: string) {
 	doPost(token, 'deny');
 }
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'OAuth',
 	icon: 'ti ti-apps',
 }));
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
index c3ad6657b0..1b98425719 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.image.vue
@@ -26,7 +26,7 @@ import * as Misskey from 'misskey-js';
 import XContainer from '../page-editor.container.vue';
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
index 36e03b4790..f275ec9517 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.note.vue
@@ -30,7 +30,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkNote from '@/components/MkNote.vue';
 import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
index 3fed07f7e8..4d1a3716e7 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.section.vue
@@ -21,14 +21,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
- 
+
 import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { v4 as uuid } from 'uuid';
 import XContainer from '../page-editor.container.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 import MkButton from '@/components/MkButton.vue';
 import { getPageBlockList } from '@/pages/page-editor/common.js';
 
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
index 5795b46c00..a8b4da2a06 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
@@ -15,12 +15,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
- 
+
 import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import XContainer from '../page-editor.container.vue';
 import { i18n } from '@/i18n.js';
-import { Autocomplete } from '@/scripts/autocomplete.js';
+import { Autocomplete } from '@/utility/autocomplete.js';
 
 const props = defineProps<{
 	modelValue: Misskey.entities.PageBlock & { type: 'text' }
diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue
index c08cfebab3..ed701ed3c0 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.vue
@@ -71,10 +71,10 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkInput from '@/components/MkInput.vue';
 import { url } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { selectFile } from '@/scripts/select-file.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { selectFile } from '@/utility/select-file.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { $i } from '@/account.js';
 import { mainRouter } from '@/router/main.js';
 import { getPageBlockList } from '@/pages/page-editor/common.js';
@@ -264,7 +264,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-note',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: props.initPageId ? i18n.ts._pages.editPage
 				: props.initPageName && props.initUser ? i18n.ts._pages.readPage
 				: i18n.ts._pages.newPage,
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index d9ad7babb7..1c288442b5 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -8,10 +8,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
 	<MkSpacer :contentMax="800">
 		<Transition
-			:enterActiveClass="defaultStore.state.animation ? $style.fadeEnterActive : ''"
-			:leaveActiveClass="defaultStore.state.animation ? $style.fadeLeaveActive : ''"
-			:enterFromClass="defaultStore.state.animation ? $style.fadeEnterFrom : ''"
-			:leaveToClass="defaultStore.state.animation ? $style.fadeLeaveTo : ''"
+			:enterActiveClass="prefer.s.animation ? $style.fadeEnterActive : ''"
+			:leaveActiveClass="prefer.s.animation ? $style.fadeLeaveActive : ''"
+			:enterFromClass="prefer.s.animation ? $style.fadeEnterFrom : ''"
+			:leaveToClass="prefer.s.animation ? $style.fadeLeaveTo : ''"
 			mode="out-in"
 		>
 			<div v-if="page" :key="page.id" class="_gaps">
@@ -100,11 +100,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, ref, defineAsyncComponent } from 'vue';
 import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
+import type { MenuItem } from '@/types/menu.js';
 import XPage from '@/components/page/page.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { url } from '@@/js/config.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkMediaImage from '@/components/MkMediaImage.vue';
 import MkImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
 import MkFollowButton from '@/components/MkFollowButton.vue';
@@ -112,16 +113,16 @@ import MkContainer from '@/components/MkContainer.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import MkPagePreview from '@/components/MkPagePreview.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { pageViewInterruptors, defaultStore } from '@/store.js';
-import { deepClone } from '@/scripts/clone.js';
+import { definePage } from '@/page.js';
+import { deepClone } from '@/utility/clone.js';
 import { $i } from '@/account.js';
-import { isSupportShare } from '@/scripts/navigator.js';
+import { isSupportShare } from '@/utility/navigator.js';
 import { instance } from '@/instance.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { useRouter } from '@/router/supplier.js';
-import type { MenuItem } from '@/types/menu.js';
+import { prefer } from '@/preferences.js';
+import { getPluginHandlers } from '@/plugin.js';
 
 const router = useRouter();
 
@@ -150,6 +151,7 @@ function fetchPage() {
 		page.value = _page;
 
 		// plugin
+		const pageViewInterruptors = getPluginHandlers('page_view_interruptor');
 		if (pageViewInterruptors.length > 0) {
 			let result = deepClone(_page);
 			for (const interruptor of pageViewInterruptors) {
@@ -318,7 +320,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: page.value ? page.value.title || page.value.name : i18n.ts.pages,
 	...page.value ? {
 		avatar: page.value.user,
diff --git a/packages/frontend/src/pages/pages.vue b/packages/frontend/src/pages/pages.vue
index 4ef9d3b091..f9bb825bd0 100644
--- a/packages/frontend/src/pages/pages.vue
+++ b/packages/frontend/src/pages/pages.vue
@@ -44,7 +44,7 @@ import MkPagination from '@/components/MkPagination.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
@@ -88,7 +88,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-heart',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.pages,
 	icon: 'ti ti-note',
 }));
diff --git a/packages/frontend/src/pages/preview.vue b/packages/frontend/src/pages/preview.vue
index 8e07b190aa..78167500f4 100644
--- a/packages/frontend/src/pages/preview.vue
+++ b/packages/frontend/src/pages/preview.vue
@@ -13,13 +13,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import MkSample from '@/components/MkPreview.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(computed(() => ({
+definePage(computed(() => ({
 	title: i18n.ts.preview,
 	icon: 'ti ti-eye',
 })));
diff --git a/packages/frontend/src/pages/registry.keys.vue b/packages/frontend/src/pages/registry.keys.vue
index 4cacbd0906..b2d0f1175d 100644
--- a/packages/frontend/src/pages/registry.keys.vue
+++ b/packages/frontend/src/pages/registry.keys.vue
@@ -36,9 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { watch, computed, ref } from 'vue';
 import JSON5 from 'json5';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import MkButton from '@/components/MkButton.vue';
@@ -96,7 +96,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.registry,
 	icon: 'ti ti-adjustments',
 }));
diff --git a/packages/frontend/src/pages/registry.value.vue b/packages/frontend/src/pages/registry.value.vue
index c40d13f664..fd0efa0d8e 100644
--- a/packages/frontend/src/pages/registry.value.vue
+++ b/packages/frontend/src/pages/registry.value.vue
@@ -48,9 +48,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { watch, computed, ref } from 'vue';
 import JSON5 from 'json5';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkButton from '@/components/MkButton.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
@@ -123,7 +123,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.registry,
 	icon: 'ti ti-adjustments',
 }));
diff --git a/packages/frontend/src/pages/registry.vue b/packages/frontend/src/pages/registry.vue
index c641874b17..0ae2efb24d 100644
--- a/packages/frontend/src/pages/registry.vue
+++ b/packages/frontend/src/pages/registry.vue
@@ -26,9 +26,9 @@ import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import JSON5 from 'json5';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import FormLink from '@/components/form/link.vue';
 import FormSection from '@/components/form/section.vue';
 import MkButton from '@/components/MkButton.vue';
@@ -73,7 +73,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.registry,
 	icon: 'ti ti-adjustments',
 }));
diff --git a/packages/frontend/src/pages/reset-password.vue b/packages/frontend/src/pages/reset-password.vue
index 6d24029535..d84c8f33dd 100644
--- a/packages/frontend/src/pages/reset-password.vue
+++ b/packages/frontend/src/pages/reset-password.vue
@@ -25,7 +25,7 @@ import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { mainRouter } from '@/router/main.js';
 
 const props = defineProps<{
@@ -55,7 +55,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.resetPassword,
 	icon: 'ti ti-lock',
 }));
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 429f502133..71dd220cfe 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -148,16 +148,16 @@ import * as Reversi from 'misskey-reversi';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 import { useInterval } from '@@/js/use-interval.js';
 import { signinRequired } from '@/account.js';
 import { url } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { userPage } from '@/filters/user.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 import * as os from '@/os.js';
-import { confetti } from '@/scripts/confetti.js';
+import { confetti } from '@/utility/confetti.js';
 
 const $i = signinRequired();
 
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index 437a1a2294..03b75f89ae 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -115,7 +115,7 @@ import * as Misskey from 'misskey-js';
 import * as Reversi from 'misskey-reversi';
 import { i18n } from '@/i18n.js';
 import { signinRequired } from '@/account.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 import MkButton from '@/components/MkButton.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
diff --git a/packages/frontend/src/pages/reversi/game.vue b/packages/frontend/src/pages/reversi/game.vue
index 10ea3717ab..053ec2aa08 100644
--- a/packages/frontend/src/pages/reversi/game.vue
+++ b/packages/frontend/src/pages/reversi/game.vue
@@ -14,8 +14,8 @@ import { computed, watch, ref, onMounted, shallowRef, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import GameSetting from './game.setting.vue';
 import GameBoard from './game.board.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import { useStream } from '@/stream.js';
 import { signinRequired } from '@/account.js';
 import { useRouter } from '@/router/supplier.js';
@@ -114,7 +114,7 @@ onUnmounted(() => {
 	}
 });
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'Reversi',
 	icon: 'ti ti-device-gamepad',
 }));
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index d608a2411c..ff2e7e922f 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -107,8 +107,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onDeactivated, onMounted, onUnmounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import { useStream } from '@/stream.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
@@ -118,8 +118,8 @@ import MkPagination from '@/components/MkPagination.vue';
 import { useRouter } from '@/router/supplier.js';
 import * as os from '@/os.js';
 import { useInterval } from '@@/js/use-interval.js';
-import { pleaseLogin } from '@/scripts/please-login.js';
-import * as sound from '@/scripts/sound.js';
+import { pleaseLogin } from '@/utility/please-login.js';
+import * as sound from '@/utility/sound.js';
 
 const myGamesPagination = {
 	endpoint: 'reversi/games' as const,
@@ -261,7 +261,7 @@ onUnmounted(() => {
 	cancelMatching();
 });
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'Reversi',
 	icon: 'ti ti-device-gamepad',
 }));
diff --git a/packages/frontend/src/pages/role.vue b/packages/frontend/src/pages/role.vue
index 46e510b49b..187b675346 100644
--- a/packages/frontend/src/pages/role.vue
+++ b/packages/frontend/src/pages/role.vue
@@ -38,9 +38,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkUserList from '@/components/MkUserList.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { instanceName } from '@@/js/config.js';
@@ -93,7 +93,7 @@ const headerTabs = computed(() => [{
 	title: i18n.ts.timeline,
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: role.value ? role.value.name : (error.value ?? i18n.ts.role),
 	icon: 'ti ti-badge',
 }));
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 22c06bfc85..ed5cd23b23 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -64,18 +64,18 @@ import MkContainer from '@/components/MkContainer.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import * as os from '@/os.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
-import type { AsUiComponent } from '@/scripts/aiscript/ui.js';
+import { definePage } from '@/page.js';
+import { registerAsUiLib } from '@/aiscript/ui.js';
+import type { AsUiComponent } from '@/aiscript/ui.js';
 import MkAsUi from '@/components/MkAsUi.vue';
 import { miLocalStorage } from '@/local-storage.js';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 
-import type { AsUiRoot } from '@/scripts/aiscript/ui.js';
+import type { AsUiRoot } from '@/aiscript/ui.js';
 
 const parser = new Parser();
 let aiscript: Interpreter;
@@ -202,7 +202,7 @@ const showns = computed(() => {
 	return result;
 });
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.scratchpad,
 	icon: 'ti ti-terminal-2',
 }));
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index e33471f2b1..7f6b7e445f 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -119,8 +119,8 @@ import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { apLookup } from '@/scripts/lookup.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { apLookup } from '@/utility/lookup.js';
 import { useRouter } from '@/router/supplier.js';
 import MkButton from '@/components/MkButton.vue';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
diff --git a/packages/frontend/src/pages/search.user.vue b/packages/frontend/src/pages/search.user.vue
index 108519070b..5a4e38e2ea 100644
--- a/packages/frontend/src/pages/search.user.vue
+++ b/packages/frontend/src/pages/search.user.vue
@@ -36,7 +36,7 @@ import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import * as os from '@/os.js';
 import MkFoldableSection from '@/components/MkFoldableSection.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useRouter } from '@/router/supplier.js';
 
 const props = withDefaults(defineProps<{
diff --git a/packages/frontend/src/pages/search.vue b/packages/frontend/src/pages/search.vue
index 38d7548fa8..d4601df752 100644
--- a/packages/frontend/src/pages/search.vue
+++ b/packages/frontend/src/pages/search.vue
@@ -27,8 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, defineAsyncComponent, ref, toRef } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { notesSearchAvailable } from '@/scripts/check-permissions.js';
+import { definePage } from '@/page.js';
+import { notesSearchAvailable } from '@/utility/check-permissions.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 
@@ -68,7 +68,7 @@ const headerTabs = computed(() => [{
 	icon: 'ti ti-users',
 }]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.search,
 	icon: 'ti ti-search',
 }));
diff --git a/packages/frontend/src/pages/settings/2fa.qrdialog.vue b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
index 18c82ffdf6..9093ffd7a9 100644
--- a/packages/frontend/src/pages/settings/2fa.qrdialog.vue
+++ b/packages/frontend/src/pages/settings/2fa.qrdialog.vue
@@ -116,7 +116,7 @@ import * as os from '@/os.js';
 import MkFolder from '@/components/MkFolder.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkLink from '@/components/MkLink.vue';
-import { confetti } from '@/scripts/confetti.js';
+import { confetti } from '@/utility/confetti.js';
 import { signinRequired } from '@/account.js';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/pages/settings/accessibility.vue b/packages/frontend/src/pages/settings/accessibility.vue
index b703be1fe1..f22e45ce1f 100644
--- a/packages/frontend/src/pages/settings/accessibility.vue
+++ b/packages/frontend/src/pages/settings/accessibility.vue
@@ -8,49 +8,63 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div class="_gaps_m">
 		<div class="_gaps_s">
 			<SearchMarker :keywords="['animation', 'motion', 'reduce']">
-				<MkSwitch v-model="reduceAnimation">
-					<template #label><SearchLabel>{{ i18n.ts.reduceUiAnimation }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="animation">
+					<MkSwitch v-model="reduceAnimation">
+						<template #label><SearchLabel>{{ i18n.ts.reduceUiAnimation }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 
 			<SearchMarker :keywords="['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif']">
-				<MkSwitch v-model="disableShowingAnimatedImages">
-					<template #label><SearchLabel>{{ i18n.ts.disableShowingAnimatedImages }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="disableShowingAnimatedImages">
+					<MkSwitch v-model="disableShowingAnimatedImages">
+						<template #label><SearchLabel>{{ i18n.ts.disableShowingAnimatedImages }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 
 			<SearchMarker :keywords="['mfm', 'enable', 'show', 'animated']">
-				<MkSwitch v-model="animatedMfm">
-					<template #label><SearchLabel>{{ i18n.ts.enableAnimatedMfm }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="animatedMfm">
+					<MkSwitch v-model="animatedMfm">
+						<template #label><SearchLabel>{{ i18n.ts.enableAnimatedMfm }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 
 			<SearchMarker :keywords="['swipe', 'horizontal', 'tab']">
-				<MkSwitch v-model="enableHorizontalSwipe">
-					<template #label><SearchLabel>{{ i18n.ts.enableHorizontalSwipe }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="enableHorizontalSwipe">
+					<MkSwitch v-model="enableHorizontalSwipe">
+						<template #label><SearchLabel>{{ i18n.ts.enableHorizontalSwipe }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 
 			<SearchMarker :keywords="['keep', 'screen', 'display', 'on']">
-				<MkSwitch v-model="keepScreenOn">
-					<template #label><SearchLabel>{{ i18n.ts.keepScreenOn }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="keepScreenOn">
+					<MkSwitch v-model="keepScreenOn">
+						<template #label><SearchLabel>{{ i18n.ts.keepScreenOn }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 
 			<SearchMarker :keywords="['native', 'system', 'video', 'audio', 'player', 'media']">
-				<MkSwitch v-model="useNativeUIForVideoAudioPlayer">
-					<template #label><SearchLabel>{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</SearchLabel></template>
-				</MkSwitch>
+				<MkPreferenceContainer k="useNativeUiForVideoAudioPlayer">
+					<MkSwitch v-model="useNativeUiForVideoAudioPlayer">
+						<template #label><SearchLabel>{{ i18n.ts.useNativeUIForVideoAudioPlayer }}</SearchLabel></template>
+					</MkSwitch>
+				</MkPreferenceContainer>
 			</SearchMarker>
 		</div>
 
 		<SearchMarker :keywords="['contextmenu', 'system', 'native']">
-			<MkSelect v-model="contextMenu">
-				<template #label><SearchLabel>{{ i18n.ts._contextMenu.title }}</SearchLabel></template>
-				<option value="app">{{ i18n.ts._contextMenu.app }}</option>
-				<option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option>
-				<option value="native">{{ i18n.ts._contextMenu.native }}</option>
-			</MkSelect>
+			<MkPreferenceContainer k="contextMenu">
+				<MkSelect v-model="contextMenu">
+					<template #label><SearchLabel>{{ i18n.ts._contextMenu.title }}</SearchLabel></template>
+					<option value="app">{{ i18n.ts._contextMenu.app }}</option>
+					<option value="appWithShift">{{ i18n.ts._contextMenu.appWithShift }}</option>
+					<option value="native">{{ i18n.ts._contextMenu.native }}</option>
+				</MkSelect>
+			</MkPreferenceContainer>
 		</SearchMarker>
 	</div>
 </SearchMarker>
@@ -60,18 +74,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref, watch } from 'vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
-import { defaultStore } from '@/store.js';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { prefer } from '@/preferences.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 
-const reduceAnimation = computed(defaultStore.makeGetterSetter('animation', v => !v, v => !v));
-const animatedMfm = computed(defaultStore.makeGetterSetter('animatedMfm'));
-const disableShowingAnimatedImages = computed(defaultStore.makeGetterSetter('disableShowingAnimatedImages'));
-const keepScreenOn = computed(defaultStore.makeGetterSetter('keepScreenOn'));
-const enableHorizontalSwipe = computed(defaultStore.makeGetterSetter('enableHorizontalSwipe'));
-const useNativeUIForVideoAudioPlayer = computed(defaultStore.makeGetterSetter('useNativeUIForVideoAudioPlayer'));
-const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu'));
+const reduceAnimation = prefer.model('animation', v => !v, v => !v);
+const animatedMfm = prefer.model('animatedMfm');
+const disableShowingAnimatedImages = prefer.model('disableShowingAnimatedImages');
+const keepScreenOn = prefer.model('keepScreenOn');
+const enableHorizontalSwipe = prefer.model('enableHorizontalSwipe');
+const useNativeUiForVideoAudioPlayer = prefer.model('useNativeUiForVideoAudioPlayer');
+const contextMenu = prefer.model('contextMenu');
 
 watch([
 	keepScreenOn,
@@ -84,7 +99,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.accessibility,
 	icon: 'ti ti-accessible',
 }));
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 4a7301f405..2cf65be2d0 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -33,10 +33,10 @@ import * as Misskey from 'misskey-js';
 import FormSuspense from '@/components/form/suspense.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { getAccounts, removeAccount as _removeAccount, login, $i, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import type { MenuItem } from '@/types/menu.js';
 
@@ -128,7 +128,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.accounts,
 	icon: 'ti ti-users',
 }));
diff --git a/packages/frontend/src/pages/settings/api.vue b/packages/frontend/src/pages/settings/api.vue
index b35d406a98..e41a7de0de 100644
--- a/packages/frontend/src/pages/settings/api.vue
+++ b/packages/frontend/src/pages/settings/api.vue
@@ -16,9 +16,9 @@ import { defineAsyncComponent, ref, computed } from 'vue';
 import FormLink from '@/components/form/link.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const isDesktop = ref(window.innerWidth >= 1100);
 
@@ -46,7 +46,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'API',
 	icon: 'ti ti-api',
 }));
diff --git a/packages/frontend/src/pages/settings/appearance.vue b/packages/frontend/src/pages/settings/appearance.vue
index 465c2a38c2..6f8eb34d37 100644
--- a/packages/frontend/src/pages/settings/appearance.vue
+++ b/packages/frontend/src/pages/settings/appearance.vue
@@ -10,73 +10,85 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div class="_gaps_m">
 				<div class="_gaps_s">
 					<SearchMarker :keywords="['blur']">
-						<MkSwitch v-model="useBlurEffect">
-							<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="useBlurEffect">
+							<MkSwitch v-model="useBlurEffect">
+								<template #label><SearchLabel>{{ i18n.ts.useBlurEffect }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['blur', 'modal']">
-						<MkSwitch v-model="useBlurEffectForModal">
-							<template #label><SearchLabel>{{ i18n.ts.useBlurEffectForModal }}</SearchLabel></template>
-						</MkSwitch>
+						<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']">
-						<MkSwitch v-model="highlightSensitiveMedia">
-							<template #label><SearchLabel>{{ i18n.ts.highlightSensitiveMedia }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="highlightSensitiveMedia">
+							<MkSwitch v-model="highlightSensitiveMedia">
+								<template #label><SearchLabel>{{ i18n.ts.highlightSensitiveMedia }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['avatar', 'icon', 'square']">
-						<MkSwitch v-model="squareAvatars">
-							<template #label><SearchLabel>{{ i18n.ts.squareAvatars }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="squareAvatars">
+							<MkSwitch v-model="squareAvatars">
+								<template #label><SearchLabel>{{ i18n.ts.squareAvatars }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['avatar', 'icon', 'decoration', 'show']">
-						<MkSwitch v-model="showAvatarDecorations">
-							<template #label><SearchLabel>{{ i18n.ts.showAvatarDecorations }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="showAvatarDecorations">
+							<MkSwitch v-model="showAvatarDecorations">
+								<template #label><SearchLabel>{{ i18n.ts.showAvatarDecorations }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['note', 'timeline', 'gap']">
-						<MkSwitch v-model="showGapBetweenNotesInTimeline">
-							<template #label><SearchLabel>{{ i18n.ts.showGapBetweenNotesInTimeline }}</SearchLabel></template>
-						</MkSwitch>
-					</SearchMarker>
-
-					<SearchMarker :keywords="['font', 'system', 'native']">
-						<MkSwitch v-model="useSystemFont">
-							<template #label><SearchLabel>{{ i18n.ts.useSystemFont }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="showGapBetweenNotesInTimeline">
+							<MkSwitch v-model="showGapBetweenNotesInTimeline">
+								<template #label><SearchLabel>{{ i18n.ts.showGapBetweenNotesInTimeline }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker :keywords="['effect', 'show']">
-						<MkSwitch v-model="enableSeasonalScreenEffect">
-							<template #label><SearchLabel>{{ i18n.ts.seasonalScreenEffect }}</SearchLabel></template>
-						</MkSwitch>
+						<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']">
-					<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 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']">
-					<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 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']">
@@ -88,6 +100,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<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>
 
@@ -97,46 +115,56 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<div class="_gaps_m">
 					<SearchMarker :keywords="['reaction', 'size', 'scale', 'display']">
-						<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 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']">
-						<MkSwitch v-model="limitWidthOfReaction">
-							<template #label><SearchLabel>{{ i18n.ts.limitWidthOfReaction }}</SearchLabel></template>
-						</MkSwitch>
+						<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']">
-						<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 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']">
-						<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 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']">
-						<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 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>
@@ -148,21 +176,25 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<div class="_gaps_m">
 					<SearchMarker :keywords="['position']">
-						<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 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']">
-						<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 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>
@@ -183,37 +215,38 @@ 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 { defaultStore } from '@/store.js';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { prefer } from '@/preferences.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.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 '@/scripts/achievements.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';
 
 const fontSize = ref(miLocalStorage.getItem('fontSize'));
 const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
 
-const showAvatarDecorations = computed(defaultStore.makeGetterSetter('showAvatarDecorations'));
-const emojiStyle = computed(defaultStore.makeGetterSetter('emojiStyle'));
-const menuStyle = computed(defaultStore.makeGetterSetter('menuStyle'));
-const useBlurEffectForModal = computed(defaultStore.makeGetterSetter('useBlurEffectForModal'));
-const useBlurEffect = computed(defaultStore.makeGetterSetter('useBlurEffect'));
-const highlightSensitiveMedia = computed(defaultStore.makeGetterSetter('highlightSensitiveMedia'));
-const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
-const enableSeasonalScreenEffect = computed(defaultStore.makeGetterSetter('enableSeasonalScreenEffect'));
-const showGapBetweenNotesInTimeline = computed(defaultStore.makeGetterSetter('showGapBetweenNotesInTimeline'));
-const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance'));
-const reactionsDisplaySize = computed(defaultStore.makeGetterSetter('reactionsDisplaySize'));
-const limitWidthOfReaction = computed(defaultStore.makeGetterSetter('limitWidthOfReaction'));
-const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
-const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
-const nsfw = computed(defaultStore.makeGetterSetter('nsfw'));
-const instanceTicker = computed(defaultStore.makeGetterSetter('instanceTicker'));
+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) {
@@ -280,7 +313,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.appearance,
 	icon: 'ti ti-device-desktop',
 }));
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index 6515503505..626e14b427 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -57,9 +57,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import FormPagination from '@/components/MkPagination.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
@@ -86,7 +86,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.installedApps,
 	icon: 'ti ti-plug',
 }));
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue
index 79be2b9b1e..ba25eee175 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.vue
@@ -52,11 +52,11 @@ import * as Misskey from 'misskey-js';
 import XDecoration from './avatar-decoration.decoration.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { signinRequired } from '@/account.js';
 import MkInfo from '@/components/MkInfo.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const $i = signinRequired();
 
@@ -131,7 +131,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.avatarDecorations,
 	icon: 'ti ti-sparkles',
 }));
diff --git a/packages/frontend/src/pages/settings/custom-css.vue b/packages/frontend/src/pages/settings/custom-css.vue
index cf05e75acc..9b0e04860e 100644
--- a/packages/frontend/src/pages/settings/custom-css.vue
+++ b/packages/frontend/src/pages/settings/custom-css.vue
@@ -18,9 +18,9 @@ import { ref, watch, computed } from 'vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import FormInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { miLocalStorage } from '@/local-storage.js';
 
 const localCustomCss = ref(miLocalStorage.getItem('customCss') ?? '');
@@ -45,7 +45,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.customCss,
 	icon: 'ti ti-code',
 }));
diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue
index e574ec7dc0..e7c5c942e9 100644
--- a/packages/frontend/src/pages/settings/deck.vue
+++ b/packages/frontend/src/pages/settings/deck.vue
@@ -23,20 +23,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
-import { deckStore } from '@/ui/deck/deck-store.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
+import { prefer } from '@/preferences.js';
 
-const navWindow = computed(deckStore.makeGetterSetter('navWindow'));
-const useSimpleUiForNonRootPages = computed(deckStore.makeGetterSetter('useSimpleUiForNonRootPages'));
-const alwaysShowMainColumn = computed(deckStore.makeGetterSetter('alwaysShowMainColumn'));
-const columnAlign = computed(deckStore.makeGetterSetter('columnAlign'));
+const navWindow = prefer.model('deck.navWindow');
+const useSimpleUiForNonRootPages = prefer.model('deck.useSimpleUiForNonRootPages');
+const alwaysShowMainColumn = prefer.model('deck.alwaysShowMainColumn');
+const columnAlign = prefer.model('deck.columnAlign');
 
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.deck,
 	icon: 'ti ti-columns',
 }));
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index c5657fce68..6b73560174 100644
--- a/packages/frontend/src/pages/settings/drive-cleaner.vue
+++ b/packages/frontend/src/pages/settings/drive-cleaner.vue
@@ -52,14 +52,14 @@ import { computed, ref, watch } from 'vue';
 import type { StyleValue } from 'vue';
 import tinycolor from 'tinycolor2';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkPagination from '@/components/MkPagination.vue';
 import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
 import { i18n } from '@/i18n.js';
 import bytes from '@/filters/bytes.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkSelect from '@/components/MkSelect.vue';
-import { getDriveFileMenu } from '@/scripts/get-drive-file-menu.js';
+import { getDriveFileMenu } from '@/utility/get-drive-file-menu.js';
 
 const sortMode = ref('+size');
 const pagination = {
@@ -118,7 +118,7 @@ function onContextMenu(ev: MouseEvent, file): void {
 	os.contextMenu(getDriveFileMenu(file), ev);
 }
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.drivecleaner,
 	icon: 'ti ti-trash',
 }));
diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue
index 0138aac1c5..8cc70f177f 100644
--- a/packages/frontend/src/pages/settings/drive.vue
+++ b/packages/frontend/src/pages/settings/drive.vue
@@ -50,17 +50,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 				</FormLink>
 
 				<SearchMarker :keywords="['keep', 'original', 'raw', 'upload']">
-					<MkSwitch v-model="keepOriginalUploading">
-						<template #label><SearchLabel>{{ i18n.ts.keepOriginalUploading }}</SearchLabel></template>
-						<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalUploadingDescription }}</SearchKeyword></template>
-					</MkSwitch>
+					<MkPreferenceContainer k="keepOriginalUploading">
+						<MkSwitch v-model="keepOriginalUploading">
+							<template #label><SearchLabel>{{ i18n.ts.keepOriginalUploading }}</SearchLabel></template>
+							<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalUploadingDescription }}</SearchKeyword></template>
+						</MkSwitch>
+					</MkPreferenceContainer>
 				</SearchMarker>
 
 				<SearchMarker :keywords="['keep', 'original', 'filename']">
-					<MkSwitch v-model="keepOriginalFilename">
-						<template #label><SearchLabel>{{ i18n.ts.keepOriginalFilename }}</SearchLabel></template>
-						<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchKeyword></template>
-					</MkSwitch>
+					<MkPreferenceContainer k="keepOriginalFilename">
+						<MkSwitch v-model="keepOriginalFilename">
+							<template #label><SearchLabel>{{ i18n.ts.keepOriginalFilename }}</SearchLabel></template>
+							<template #caption><SearchKeyword>{{ i18n.ts.keepOriginalFilenameDescription }}</SearchKeyword></template>
+						</MkSwitch>
+					</MkPreferenceContainer>
 				</SearchMarker>
 
 				<SearchMarker :keywords="['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file']">
@@ -91,13 +95,14 @@ import FormSection from '@/components/form/section.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import FormSplit from '@/components/form/split.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import bytes from '@/filters/bytes.js';
-import { defaultStore } from '@/store.js';
 import MkChart from '@/components/MkChart.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { signinRequired } from '@/account.js';
+import { prefer } from '@/preferences.js';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 
 const $i = signinRequired();
 
@@ -120,8 +125,8 @@ const meterStyle = computed(() => {
 	};
 });
 
-const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading'));
-const keepOriginalFilename = computed(defaultStore.makeGetterSetter('keepOriginalFilename'));
+const keepOriginalUploading = prefer.model('keepOriginalUploading');
+const keepOriginalFilename = prefer.model('keepOriginalFilename');
 
 misskeyApi('drive').then(info => {
 	capacity.value = info.capacity;
@@ -129,9 +134,9 @@ misskeyApi('drive').then(info => {
 	fetching.value = false;
 });
 
-if (defaultStore.state.uploadFolder) {
+if (prefer.s.uploadFolder) {
 	misskeyApi('drive/folders/show', {
-		folderId: defaultStore.state.uploadFolder,
+		folderId: prefer.s.uploadFolder,
 	}).then(response => {
 		uploadFolder.value = response;
 	});
@@ -139,11 +144,11 @@ if (defaultStore.state.uploadFolder) {
 
 function chooseUploadFolder() {
 	os.selectDriveFolder(false).then(async folder => {
-		defaultStore.set('uploadFolder', folder[0] ? folder[0].id : null);
+		prefer.commit('uploadFolder', folder[0] ? folder[0].id : null);
 		os.success();
-		if (defaultStore.state.uploadFolder) {
+		if (prefer.s.uploadFolder) {
 			uploadFolder.value = await misskeyApi('drive/folders/show', {
-				folderId: defaultStore.state.uploadFolder,
+				folderId: prefer.s.uploadFolder,
 			});
 		} else {
 			uploadFolder.value = null;
@@ -169,7 +174,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.drive,
 	icon: 'ti ti-cloud',
 }));
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index e7a8fc5634..0cbda44882 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -66,10 +66,10 @@ import MkInput from '@/components/MkInput.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkDisableSection from '@/components/MkDisableSection.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { signinRequired } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { instance } from '@/instance.js';
 
 const $i = signinRequired();
@@ -125,7 +125,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.email,
 	icon: 'ti ti-mail',
 }));
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
index bd5384df7c..cc648aa1df 100644
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ b/packages/frontend/src/pages/settings/emoji-picker.vue
@@ -100,37 +100,45 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<template #label>{{ i18n.ts.emojiPickerDisplay }}</template>
 
 		<div class="_gaps_m">
-			<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 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>
 
-			<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 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>
 
-			<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 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>
 
-			<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 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>
@@ -138,31 +146,33 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue';
-import type { Ref } 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 FromSlot from '@/components/form/slot.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { deepClone } from '@/scripts/clone.js';
-import { reactionPicker } from '@/scripts/reaction-picker.js';
-import { emojiPicker } from '@/scripts/emoji-picker.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(defaultStore.state.reactions));
-const pinnedEmojis: Ref<string[]> = ref(deepClone(defaultStore.state.pinnedEmojis));
+const pinnedEmojisForReaction: Ref<string[]> = ref(deepClone(store.s.reactions));
+const pinnedEmojis: Ref<string[]> = ref(deepClone(store.s.pinnedEmojis));
 
-const emojiPickerScale = computed(defaultStore.makeGetterSetter('emojiPickerScale'));
-const emojiPickerWidth = computed(defaultStore.makeGetterSetter('emojiPickerWidth'));
-const emojiPickerHeight = computed(defaultStore.makeGetterSetter('emojiPickerHeight'));
-const emojiPickerStyle = computed(defaultStore.makeGetterSetter('emojiPickerStyle'));
+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);
@@ -224,7 +234,7 @@ async function setDefault(itemsRef: Ref<string[]>) {
 	});
 	if (canceled) return;
 
-	itemsRef.value = deepClone(defaultStore.def.reactions.default);
+	itemsRef.value = deepClone(store.def.reactions.default);
 }
 
 async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) {
@@ -256,18 +266,18 @@ function getHTMLElement(ev: MouseEvent): HTMLElement {
 }
 
 watch(pinnedEmojisForReaction, () => {
-	defaultStore.set('reactions', pinnedEmojisForReaction.value);
+	store.set('reactions', pinnedEmojisForReaction.value);
 }, {
 	deep: true,
 });
 
 watch(pinnedEmojis, () => {
-	defaultStore.set('pinnedEmojis', pinnedEmojis.value);
+	store.set('pinnedEmojis', pinnedEmojis.value);
 }, {
 	deep: true,
 });
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.emojiPicker,
 	icon: 'ti ti-mood-happy',
 }));
diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue
index 2ed780c3d3..9d6ccc37c5 100644
--- a/packages/frontend/src/pages/settings/import-export.vue
+++ b/packages/frontend/src/pages/settings/import-export.vue
@@ -185,17 +185,17 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { selectFile } from '@/scripts/select-file.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { selectFile } from '@/utility/select-file.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { $i } from '@/account.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 const excludeMutingUsers = ref(false);
 const excludeInactiveUsers = ref(false);
 const noteType = ref(null);
-const withReplies = ref(defaultStore.state.defaultWithReplies);
+const withReplies = ref(store.s.defaultWithReplies);
 
 const onExportSuccess = () => {
 	os.alert({
@@ -308,7 +308,7 @@ const importAntennas = async (ev) => {
 
 //const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.importAndExport,
 	icon: 'ti ti-package',
 }));
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index f2b766006b..37f1daf620 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div v-if="!narrow || currentPage?.route.name == null" class="nav">
 					<div class="baaadecd">
 						<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>
+							<div><button class="_textButton" @click="enableAutoBackup">{{ i18n.ts.enable }}</button> | <button class="_textButton" @click="skipAutoBackup">{{ i18n.ts.skip }}</button></div>
+						</MkInfo>
 						<MkSuperMenu :def="menuDef" :grid="narrow" :searchIndex="SETTING_INDEX"></MkSuperMenu>
 					</div>
 				</div>
@@ -29,18 +33,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script setup lang="ts">
 import { computed, onActivated, onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import type { PageMetadata } from '@/page.js';
 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 { clearCache } from '@/scripts/clear-cache.js';
+import { clearCache } from '@/utility/clear-cache.js';
 import { instance } from '@/instance.js';
-import { definePageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
+import { definePage, provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
 import * as os from '@/os.js';
 import { useRouter } from '@/router/supplier.js';
-import { searchIndexes } from '@/scripts/autogen/settings-search-index.js';
+import { searchIndexes } from '@/utility/autogen/settings-search-index.js';
+import { enableAutoBackup, getPreferencesProfileMenu } from '@/preferences/utility.js';
+import { store } from '@/store.js';
 
 const SETTING_INDEX = searchIndexes; // TODO: lazy load
 
@@ -65,6 +71,10 @@ const ro = new ResizeObserver((entries, observer) => {
 	narrow.value = entries[0].borderBoxSize[0].inlineSize < NARROW_THRESHOLD;
 });
 
+function skipAutoBackup() {
+	store.set('showPreferencesAutoCloudBackupSuggestion', false);
+}
+
 const menuDef = computed<SuperMenuDef[]>(() => [{
 	items: [{
 		icon: 'ti ti-user',
@@ -173,10 +183,12 @@ const menuDef = computed<SuperMenuDef[]>(() => [{
 	}],
 }, {
 	items: [{
-		icon: 'ti ti-device-floppy',
-		text: i18n.ts.preferencesBackups,
-		to: '/settings/preferences-backups',
-		active: currentPage.value?.route.name === 'preferences-backups',
+		type: 'button',
+		icon: 'ti ti-settings-2',
+		text: i18n.ts.preferencesProfile,
+		action: async (ev: MouseEvent) => {
+			os.popupMenu(getPreferencesProfileMenu(), ev.currentTarget ?? ev.target);
+		},
 	}, {
 		type: 'button',
 		icon: 'ti ti-trash',
@@ -245,7 +257,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => INFO.value);
+definePage(() => INFO.value);
 // w 890
 // h 700
 </script>
diff --git a/packages/frontend/src/pages/settings/migration.vue b/packages/frontend/src/pages/settings/migration.vue
index 1c00d64d73..60386da545 100644
--- a/packages/frontend/src/pages/settings/migration.vue
+++ b/packages/frontend/src/pages/settings/migration.vue
@@ -66,10 +66,10 @@ import MkButton from '@/components/MkButton.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkUserInfo from '@/components/MkUserInfo.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { signinRequired } from '@/account.js';
-import { unisonReload } from '@/scripts/unison-reload.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 d1fde2fc1c..52e1937663 100644
--- a/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
+++ b/packages/frontend/src/pages/settings/mute-block.instance-mute.vue
@@ -20,7 +20,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkButton from '@/components/MkButton.vue';
 import { signinRequired } from '@/account.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 
 const $i = signinRequired();
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index 4aac2a25bd..d9c190f546 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -178,7 +178,7 @@ import XWordMute from './mute-block.word-mute.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import { userPage } from '@/filters/user.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkUserCardMini from '@/components/MkUserCardMini.vue';
 import * as os from '@/os.js';
 import { instance, infoImageUrl } from '@/instance.js';
@@ -186,8 +186,8 @@ import { signinRequired } from '@/account.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
-import { defaultStore } from '@/store';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
+import { prefer } from '@/preferences.js';
 
 const $i = signinRequired();
 
@@ -210,7 +210,7 @@ const expandedRenoteMuteItems = ref([]);
 const expandedMuteItems = ref([]);
 const expandedBlockItems = ref([]);
 
-const showSoftWordMutedWord = computed(defaultStore.makeGetterSetter('showSoftWordMutedWord'));
+const showSoftWordMutedWord = prefer.model('showSoftWordMutedWord');
 
 watch([
 	showSoftWordMutedWord,
@@ -287,7 +287,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.muteAndBlock,
 	icon: 'ti ti-ban',
 }));
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index c38cdc4fc2..706cb731eb 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -53,22 +53,24 @@ import FormSlot from '@/components/form/slot.vue';
 import MkContainer from '@/components/MkContainer.vue';
 import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
-import { defaultStore } from '@/store.js';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { store } from '@/store.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
+import { prefer } from '@/preferences.js';
+import { PREF_DEF } from '@/preferences/def.js';
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
-const items = ref(defaultStore.state.menu.map(x => ({
+const items = ref(prefer.s.menu.map(x => ({
 	id: Math.random().toString(),
 	type: x,
 })));
 
-const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
+const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
 
 async function addItem() {
-	const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k));
+	const menu = Object.keys(navbarItemDef).filter(k => !prefer.s.menu.includes(k));
 	const { canceled, result: item } = await os.select({
 		title: i18n.ts.addItem,
 		items: [...menu.map(k => ({
@@ -89,12 +91,12 @@ function removeItem(index: number) {
 }
 
 async function save() {
-	defaultStore.set('menu', items.value.map(x => x.type));
+	prefer.commit('menu', items.value.map(x => x.type));
 	await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
 }
 
 function reset() {
-	items.value = defaultStore.def.menu.default.map(x => ({
+	items.value = PREF_DEF.menu.default.map(x => ({
 		id: Math.random().toString(),
 		type: x,
 	}));
@@ -104,7 +106,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.navbar,
 	icon: 'ti ti-list',
 }));
diff --git a/packages/frontend/src/pages/settings/notifications.vue b/packages/frontend/src/pages/settings/notifications.vue
index 1e7436bf9c..ca0de0b4b1 100644
--- a/packages/frontend/src/pages/settings/notifications.vue
+++ b/packages/frontend/src/pages/settings/notifications.vue
@@ -71,9 +71,9 @@ import MkFolder from '@/components/MkFolder.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import * as os from '@/os.js';
 import { signinRequired } from '@/account.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
 import { notificationTypes } from '@@/js/const.js';
 
@@ -138,7 +138,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.notifications,
 	icon: 'ti ti-bell',
 }));
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index 9742c548e7..835739a6c6 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -116,21 +116,22 @@ import FormInfo from '@/components/MkInfo.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.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 { definePageMetadata } from '@/scripts/page-metadata.js';
-import { reloadAsk } from '@/scripts/reload-ask.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';
 
 const $i = signinRequired();
 
-const reportError = computed(defaultStore.makeGetterSetter('reportError'));
-const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine'));
-const skipNoteRender = computed(defaultStore.makeGetterSetter('skipNoteRender'));
-const devMode = computed(defaultStore.makeGetterSetter('devMode'));
-const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
+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 });
@@ -174,7 +175,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.other,
 	icon: 'ti ti-dots',
 }));
diff --git a/packages/frontend/src/pages/settings/pari.vue b/packages/frontend/src/pages/settings/pari.vue
index 875dad0c0d..ea9c0e7b02 100644
--- a/packages/frontend/src/pages/settings/pari.vue
+++ b/packages/frontend/src/pages/settings/pari.vue
@@ -88,12 +88,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { computed, ref, watch } from 'vue';
+import { computed, ref } from 'vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { defaultStore } from '@/store.js';
+import { definePage } from '@/page.js';
+import { store } from '@/store.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { getDefaultFontSettings } from '@/scripts/font-settings.js';
+import { getDefaultFontSettings } from '@/utility/font-settings.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkRadios from '@/components/MkRadios.vue';
@@ -117,21 +117,21 @@ function saveFontSize() {
 	fontSizeNumberOld.value = fontSizeNumber.value;
 }
 
-const enableRenderingOptimization = computed(defaultStore.makeGetterSetter('enableRenderingOptimization'));
+const enableRenderingOptimization = computed(store.makeGetterSetter('enableRenderingOptimization'));
 
-const enableTranslateButton = computed(defaultStore.makeGetterSetter('enableTranslateButton'));
-const showDetailTimeWhenHover = computed(defaultStore.makeGetterSetter('showDetailTimeWhenHover'));
-const noteClickToOpen = computed(defaultStore.makeGetterSetter('noteClickToOpen'));
-const enableFallbackReactButton = computed(defaultStore.makeGetterSetter('enableFallbackReactButton'));
-const enableMFMCheatsheet = computed(defaultStore.makeGetterSetter('enableMFMCheatsheet'));
-const enableUndoClearPostForm = computed(defaultStore.makeGetterSetter('enableUndoClearPostForm'));
-const autoSpacingBehaviour = computed(defaultStore.makeGetterSetter('autoSpacingBehaviour'));
-const collapseNotesRepliedTo = computed(defaultStore.makeGetterSetter('collapseNotesRepliedTo'));
-const disableReactionsViewer = computed(defaultStore.makeGetterSetter('disableReactionsViewer'));
-const emojiAutoSpacing = computed(defaultStore.makeGetterSetter('emojiAutoSpacing'));
-const clickToShowInstanceTickerWindow = computed(defaultStore.makeGetterSetter('clickToShowInstanceTickerWindow'));
+const enableTranslateButton = computed(store.makeGetterSetter('enableTranslateButton'));
+const showDetailTimeWhenHover = computed(store.makeGetterSetter('showDetailTimeWhenHover'));
+const noteClickToOpen = computed(store.makeGetterSetter('noteClickToOpen'));
+const enableFallbackReactButton = computed(store.makeGetterSetter('enableFallbackReactButton'));
+const enableMFMCheatsheet = computed(store.makeGetterSetter('enableMFMCheatsheet'));
+const enableUndoClearPostForm = computed(store.makeGetterSetter('enableUndoClearPostForm'));
+const autoSpacingBehaviour = computed(store.makeGetterSetter('autoSpacingBehaviour'));
+const collapseNotesRepliedTo = computed(store.makeGetterSetter('collapseNotesRepliedTo'));
+const disableReactionsViewer = computed(store.makeGetterSetter('disableReactionsViewer'));
+const emojiAutoSpacing = computed(store.makeGetterSetter('emojiAutoSpacing'));
+const clickToShowInstanceTickerWindow = computed(store.makeGetterSetter('clickToShowInstanceTickerWindow'));
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'Pari Plus!',
 	icon: 'ti ti-plus',
 }));
diff --git a/packages/frontend/src/pages/settings/plugin.install.vue b/packages/frontend/src/pages/settings/plugin.install.vue
index 3ab26e80d9..e984ed7f8a 100644
--- a/packages/frontend/src/pages/settings/plugin.install.vue
+++ b/packages/frontend/src/pages/settings/plugin.install.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkCodeEditor>
 
 	<div>
-		<MkButton :disabled="code == null" primary inline @click="install"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
+		<MkButton :disabled="code == null || code.trim() === ''" primary inline @click="install"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
 	</div>
 </div>
 </template>
@@ -23,11 +23,12 @@ import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import MkButton from '@/components/MkButton.vue';
 import FormInfo from '@/components/MkInfo.vue';
 import * as os from '@/os.js';
-import { installPlugin } from '@/scripts/install-plugin.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
+import { installPlugin } from '@/plugin.js';
+import { useRouter } from '@/router/supplier.js';
 
+const router = useRouter();
 const code = ref<string | null>(null);
 
 async function install() {
@@ -36,10 +37,9 @@ async function install() {
 	try {
 		await installPlugin(code.value);
 		os.success();
+		code.value = null;
 
-		nextTick(() => {
-			unisonReload();
-		});
+		router.push('/settings/plugin');
 	} catch (err) {
 		os.alert({
 			type: 'error',
@@ -53,7 +53,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts._plugin.install,
 	icon: 'ti ti-download',
 }));
diff --git a/packages/frontend/src/pages/settings/plugin.vue b/packages/frontend/src/pages/settings/plugin.vue
index 3c3dcfe41e..93a0e8a850 100644
--- a/packages/frontend/src/pages/settings/plugin.vue
+++ b/packages/frontend/src/pages/settings/plugin.vue
@@ -4,76 +4,93 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div class="_gaps_m">
-	<FormLink to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
+<SearchMarker path="/settings/plugin" :label="i18n.ts.plugins" :keywords="['plugin']" icon="ti ti-plug">
+	<div class="_gaps_m">
+		<FormLink to="/settings/plugin/install"><template #icon><i class="ti ti-download"></i></template>{{ i18n.ts._plugin.install }}</FormLink>
 
-	<FormSection>
-		<template #label>{{ i18n.ts.manage }}</template>
-		<div class="_gaps_s">
-			<div v-for="plugin in plugins" :key="plugin.id" class="_panel _gaps_m" style="padding: 20px;">
-				<div class="_gaps_s">
-					<span style="display: flex; align-items: center;"><b>{{ plugin.name }}</b><span style="margin-left: auto;">v{{ plugin.version }}</span></span>
-					<MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch>
-				</div>
-
-				<div class="_gaps_s">
-					<MkKeyValue>
-						<template #key>{{ i18n.ts.author }}</template>
-						<template #value>{{ plugin.author }}</template>
-					</MkKeyValue>
-					<MkKeyValue>
-						<template #key>{{ i18n.ts.description }}</template>
-						<template #value>{{ plugin.description }}</template>
-					</MkKeyValue>
-					<MkKeyValue>
-						<template #key>{{ i18n.ts.permission }}</template>
-						<template #value>
-							<ul style="margin-top: 0; margin-bottom: 0;">
-								<li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
-								<li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li>
-							</ul>
-						</template>
-					</MkKeyValue>
-				</div>
-
-				<div class="_buttons">
-					<MkButton v-if="plugin.config" inline @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton>
-					<MkButton inline danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
-				</div>
-
-				<MkFolder>
-					<template #icon><i class="ti ti-terminal-2"></i></template>
-					<template #label>{{ i18n.ts._plugin.viewLog }}</template>
-
-					<div class="_gaps_s">
+		<FormSection>
+			<template #label>{{ i18n.ts.manage }}</template>
+			<div class="_gaps_s">
+				<MkFolder v-for="plugin in plugins" :key="plugin.installId">
+					<template #icon><i class="ti ti-plug"></i></template>
+					<template #suffix>
+						<i v-if="plugin.active" class="ti ti-player-play" style="color: var(--MI_THEME-success);"></i>
+						<i v-else class="ti ti-player-pause" style="opacity: 0.7;"></i>
+					</template>
+					<template #label>
+						<div :style="plugin.active ? '' : 'opacity: 0.7;'">
+							{{ plugin.name }}
+							<span style="margin-left: 1em; opacity: 0.7;">v{{ plugin.version }}</span>
+						</div>
+					</template>
+					<template #caption>
+						{{ plugin.description }}
+					</template>
+					<template #footer>
 						<div class="_buttons">
-							<MkButton inline @click="copy(pluginLogs.get(plugin.id)?.join('\n'))"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+							<MkButton :disabled="!plugin.active" @click="reload(plugin)"><i class="ti ti-refresh"></i> {{ i18n.ts.reload }}</MkButton>
+							<MkButton danger @click="uninstall(plugin)"><i class="ti ti-trash"></i> {{ i18n.ts.uninstall }}</MkButton>
+							<MkButton v-if="plugin.config" style="margin-left: auto;" @click="config(plugin)"><i class="ti ti-settings"></i> {{ i18n.ts.settings }}</MkButton>
+						</div>
+					</template>
+
+					<div class="_gaps_m">
+						<div class="_gaps_s">
+							<MkSwitch :modelValue="plugin.active" @update:modelValue="changeActive(plugin, $event)">{{ i18n.ts.makeActive }}</MkSwitch>
 						</div>
 
-						<MkCode :code="pluginLogs.get(plugin.id)?.join('\n') ?? ''"/>
-					</div>
-				</MkFolder>
-
-				<MkFolder>
-					<template #icon><i class="ti ti-code"></i></template>
-					<template #label>{{ i18n.ts._plugin.viewSource }}</template>
-
-					<div class="_gaps_s">
-						<div class="_buttons">
-							<MkButton inline @click="copy(plugin.src)"><i class="ti ti-copy"></i> {{ i18n.ts.copy }}</MkButton>
+						<div class="_gaps_s">
+							<MkKeyValue>
+								<template #key>{{ i18n.ts.author }}</template>
+								<template #value>{{ plugin.author }}</template>
+							</MkKeyValue>
+							<MkKeyValue>
+								<template #key>{{ i18n.ts.description }}</template>
+								<template #value>{{ plugin.description }}</template>
+							</MkKeyValue>
+							<MkKeyValue>
+								<template #key>{{ i18n.ts.permission }}</template>
+								<template #value>
+									<ul style="margin-top: 0; margin-bottom: 0;">
+										<li v-for="permission in plugin.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
+										<li v-if="!plugin.permissions || plugin.permissions.length === 0">{{ i18n.ts.none }}</li>
+									</ul>
+								</template>
+							</MkKeyValue>
 						</div>
 
-						<MkCode :code="plugin.src ?? ''" lang="is"/>
+						<div class="_gaps_s">
+							<MkFolder>
+								<template #icon><i class="ti ti-terminal-2"></i></template>
+								<template #label>{{ i18n.ts.logs }}</template>
+
+								<div>
+									<div v-for="log in pluginLogs.get(plugin.installId)" :class="[$style.log, { [$style.isSystemLog]: log.isSystem }]">
+										<div class="_monospace">{{ timeToHhMmSs(log.at) }} {{ log.message }}</div>
+									</div>
+								</div>
+							</MkFolder>
+
+							<MkFolder :withSpacer="false">
+								<template #icon><i class="ti ti-code"></i></template>
+								<template #label>{{ i18n.ts._plugin.viewSource }}</template>
+
+								<div class="_gaps_s">
+									<MkCode :code="plugin.src ?? ''" lang="ais"/>
+								</div>
+							</MkFolder>
+						</div>
 					</div>
 				</MkFolder>
 			</div>
-		</div>
-	</FormSection>
-</div>
+		</FormSection>
+	</div>
+</SearchMarker>
 </template>
 
 <script lang="ts" setup>
 import { nextTick, ref, computed } from 'vue';
+import type { Plugin } from '@/plugin.js';
 import FormLink from '@/components/form/link.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import FormSection from '@/components/form/section.vue';
@@ -81,66 +98,57 @@ import MkButton from '@/components/MkButton.vue';
 import MkCode from '@/components/MkCode.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkKeyValue from '@/components/MkKeyValue.vue';
-import * as os from '@/os.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { ColdDeviceStorage } from '@/store.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { pluginLogs } from '@/plugin.js';
+import { definePage } from '@/page.js';
+import { changePluginActive, configPlugin, pluginLogs, uninstallPlugin, reloadPlugin } from '@/plugin.js';
+import { prefer } from '@/preferences.js';
+import * as os from '@/os.js';
 
-const plugins = ref(ColdDeviceStorage.get('plugins'));
+const plugins = prefer.r.plugins;
 
-async function uninstall(plugin) {
-	ColdDeviceStorage.set('plugins', plugins.value.filter(x => x.id !== plugin.id));
-	await os.apiWithDialog('i/revoke-token', {
-		token: plugin.token,
+async function uninstall(plugin: Plugin) {
+	const { canceled } = await os.confirm({
+		type: 'warning',
+		text: i18n.tsx.removeAreYouSure({ x: plugin.name }),
 	});
-	nextTick(() => {
-		unisonReload();
-	});
-}
+	if (canceled) return;
+
+	await uninstallPlugin(plugin);
 
-function copy(text) {
-	copyToClipboard(text ?? '');
 	os.success();
 }
 
-// TODO: この処理をstore側にactionとして移動し、設定画面を開くAiScriptAPIを実装できるようにする
-async function config(plugin) {
-	const config = plugin.config;
-	for (const key in plugin.configData) {
-		config[key].default = plugin.configData[key];
-	}
-
-	const { canceled, result } = await os.form(plugin.name, config);
-	if (canceled) return;
-
-	const coldPlugins = ColdDeviceStorage.get('plugins');
-	coldPlugins.find(p => p.id === plugin.id)!.configData = result;
-	ColdDeviceStorage.set('plugins', coldPlugins);
-
-	nextTick(() => {
-		location.reload();
-	});
+function reload(plugin: Plugin) {
+	reloadPlugin(plugin);
 }
 
-function changeActive(plugin, active) {
-	const coldPlugins = ColdDeviceStorage.get('plugins');
-	coldPlugins.find(p => p.id === plugin.id)!.active = active;
-	ColdDeviceStorage.set('plugins', coldPlugins);
+async function config(plugin: Plugin) {
+	await configPlugin(plugin);
+}
 
-	nextTick(() => {
-		location.reload();
-	});
+function changeActive(plugin: Plugin, active: boolean) {
+	changePluginActive(plugin, active);
+}
+
+function timeToHhMmSs(unixtime: number) {
+	return new Date(unixtime).toTimeString().split(' ')[0];
 }
 
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.plugins,
 	icon: 'ti ti-plug',
 }));
 </script>
+
+<style module>
+.log {
+}
+
+.isSystemLog {
+	opacity: 0.5;
+}
+</style>
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
deleted file mode 100644
index 7388e014ed..0000000000
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ /dev/null
@@ -1,465 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<template>
-<div class="_gaps_m">
-	<div :class="$style.buttons">
-		<MkButton inline primary @click="saveNew">{{ i18n.ts._preferencesBackups.saveNew }}</MkButton>
-		<MkButton inline @click="loadFile">{{ i18n.ts._preferencesBackups.loadFile }}</MkButton>
-	</div>
-
-	<FormSection>
-		<template #label>{{ i18n.ts._preferencesBackups.list }}</template>
-		<template v-if="profiles && Object.keys(profiles).length > 0">
-			<div class="_gaps_s">
-				<div
-					v-for="(profile, id) in profiles"
-					:key="id"
-					class="_panel"
-					:class="$style.profile"
-					@click="$event => menu($event, id)"
-					@contextmenu.prevent.stop="$event => menu($event, id)"
-				>
-					<div :class="$style.profileName">{{ profile.name }}</div>
-					<div :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.createdAt({ date: (new Date(profile.createdAt)).toLocaleDateString(), time: (new Date(profile.createdAt)).toLocaleTimeString() }) }}</div>
-					<div v-if="profile.updatedAt" :class="$style.profileTime">{{ i18n.tsx._preferencesBackups.updatedAt({ date: (new Date(profile.updatedAt)).toLocaleDateString(), time: (new Date(profile.updatedAt)).toLocaleTimeString() }) }}</div>
-				</div>
-			</div>
-		</template>
-		<div v-else-if="profiles">
-			<MkInfo>{{ i18n.ts._preferencesBackups.noBackups }}</MkInfo>
-		</div>
-		<MkLoading v-else/>
-	</FormSection>
-</div>
-</template>
-
-<script lang="ts" setup>
-import { onMounted, onUnmounted, ref } from 'vue';
-import { v4 as uuid } from 'uuid';
-import { version, host } from '@@/js/config.js';
-import FormSection from '@/components/form/section.vue';
-import MkButton from '@/components/MkButton.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { ColdDeviceStorage, defaultStore } from '@/store.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
-import { useStream } from '@/stream.js';
-import { $i } from '@/account.js';
-import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { miLocalStorage } from '@/local-storage.js';
-
-const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
-	'collapseRenotes',
-	'menu',
-	'visibility',
-	'localOnly',
-	'statusbars',
-	'widgets',
-	'tl',
-	'pinnedUserLists',
-	'overridedDeviceKind',
-	'serverDisconnectedBehavior',
-	'nsfw',
-	'highlightSensitiveMedia',
-	'animation',
-	'animatedMfm',
-	'advancedMfm',
-	'showReactionsCount',
-	'loadRawImages',
-	'imageNewTab',
-	'dataSaver',
-	'disableShowingAnimatedImages',
-	'emojiStyle',
-	'menuStyle',
-	'useBlurEffectForModal',
-	'useBlurEffect',
-	'showFixedPostForm',
-	'showFixedPostFormInChannel',
-	'enableInfiniteScroll',
-	'useReactionPickerForContextMenu',
-	'showGapBetweenNotesInTimeline',
-	'instanceTicker',
-	'emojiPickerScale',
-	'emojiPickerWidth',
-	'emojiPickerHeight',
-	'emojiPickerStyle',
-	'defaultSideView',
-	'menuDisplay',
-	'reportError',
-	'squareAvatars',
-	'showAvatarDecorations',
-	'numberOfPageCache',
-	'showNoteActionsOnlyHover',
-	'showClipButtonInNoteFooter',
-	'reactionsDisplaySize',
-	'forceShowAds',
-	'aiChanMode',
-	'devMode',
-	'mediaListWithOneImageAppearance',
-	'notificationPosition',
-	'notificationStackAxis',
-	'keepScreenOn',
-	'defaultWithReplies',
-	'disableStreamingTimeline',
-	'useGroupedNotifications',
-	'sound_masterVolume',
-	'sound_note',
-	'sound_noteMy',
-	'sound_notification',
-];
-const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
-	'lightTheme',
-	'darkTheme',
-	'syncDeviceDarkMode',
-	'plugins',
-];
-
-const scope = ['clientPreferencesProfiles'];
-
-const profileProps = ['name', 'createdAt', 'updatedAt', 'misskeyVersion', 'settings', 'host'];
-
-type Profile = {
-	name: string;
-	createdAt: string;
-	updatedAt: string | null;
-	misskeyVersion: string;
-	host: string;
-	settings: {
-		hot: Record<keyof typeof defaultStoreSaveKeys, unknown>;
-		cold: Record<keyof typeof coldDeviceStorageSaveKeys, unknown>;
-		fontSize: string | null;
-		useSystemFont: 't' | null;
-		wallpaper: string | null;
-	};
-};
-
-const connection = $i && useStream().useChannel('main');
-
-const profiles = ref<Record<string, Profile> | null>(null);
-
-misskeyApi('i/registry/get-all', { scope })
-	.then(res => {
-		profiles.value = res || {};
-	});
-
-function isObject(value: unknown): value is Record<string, unknown> {
-	return value != null && typeof value === 'object' && !Array.isArray(value);
-}
-
-function validate(profile: any): void {
-	if (!isObject(profile)) throw new Error('not an object');
-
-	// Check if unnecessary properties exist
-	if (Object.keys(profile).some(key => !profileProps.includes(key))) throw new Error('Unnecessary properties exist');
-
-	if (!profile.name) throw new Error('Missing required prop: name');
-	if (!profile.misskeyVersion) throw new Error('Missing required prop: misskeyVersion');
-
-	// Check if createdAt and updatedAt is Date
-	// https://zenn.dev/lollipop_onl/articles/eoz-judge-js-invalid-date
-	if (!profile.createdAt || Number.isNaN(new Date(profile.createdAt as any).getTime())) throw new Error('createdAt is falsy or not Date');
-	if (profile.updatedAt) {
-		if (Number.isNaN(new Date(profile.updatedAt as any).getTime())) {
-			throw new Error('updatedAt is not Date');
-		}
-	} else if (profile.updatedAt !== null) {
-		throw new Error('updatedAt is not null');
-	}
-
-	if (!profile.settings) throw new Error('Missing required prop: settings');
-	if (!isObject(profile.settings)) throw new Error('Invalid prop: settings');
-}
-
-function getSettings(): Profile['settings'] {
-	const hot = {} as Record<keyof typeof defaultStoreSaveKeys, unknown>;
-	for (const key of defaultStoreSaveKeys) {
-		hot[key] = defaultStore.state[key];
-	}
-
-	const cold = {} as Record<keyof typeof coldDeviceStorageSaveKeys, unknown>;
-	for (const key of coldDeviceStorageSaveKeys) {
-		cold[key] = ColdDeviceStorage.get(key);
-	}
-
-	return {
-		hot,
-		cold,
-		fontSize: miLocalStorage.getItem('fontSize'),
-		useSystemFont: miLocalStorage.getItem('useSystemFont') as 't' | null,
-		wallpaper: miLocalStorage.getItem('wallpaper'),
-	};
-}
-
-async function saveNew(): Promise<void> {
-	if (!profiles.value) return;
-
-	const { canceled, result: name } = await os.inputText({
-		title: i18n.ts._preferencesBackups.inputName,
-		default: '',
-	});
-	if (canceled) return;
-
-	if (Object.values(profiles.value).some(x => x.name === name)) {
-		return os.alert({
-			title: i18n.ts._preferencesBackups.cannotSave,
-			text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }),
-		});
-	}
-
-	const id = uuid();
-	const profile: Profile = {
-		name,
-		createdAt: (new Date()).toISOString(),
-		updatedAt: null,
-		misskeyVersion: version,
-		host,
-		settings: getSettings(),
-	};
-	await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile });
-}
-
-function loadFile(): void {
-	const input = document.createElement('input');
-	input.type = 'file';
-	input.multiple = false;
-	input.onchange = async () => {
-		if (!profiles.value) return;
-		if (!input.files || input.files.length === 0) return;
-
-		const file = input.files[0];
-
-		if (file.type !== 'application/json') {
-			return os.alert({
-				type: 'error',
-				title: i18n.ts._preferencesBackups.cannotLoad,
-				text: i18n.ts._preferencesBackups.invalidFile,
-			});
-		}
-
-		let profile: Profile;
-		try {
-			profile = JSON.parse(await file.text()) as unknown as Profile;
-			validate(profile);
-		} catch (err) {
-			return os.alert({
-				type: 'error',
-				title: i18n.ts._preferencesBackups.cannotLoad,
-				text: (err as any)?.message ?? '',
-			});
-		}
-
-		const id = uuid();
-		await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile });
-
-		// 一応廃棄
-		(window as any).__misskey_input_ref__ = null;
-	};
-
-	// https://qiita.com/fukasawah/items/b9dc732d95d99551013d
-	// iOS Safari で正常に動かす為のおまじない
-	(window as any).__misskey_input_ref__ = input;
-
-	input.click();
-}
-
-async function applyProfile(id: string): Promise<void> {
-	if (!profiles.value) return;
-
-	const profile = profiles.value[id];
-
-	const { canceled: cancel1 } = await os.confirm({
-		type: 'warning',
-		title: i18n.ts._preferencesBackups.apply,
-		text: i18n.tsx._preferencesBackups.applyConfirm({ name: profile.name }),
-	});
-	if (cancel1) return;
-
-	// TODO: バージョン or ホストが違ったらさらに警告を表示
-
-	const settings = profile.settings;
-
-	// defaultStore
-	for (const key of defaultStoreSaveKeys) {
-		if (settings.hot[key] !== undefined) {
-			defaultStore.set(key, settings.hot[key]);
-		}
-	}
-
-	// coldDeviceStorage
-	for (const key of coldDeviceStorageSaveKeys) {
-		if (settings.cold[key] !== undefined) {
-			ColdDeviceStorage.set(key, settings.cold[key]);
-		}
-	}
-
-	// fontSize
-	if (settings.fontSize) {
-		miLocalStorage.setItem('fontSize', settings.fontSize);
-	} else {
-		miLocalStorage.removeItem('fontSize');
-	}
-
-	// useSystemFont
-	if (settings.useSystemFont) {
-		miLocalStorage.setItem('useSystemFont', settings.useSystemFont);
-	} else {
-		miLocalStorage.removeItem('useSystemFont');
-	}
-
-	// wallpaper
-	if (settings.wallpaper != null) {
-		miLocalStorage.setItem('wallpaper', settings.wallpaper);
-	} else {
-		miLocalStorage.removeItem('wallpaper');
-	}
-
-	const { canceled: cancel2 } = await os.confirm({
-		type: 'info',
-		text: i18n.ts.reloadToApplySetting,
-	});
-	if (cancel2) return;
-
-	unisonReload();
-}
-
-async function deleteProfile(id: string): Promise<void> {
-	if (!profiles.value) return;
-
-	const { canceled } = await os.confirm({
-		type: 'info',
-		title: i18n.ts.delete,
-		text: i18n.tsx.deleteAreYouSure({ x: profiles.value[id].name }),
-	});
-	if (canceled) return;
-
-	await os.apiWithDialog('i/registry/remove', { scope, key: id });
-	delete profiles.value[id];
-}
-
-async function save(id: string): Promise<void> {
-	if (!profiles.value) return;
-
-	const { name, createdAt } = profiles.value[id];
-
-	const { canceled } = await os.confirm({
-		type: 'info',
-		title: i18n.ts._preferencesBackups.save,
-		text: i18n.tsx._preferencesBackups.saveConfirm({ name }),
-	});
-	if (canceled) return;
-
-	const profile: Profile = {
-		name,
-		createdAt,
-		updatedAt: (new Date()).toISOString(),
-		misskeyVersion: version,
-		host,
-		settings: getSettings(),
-	};
-	await os.apiWithDialog('i/registry/set', { scope, key: id, value: profile });
-}
-
-async function rename(id: string): Promise<void> {
-	if (!profiles.value) return;
-
-	const { canceled: cancel1, result: name } = await os.inputText({
-		title: i18n.ts._preferencesBackups.inputName,
-		default: '',
-	});
-	if (cancel1 || profiles.value[id].name === name) return;
-
-	if (Object.values(profiles.value).some(x => x.name === name)) {
-		return os.alert({
-			title: i18n.ts._preferencesBackups.cannotSave,
-			text: i18n.tsx._preferencesBackups.nameAlreadyExists({ name }),
-		});
-	}
-
-	const registry = Object.assign({}, { ...profiles.value[id] });
-
-	const { canceled: cancel2 } = await os.confirm({
-		type: 'info',
-		title: i18n.ts.rename,
-		text: i18n.tsx._preferencesBackups.renameConfirm({ old: registry.name, new: name }),
-	});
-	if (cancel2) return;
-
-	registry.name = name;
-	await os.apiWithDialog('i/registry/set', { scope, key: id, value: registry });
-}
-
-function menu(ev: MouseEvent, profileId: string) {
-	if (!profiles.value) return;
-
-	return os.popupMenu([{
-		text: i18n.ts._preferencesBackups.apply,
-		icon: 'ti ti-check',
-		action: () => applyProfile(profileId),
-	}, {
-		type: 'a',
-		text: i18n.ts.download,
-		icon: 'ti ti-download',
-		href: URL.createObjectURL(new Blob([JSON.stringify(profiles.value[profileId], null, 2)], { type: 'application/json' })),
-		download: `${profiles.value[profileId].name}.json`,
-	}, { type: 'divider' }, {
-		text: i18n.ts.rename,
-		icon: 'ti ti-forms',
-		action: () => rename(profileId),
-	}, {
-		text: i18n.ts._preferencesBackups.save,
-		icon: 'ti ti-device-floppy',
-		action: () => save(profileId),
-	}, { type: 'divider' }, {
-		text: i18n.ts.delete,
-		icon: 'ti ti-trash',
-		action: () => deleteProfile(profileId),
-		danger: true,
-	}], (ev.currentTarget ?? ev.target ?? undefined) as unknown as HTMLElement | undefined);
-}
-
-onMounted(() => {
-	// streamingのuser storage updateイベントを監視して更新
-	connection?.on('registryUpdated', ({ scope: recievedScope, key, value }) => {
-		if (!recievedScope || recievedScope.length !== scope.length || recievedScope[0] !== scope[0]) return;
-		if (!profiles.value) return;
-
-		profiles.value[key] = value;
-	});
-});
-
-onUnmounted(() => {
-	connection?.off('registryUpdated');
-});
-
-definePageMetadata(() => ({
-	title: i18n.ts.preferencesBackups,
-	icon: 'ti ti-device-floppy',
-}));
-</script>
-
-<style lang="scss" module>
-.buttons {
-	display: flex;
-	gap: var(--MI-margin);
-	flex-wrap: wrap;
-}
-
-.profile {
-	padding: 20px;
-	cursor: pointer;
-
-	&Name {
-		font-weight: 700;
-	}
-
-	&Time {
-		font-size: .85em;
-		opacity: .7;
-	}
-}
-</style>
diff --git a/packages/frontend/src/pages/settings/preferences.vue b/packages/frontend/src/pages/settings/preferences.vue
index 4f3d8daf17..5c198d5862 100644
--- a/packages/frontend/src/pages/settings/preferences.vue
+++ b/packages/frontend/src/pages/settings/preferences.vue
@@ -33,30 +33,75 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<FormSection>
 			<div class="_gaps_s">
 				<SearchMarker :keywords="['post', 'form', 'timeline']">
-					<MkSwitch v-model="showFixedPostForm">
-						<template #label><SearchLabel>{{ i18n.ts.showFixedPostForm }}</SearchLabel></template>
-					</MkSwitch>
+					<MkPreferenceContainer k="showFixedPostForm">
+						<MkSwitch v-model="showFixedPostForm">
+							<template #label><SearchLabel>{{ i18n.ts.showFixedPostForm }}</SearchLabel></template>
+						</MkSwitch>
+					</MkPreferenceContainer>
 				</SearchMarker>
 
 				<SearchMarker :keywords="['post', 'form', 'timeline', 'channel']">
-					<MkSwitch v-model="showFixedPostFormInChannel">
-						<template #label><SearchLabel>{{ i18n.ts.showFixedPostFormInChannel }}</SearchLabel></template>
-					</MkSwitch>
+					<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="defaultStore.reactiveState.pinnedUserLists.value.length === 0" @click="setPinnedList()">{{ i18n.ts.add }}</MkButton>
+						<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']">
-					<MkSwitch v-model="enableQuickAddMfmFunction">
-						<template #label><SearchLabel>{{ i18n.ts.enableQuickAddMfmFunction }}</SearchLabel></template>
-					</MkSwitch>
+					<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>
@@ -68,40 +113,52 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div class="_gaps_m">
 					<div class="_gaps_s">
 						<SearchMarker :keywords="['renote']">
-							<MkSwitch v-model="collapseRenotes">
-								<template #label><SearchLabel>{{ i18n.ts.collapseRenotes }}</SearchLabel></template>
-								<template #caption><SearchKeyword>{{ i18n.ts.collapseRenotesDescription }}</SearchKeyword></template>
-							</MkSwitch>
+							<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="['hover', 'show', 'footer', 'action']">
-							<MkSwitch v-model="showNoteActionsOnlyHover">
-								<template #label><SearchLabel>{{ i18n.ts.showNoteActionsOnlyHover }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="showNoteActionsOnlyHover">
+								<MkSwitch v-model="showNoteActionsOnlyHover">
+									<template #label><SearchLabel>{{ i18n.ts.showNoteActionsOnlyHover }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['footer', 'action', 'clip', 'show']">
-							<MkSwitch v-model="showClipButtonInNoteFooter">
-								<template #label><SearchLabel>{{ i18n.ts.showClipButtonInNoteFooter }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="showClipButtonInNoteFooter">
+								<MkSwitch v-model="showClipButtonInNoteFooter">
+									<template #label><SearchLabel>{{ i18n.ts.showClipButtonInNoteFooter }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['mfm', 'enable', 'show', 'advanced']">
-							<MkSwitch v-model="advancedMfm">
-								<template #label><SearchLabel>{{ i18n.ts.enableAdvancedMfm }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="advancedMfm">
+								<MkSwitch v-model="advancedMfm">
+									<template #label><SearchLabel>{{ i18n.ts.enableAdvancedMfm }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['reaction', 'count', 'show']">
-							<MkSwitch v-model="showReactionsCount">
-								<template #label><SearchLabel>{{ i18n.ts.showReactionsCount }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="showReactionsCount">
+								<MkSwitch v-model="showReactionsCount">
+									<template #label><SearchLabel>{{ i18n.ts.showReactionsCount }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment']">
-							<MkSwitch v-model="loadRawImages">
-								<template #label><SearchLabel>{{ i18n.ts.loadRawImages }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="loadRawImages">
+								<MkSwitch v-model="loadRawImages">
+									<template #label><SearchLabel>{{ i18n.ts.loadRawImages }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 					</div>
 				</div>
@@ -114,9 +171,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<div class="_gaps_m">
 					<SearchMarker :keywords="['group']">
-						<MkSwitch v-model="useGroupedNotifications">
-							<template #label><SearchLabel>{{ i18n.ts.useGroupedNotifications }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="useGroupedNotifications">
+							<MkSwitch v-model="useGroupedNotifications">
+								<template #label><SearchLabel>{{ i18n.ts.useGroupedNotifications }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 				</div>
 			</FormSection>
@@ -129,63 +188,89 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<div class="_gaps_m">
 					<div class="_gaps_s">
 						<SearchMarker :keywords="['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab']">
-							<MkSwitch v-model="imageNewTab">
-								<template #label><SearchLabel>{{ i18n.ts.openImageInNewTab }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="imageNewTab">
+								<MkSwitch v-model="imageNewTab">
+									<template #label><SearchLabel>{{ i18n.ts.openImageInNewTab }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['reaction', 'picker', 'contextmenu', 'open']">
-							<MkSwitch v-model="useReactionPickerForContextMenu">
-								<template #label><SearchLabel>{{ i18n.ts.useReactionPickerForContextMenu }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="useReactionPickerForContextMenu">
+								<MkSwitch v-model="useReactionPickerForContextMenu">
+									<template #label><SearchLabel>{{ i18n.ts.useReactionPickerForContextMenu }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['load', 'auto', 'more']">
-							<MkSwitch v-model="enableInfiniteScroll">
-								<template #label><SearchLabel>{{ i18n.ts.enableInfiniteScroll }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="enableInfiniteScroll">
+								<MkSwitch v-model="enableInfiniteScroll">
+									<template #label><SearchLabel>{{ i18n.ts.enableInfiniteScroll }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['disable', 'streaming', 'timeline']">
-							<MkSwitch v-model="disableStreamingTimeline">
-								<template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="disableStreamingTimeline">
+								<MkSwitch v-model="disableStreamingTimeline">
+									<template #label><SearchLabel>{{ i18n.ts.disableStreamingTimeline }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['follow', 'confirm', 'always']">
-							<MkSwitch v-model="alwaysConfirmFollow">
-								<template #label><SearchLabel>{{ i18n.ts.alwaysConfirmFollow }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="alwaysConfirmFollow">
+								<MkSwitch v-model="alwaysConfirmFollow">
+									<template #label><SearchLabel>{{ i18n.ts.alwaysConfirmFollow }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm']">
-							<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">
-								<template #label><SearchLabel>{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="confirmWhenRevealingSensitiveMedia">
+								<MkSwitch v-model="confirmWhenRevealingSensitiveMedia">
+									<template #label><SearchLabel>{{ i18n.ts.confirmWhenRevealingSensitiveMedia }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 
 						<SearchMarker :keywords="['reaction', 'confirm']">
-							<MkSwitch v-model="confirmOnReact">
-								<template #label><SearchLabel>{{ i18n.ts.confirmOnReact }}</SearchLabel></template>
-							</MkSwitch>
+							<MkPreferenceContainer k="confirmOnReact">
+								<MkSwitch v-model="confirmOnReact">
+									<template #label><SearchLabel>{{ i18n.ts.confirmOnReact }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
+						</SearchMarker>
+
+						<SearchMarker :keywords="['remember', 'keep', 'note', 'cw']">
+							<MkPreferenceContainer k="keepCw">
+								<MkSwitch v-model="keepCw">
+									<template #label><SearchLabel>{{ i18n.ts.keepCw }}</SearchLabel></template>
+								</MkSwitch>
+							</MkPreferenceContainer>
 						</SearchMarker>
 					</div>
 
 					<SearchMarker :keywords="['server', 'disconnect', 'reconnect', 'reload', 'streaming']">
-						<MkSelect v-model="serverDisconnectedBehavior">
-							<template #label><SearchLabel>{{ i18n.ts.whenServerDisconnected }}</SearchLabel></template>
-							<option value="null">{{ i18n.ts.doNothing }}</option>
-							<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 k="serverDisconnectedBehavior">
+							<MkSelect v-model="serverDisconnectedBehavior">
+								<template #label><SearchLabel>{{ i18n.ts.whenServerDisconnected }}</SearchLabel></template>
+								<option value="null">{{ i18n.ts.doNothing }}</option>
+								<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']">
-						<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 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 :label="i18n.ts.dataSaver" :keywords="['datasaver']">
@@ -230,18 +315,22 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 				<div class="_gaps">
 					<SearchMarker :keywords="['ad', 'show']">
-						<MkSwitch v-model="forceShowAds">
-							<template #label><SearchLabel>{{ i18n.ts.forceShowAds }}</SearchLabel></template>
-						</MkSwitch>
+						<MkPreferenceContainer k="forceShowAds">
+							<MkSwitch v-model="forceShowAds">
+								<template #label><SearchLabel>{{ i18n.ts.forceShowAds }}</SearchLabel></template>
+							</MkSwitch>
+						</MkPreferenceContainer>
 					</SearchMarker>
 
 					<SearchMarker>
-						<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 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']">
@@ -249,8 +338,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 							<template #label><SearchLabel>{{ i18n.ts.additionalEmojiDictionary }}</SearchLabel></template>
 							<div class="_buttons">
 								<template v-for="lang in emojiIndexLangs" :key="lang">
-									<MkButton v-if="defaultStore.reactiveState.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) }}{{ defaultStore.reactiveState.additionalUnicodeEmojiIndexes.value[lang] ? ` (${ i18n.ts.installed })` : '' }}</MkButton>
+									<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>
@@ -273,7 +362,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, ref, watch } from 'vue';
-import * as Misskey from 'misskey-js';
 import { langs } from '@@/js/config.js';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
@@ -285,41 +373,46 @@ import FormSection from '@/components/form/section.vue';
 import FormLink from '@/components/form/link.vue';
 import MkLink from '@/components/MkLink.vue';
 import MkInfo from '@/components/MkInfo.vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import * as os from '@/os.js';
-import { instance } from '@/instance.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { reloadAsk } from '@/scripts/reload-ask.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { miLocalStorage } from '@/local-storage.js';
+import { prefer } from '@/preferences.js';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
 
 const lang = ref(miLocalStorage.getItem('lang'));
-const dataSaver = ref(defaultStore.state.dataSaver);
+const dataSaver = ref(prefer.s.dataSaver);
 
-const hemisphere = computed(defaultStore.makeGetterSetter('hemisphere'));
-const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
-const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
-const showNoteActionsOnlyHover = computed(defaultStore.makeGetterSetter('showNoteActionsOnlyHover'));
-const showClipButtonInNoteFooter = computed(defaultStore.makeGetterSetter('showClipButtonInNoteFooter'));
-const collapseRenotes = computed(defaultStore.makeGetterSetter('collapseRenotes'));
-const advancedMfm = computed(defaultStore.makeGetterSetter('advancedMfm'));
-const showReactionsCount = computed(defaultStore.makeGetterSetter('showReactionsCount'));
-const enableQuickAddMfmFunction = computed(defaultStore.makeGetterSetter('enableQuickAddMfmFunction'));
-const forceShowAds = computed(defaultStore.makeGetterSetter('forceShowAds'));
-const loadRawImages = computed(defaultStore.makeGetterSetter('loadRawImages'));
-const imageNewTab = computed(defaultStore.makeGetterSetter('imageNewTab'));
-const showFixedPostForm = computed(defaultStore.makeGetterSetter('showFixedPostForm'));
-const showFixedPostFormInChannel = computed(defaultStore.makeGetterSetter('showFixedPostFormInChannel'));
-const numberOfPageCache = computed(defaultStore.makeGetterSetter('numberOfPageCache'));
-const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfiniteScroll'));
-const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu'));
-const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
-const useGroupedNotifications = computed(defaultStore.makeGetterSetter('useGroupedNotifications'));
-const alwaysConfirmFollow = computed(defaultStore.makeGetterSetter('alwaysConfirmFollow'));
-const confirmWhenRevealingSensitiveMedia = computed(defaultStore.makeGetterSetter('confirmWhenRevealingSensitiveMedia'));
-const confirmOnReact = computed(defaultStore.makeGetterSetter('confirmOnReact'));
-const contextMenu = computed(defaultStore.makeGetterSetter('contextMenu'));
+const overridedDeviceKind = prefer.model('overridedDeviceKind');
+const keepCw = prefer.model('keepCw');
+const serverDisconnectedBehavior = prefer.model('serverDisconnectedBehavior');
+const hemisphere = prefer.model('hemisphere');
+const showNoteActionsOnlyHover = prefer.model('showNoteActionsOnlyHover');
+const showClipButtonInNoteFooter = prefer.model('showClipButtonInNoteFooter');
+const collapseRenotes = prefer.model('collapseRenotes');
+const advancedMfm = prefer.model('advancedMfm');
+const showReactionsCount = prefer.model('showReactionsCount');
+const enableQuickAddMfmFunction = prefer.model('enableQuickAddMfmFunction');
+const forceShowAds = prefer.model('forceShowAds');
+const loadRawImages = prefer.model('loadRawImages');
+const imageNewTab = prefer.model('imageNewTab');
+const showFixedPostForm = prefer.model('showFixedPostForm');
+const showFixedPostFormInChannel = prefer.model('showFixedPostFormInChannel');
+const numberOfPageCache = prefer.model('numberOfPageCache');
+const enableInfiniteScroll = prefer.model('enableInfiniteScroll');
+const useReactionPickerForContextMenu = prefer.model('useReactionPickerForContextMenu');
+const disableStreamingTimeline = prefer.model('disableStreamingTimeline');
+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');
 
 watch(lang, () => {
 	miLocalStorage.setItem('lang', lang.value as string);
@@ -357,7 +450,7 @@ function getEmojiIndexLangName(targetLang: typeof emojiIndexLangs[number]) {
 
 function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
 	async function main() {
-		const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
+		const currentIndexes = store.s.additionalUnicodeEmojiIndexes;
 
 		function download() {
 			switch (lang) {
@@ -369,7 +462,7 @@ function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
 		}
 
 		currentIndexes[lang] = await download();
-		await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
+		await store.set('additionalUnicodeEmojiIndexes', currentIndexes);
 	}
 
 	os.promiseDialog(main());
@@ -377,9 +470,9 @@ function downloadEmojiIndex(lang: typeof emojiIndexLangs[number]) {
 
 function removeEmojiIndex(lang: string) {
 	async function main() {
-		const currentIndexes = defaultStore.state.additionalUnicodeEmojiIndexes;
+		const currentIndexes = store.s.additionalUnicodeEmojiIndexes;
 		delete currentIndexes[lang];
-		await defaultStore.set('additionalUnicodeEmojiIndexes', currentIndexes);
+		await store.set('additionalUnicodeEmojiIndexes', currentIndexes);
 	}
 
 	os.promiseDialog(main());
@@ -394,16 +487,17 @@ async function setPinnedList() {
 		})),
 	});
 	if (canceled) return;
+	if (list == null) return;
 
-	defaultStore.set('pinnedUserLists', [list]);
+	prefer.commit('pinnedUserLists', [list]);
 }
 
 function removePinnedList() {
-	defaultStore.set('pinnedUserLists', []);
+	prefer.commit('pinnedUserLists', []);
 }
 
 function enableAllDataSaver() {
-	const g = { ...defaultStore.state.dataSaver };
+	const g = { ...prefer.s.dataSaver };
 
 	Object.keys(g).forEach((key) => { g[key] = true; });
 
@@ -411,7 +505,7 @@ function enableAllDataSaver() {
 }
 
 function disableAllDataSaver() {
-	const g = { ...defaultStore.state.dataSaver };
+	const g = { ...prefer.s.dataSaver };
 
 	Object.keys(g).forEach((key) => { g[key] = false; });
 
@@ -419,7 +513,7 @@ function disableAllDataSaver() {
 }
 
 watch(dataSaver, (to) => {
-	defaultStore.set('dataSaver', to);
+	prefer.commit('dataSaver', to);
 }, {
 	deep: true,
 });
@@ -428,7 +522,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.general,
 	icon: 'ti ti-adjustments',
 }));
diff --git a/packages/frontend/src/pages/settings/privacy.vue b/packages/frontend/src/pages/settings/privacy.vue
index cd0d54a73b..d42dd323e0 100644
--- a/packages/frontend/src/pages/settings/privacy.vue
+++ b/packages/frontend/src/pages/settings/privacy.vue
@@ -123,7 +123,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 							<template #caption>
 								<div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription }}</SearchKeyword></div>
-								<div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
 							</template>
 						</FormSlot>
 					</SearchMarker>
@@ -161,49 +160,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 							<template #caption>
 								<div><SearchKeyword>{{ i18n.ts._accountSettings.makeNotesHiddenBeforeDescription }}</SearchKeyword></div>
-								<div v-if="instance.federation !== 'none'"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.ts._accountSettings.mayNotEffectForFederatedNotes }}</div>
 							</template>
 						</FormSlot>
 					</SearchMarker>
+
+					<MkInfo warn>{{ i18n.ts._accountSettings.mayNotEffectSomeSituations }}</MkInfo>
 				</div>
 			</FormSection>
 		</SearchMarker>
-
-		<FormSection>
-			<div class="_gaps_m">
-				<SearchMarker :keywords="['remember', 'keep', 'note', 'visibility']">
-					<MkSwitch v-model="rememberNoteVisibility" @update:modelValue="save()">
-						<template #label><SearchLabel>{{ i18n.ts.rememberNoteVisibility }}</SearchLabel></template>
-					</MkSwitch>
-				</SearchMarker>
-
-				<SearchMarker :keywords="['default', 'note', 'visibility']">
-					<MkFolder v-if="!rememberNoteVisibility">
-						<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">
-							<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>
-							<MkSwitch v-model="defaultNoteLocalOnly">{{ i18n.ts._visibility.disableFederation }}</MkSwitch>
-						</div>
-					</MkFolder>
-				</SearchMarker>
-			</div>
-		</FormSection>
-
-		<SearchMarker :keywords="['remember', 'keep', 'note', 'cw']">
-			<MkSwitch v-model="keepCw" @update:modelValue="save()">
-				<template #label><SearchLabel>{{ i18n.ts.keepCw }}</SearchLabel></template>
-			</MkSwitch>
-		</SearchMarker>
 	</div>
 </SearchMarker>
 </template>
@@ -214,17 +178,17 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import { signinRequired } from '@/account.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import FormSlot from '@/components/form/slot.vue';
-import { formatDateTimeString } from '@/scripts/format-time-string.js';
+import { formatDateTimeString } from '@/utility/format-time-string.js';
 import MkInput from '@/components/MkInput.vue';
 import * as os from '@/os.js';
 import MkDisableSection from '@/components/MkDisableSection.vue';
+import MkInfo from '@/components/MkInfo.vue';
 
 const $i = signinRequired();
 
@@ -241,11 +205,6 @@ const publicReactions = ref($i.publicReactions);
 const followingVisibility = ref($i.followingVisibility);
 const followersVisibility = ref($i.followersVisibility);
 
-const defaultNoteVisibility = computed(defaultStore.makeGetterSetter('defaultNoteVisibility'));
-const defaultNoteLocalOnly = computed(defaultStore.makeGetterSetter('defaultNoteLocalOnly'));
-const rememberNoteVisibility = computed(defaultStore.makeGetterSetter('rememberNoteVisibility'));
-const keepCw = computed(defaultStore.makeGetterSetter('keepCw'));
-
 const makeNotesFollowersOnlyBefore_type = computed(() => {
 	if (makeNotesFollowersOnlyBefore.value == null) {
 		return null;
@@ -304,7 +263,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.privacy,
 	icon: 'ti ti-lock-open',
 }));
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index 51148a1f72..1d85ba7834 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -161,14 +161,14 @@ import MkSelect from '@/components/MkSelect.vue';
 import FormSplit from '@/components/form/split.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import FormSlot from '@/components/form/slot.vue';
-import { selectFile } from '@/scripts/select-file.js';
+import { selectFile } from '@/utility/select-file.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { signinRequired } from '@/account.js';
-import { langmap } from '@/scripts/langmap.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { claimAchievement } from '@/scripts/achievements.js';
-import { defaultStore } from '@/store.js';
+import { langmap } from '@/utility/langmap.js';
+import { definePage } from '@/page.js';
+import { claimAchievement } from '@/utility/achievements.js';
+import { store } from '@/store.js';
 import { globalEvents } from '@/events.js';
 import MkInfo from '@/components/MkInfo.vue';
 import MkTextarea from '@/components/MkTextarea.vue';
@@ -177,7 +177,7 @@ const $i = signinRequired();
 
 const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
 
-const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance'));
+const reactionAcceptance = computed(store.makeGetterSetter('reactionAcceptance'));
 
 function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
 	return lang != null && lang in langmap;
@@ -316,7 +316,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.profile,
 	icon: 'ti ti-user',
 }));
diff --git a/packages/frontend/src/pages/settings/roles.vue b/packages/frontend/src/pages/settings/roles.vue
index 5346a58a79..c1cabad2c3 100644
--- a/packages/frontend/src/pages/settings/roles.vue
+++ b/packages/frontend/src/pages/settings/roles.vue
@@ -28,7 +28,7 @@ import FormSection from '@/components/form/section.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { signinRequired } from '@/account.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import MkRolePreview from '@/components/MkRolePreview.vue';
 
 const $i = signinRequired();
@@ -37,7 +37,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.roles,
 	icon: 'ti ti-badges',
 }));
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index f365146e0a..9b664fa98a 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -56,9 +56,9 @@ import FormSlot from '@/components/form/slot.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkPagination from '@/components/MkPagination.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const pagination = {
 	endpoint: 'i/signin-history' as const,
@@ -112,7 +112,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.security,
 	icon: 'ti ti-lock',
 }));
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 56f65e2309..1bac19fe47 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -32,15 +32,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { ref, computed, watch } from 'vue';
-import type { SoundType } from '@/scripts/sound.js';
+import type { SoundType } from '@/utility/sound.js';
 import MkSelect from '@/components/MkSelect.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkRange from '@/components/MkRange.vue';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/scripts/sound.js';
-import { selectFile } from '@/scripts/select-file.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { playMisskeySfxFile, soundsTypes, getSoundDuration } from '@/utility/sound.js';
+import { selectFile } from '@/utility/select-file.js';
 
 const props = defineProps<{
 	type: SoundType;
diff --git a/packages/frontend/src/pages/settings/sounds.vue b/packages/frontend/src/pages/settings/sounds.vue
index 1df2d89277..0c447b1a67 100644
--- a/packages/frontend/src/pages/settings/sounds.vue
+++ b/packages/frontend/src/pages/settings/sounds.vue
@@ -7,21 +7,27 @@ SPDX-License-Identifier: AGPL-3.0-only
 <SearchMarker path="/settings/sounds" :label="i18n.ts.sounds" :keywords="['sounds']" icon="ti ti-music">
 	<div class="_gaps_m">
 		<SearchMarker :keywords="['mute']">
-			<MkSwitch v-model="notUseSound">
-				<template #label><SearchLabel>{{ i18n.ts.notUseSound }}</SearchLabel></template>
-			</MkSwitch>
+			<MkPreferenceContainer k="sound.notUseSound">
+				<MkSwitch v-model="notUseSound">
+					<template #label><SearchLabel>{{ i18n.ts.notUseSound }}</SearchLabel></template>
+				</MkSwitch>
+			</MkPreferenceContainer>
 		</SearchMarker>
 
 		<SearchMarker :keywords="['active', 'mute']">
-			<MkSwitch v-model="useSoundOnlyWhenActive">
-				<template #label><SearchLabel>{{ i18n.ts.useSoundOnlyWhenActive }}</SearchLabel></template>
-			</MkSwitch>
+			<MkPreferenceContainer k="sound.useSoundOnlyWhenActive">
+				<MkSwitch v-model="useSoundOnlyWhenActive">
+					<template #label><SearchLabel>{{ i18n.ts.useSoundOnlyWhenActive }}</SearchLabel></template>
+				</MkSwitch>
+			</MkPreferenceContainer>
 		</SearchMarker>
 
 		<SearchMarker :keywords="['volume', 'master']">
-			<MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
-				<template #label><SearchLabel>{{ i18n.ts.masterVolume }}</SearchLabel></template>
-			</MkRange>
+			<MkPreferenceContainer k="sound.masterVolume">
+				<MkRange v-model="masterVolume" :min="0" :max="1" :step="0.05" :textConverter="(v) => `${Math.floor(v * 100)}%`">
+					<template #label><SearchLabel>{{ i18n.ts.masterVolume }}</SearchLabel></template>
+				</MkRange>
+			</MkPreferenceContainer>
 		</SearchMarker>
 
 		<FormSection>
@@ -51,27 +57,29 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref } from 'vue';
 import XSound from './sounds.sound.vue';
 import type { Ref } from 'vue';
-import type { SoundType, OperationType } from '@/scripts/sound.js';
-import type { SoundStore } from '@/store.js';
+import type { SoundType, OperationType } from '@/utility/sound.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { prefer } from '@/preferences.js';
 import MkRange from '@/components/MkRange.vue';
 import MkButton from '@/components/MkButton.vue';
 import FormSection from '@/components/form/section.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { operationTypes } from '@/scripts/sound.js';
-import { defaultStore } from '@/store.js';
+import { definePage } from '@/page.js';
+import { operationTypes } from '@/utility/sound.js';
 import MkSwitch from '@/components/MkSwitch.vue';
+import MkPreferenceContainer from '@/components/MkPreferenceContainer.vue';
+import { PREF_DEF } from '@/preferences/def.js';
 
-const notUseSound = computed(defaultStore.makeGetterSetter('sound_notUseSound'));
-const useSoundOnlyWhenActive = computed(defaultStore.makeGetterSetter('sound_useSoundOnlyWhenActive'));
-const masterVolume = computed(defaultStore.makeGetterSetter('sound_masterVolume'));
+const notUseSound = prefer.model('sound.notUseSound');
+const useSoundOnlyWhenActive = prefer.model('sound.useSoundOnlyWhenActive');
+const masterVolume = prefer.model('sound.masterVolume');
 
 const sounds = ref<Record<OperationType, Ref<SoundStore>>>({
-	note: defaultStore.reactiveState.sound_note,
-	noteMy: defaultStore.reactiveState.sound_noteMy,
-	notification: defaultStore.reactiveState.sound_notification,
-	reaction: defaultStore.reactiveState.sound_reaction,
+	note: prefer.r['sound.on.note'],
+	noteMy: prefer.r['sound.on.noteMy'],
+	notification: prefer.r['sound.on.notification'],
+	reaction: prefer.r['sound.on.reaction'],
 });
 
 function getSoundTypeName(f: SoundType): string {
@@ -93,14 +101,14 @@ async function updated(type: keyof typeof sounds.value, sound) {
 		volume: sound.volume,
 	};
 
-	defaultStore.set(`sound_${type}`, v);
+	prefer.commit(`sound.on.${type}`, v);
 	sounds.value[type] = v;
 }
 
 function reset() {
 	for (const sound of Object.keys(sounds.value) as Array<keyof typeof sounds.value>) {
-		const v = defaultStore.def[`sound_${sound}`].default;
-		defaultStore.set(`sound_${sound}`, v);
+		const v = PREF_DEF[`sound.on.${sound}`].default;
+		prefer.commit(`sound.on.${sound}`, v);
 		sounds.value[sound] = v;
 	}
 }
@@ -109,7 +117,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.sounds,
 	icon: 'ti ti-music',
 }));
diff --git a/packages/frontend/src/pages/settings/statusbar.statusbar.vue b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
index 140b6beb14..dbb640123a 100644
--- a/packages/frontend/src/pages/settings/statusbar.statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.statusbar.vue
@@ -94,17 +94,17 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkRadios from '@/components/MkRadios.vue';
 import MkButton from '@/components/MkButton.vue';
 import MkRange from '@/components/MkRange.vue';
-import { defaultStore } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
+import { prefer } from '@/preferences.js';
 
 const props = defineProps<{
 	_id: string;
 	userLists: Misskey.entities.UserList[] | null;
 }>();
 
-const statusbar = reactive(deepClone(defaultStore.state.statusbars.find(x => x.id === props._id)));
+const statusbar = reactive(deepClone(prefer.s.statusbars.find(x => x.id === props._id)));
 
 watch(() => statusbar.type, () => {
 	if (statusbar.type === 'rss') {
@@ -134,13 +134,13 @@ watch(() => statusbar.type, () => {
 watch(statusbar, save);
 
 async function save() {
-	const i = defaultStore.state.statusbars.findIndex(x => x.id === props._id);
-	const statusbars = deepClone(defaultStore.state.statusbars);
+	const i = prefer.s.statusbars.findIndex(x => x.id === props._id);
+	const statusbars = deepClone(prefer.s.statusbars);
 	statusbars[i] = deepClone(statusbar);
-	defaultStore.set('statusbars', statusbars);
+	prefer.commit('statusbars', statusbars);
 }
 
 function del() {
-	defaultStore.set('statusbars', defaultStore.state.statusbars.filter(x => x.id !== props._id));
+	prefer.commit('statusbars', prefer.s.statusbars.filter(x => x.id !== props._id));
 }
 </script>
diff --git a/packages/frontend/src/pages/settings/statusbar.vue b/packages/frontend/src/pages/settings/statusbar.vue
index 1ae3de7994..7e6a536216 100644
--- a/packages/frontend/src/pages/settings/statusbar.vue
+++ b/packages/frontend/src/pages/settings/statusbar.vue
@@ -21,12 +21,12 @@ import { v4 as uuid } from 'uuid';
 import XStatusbar from './statusbar.statusbar.vue';
 import MkFolder from '@/components/MkFolder.vue';
 import MkButton from '@/components/MkButton.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
+import { prefer } from '@/preferences.js';
 
-const statusbars = defaultStore.reactiveState.statusbars;
+const statusbars = prefer.r.statusbars;
 
 const userLists = ref<Misskey.entities.UserList[] | null>(null);
 
@@ -37,20 +37,20 @@ onMounted(() => {
 });
 
 async function add() {
-	defaultStore.push('statusbars', {
+	prefer.commit('statusbars', [...statusbars.value, {
 		id: uuid(),
 		type: null,
 		black: false,
 		size: 'medium',
 		props: {},
-	});
+	}]);
 }
 
 const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.statusbar,
 	icon: 'ti ti-list',
 }));
diff --git a/packages/frontend/src/pages/settings/theme.install.vue b/packages/frontend/src/pages/settings/theme.install.vue
index 4f05d3784c..68e4bef5c4 100644
--- a/packages/frontend/src/pages/settings/theme.install.vue
+++ b/packages/frontend/src/pages/settings/theme.install.vue
@@ -10,8 +10,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</MkCodeEditor>
 
 	<div class="_buttons">
-		<MkButton :disabled="installThemeCode == null" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
-		<MkButton :disabled="installThemeCode == null" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
+		<MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" inline @click="() => previewTheme(installThemeCode)"><i class="ti ti-eye"></i> {{ i18n.ts.preview }}</MkButton>
+		<MkButton :disabled="installThemeCode == null || installThemeCode.trim() === ''" primary inline @click="() => install(installThemeCode)"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
 	</div>
 </div>
 </template>
@@ -20,11 +20,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, computed } from 'vue';
 import MkCodeEditor from '@/components/MkCodeEditor.vue';
 import MkButton from '@/components/MkButton.vue';
-import { parseThemeCode, previewTheme, installTheme } from '@/scripts/install-theme.js';
+import { parseThemeCode, previewTheme, installTheme } from '@/theme.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
+import { useRouter } from '@/router/supplier.js';
 
+const router = useRouter();
 const installThemeCode = ref<string | null>(null);
 
 async function install(code: string): Promise<void> {
@@ -35,6 +37,8 @@ async function install(code: string): Promise<void> {
 			type: 'success',
 			text: i18n.tsx._theme.installed({ name: theme.name }),
 		});
+		installThemeCode.value = null;
+		router.push('/settings/theme');
 	} catch (err) {
 		switch (err.message.toLowerCase()) {
 			case 'this theme is already installed':
@@ -59,7 +63,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts._theme.install,
 	icon: 'ti ti-download',
 }));
diff --git a/packages/frontend/src/pages/settings/theme.manage.vue b/packages/frontend/src/pages/settings/theme.manage.vue
index f63f15fc13..e2b48ea232 100644
--- a/packages/frontend/src/pages/settings/theme.manage.vue
+++ b/packages/frontend/src/pages/settings/theme.manage.vue
@@ -37,13 +37,13 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkSelect from '@/components/MkSelect.vue';
 import MkInput from '@/components/MkInput.vue';
 import MkButton from '@/components/MkButton.vue';
-import { getBuiltinThemesRef } from '@/scripts/theme.js';
-import type { Theme } from '@/scripts/theme.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { getBuiltinThemesRef } from '@/theme.js';
+import type { Theme } from '@/theme.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import * as os from '@/os.js';
 import { getThemes, removeTheme } from '@/theme-store.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const installedThemes = ref(getThemes());
 const builtinThemes = getBuiltinThemesRef();
@@ -77,7 +77,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts._theme.manage,
 	icon: 'ti ti-tool',
 }));
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index b0e4ce13d5..0e4f791f2c 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <SearchMarker path="/settings/theme" :label="i18n.ts.theme" :keywords="['theme']" icon="ti ti-palette">
-	<div class="_gaps_m rsljpzjq">
+	<div class="_gaps_m">
 		<div v-adaptive-border class="rfqxtzch _panel">
 			<div class="toggle">
 				<div class="toggleWrapper">
@@ -36,23 +36,149 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 		</div>
 
-		<div class="selects">
-			<div class="select">
+		<div class="_gaps">
+			<template v-if="!darkMode">
 				<SearchMarker :keywords="['light', 'theme']">
-					<MkSelect v-model="lightThemeId" large :items="lightThemeSelectorItems">
+					<MkFolder :defaultOpen="true" :max-height="500">
+						<template #icon><i class="ti ti-sun"></i></template>
 						<template #label><SearchLabel>{{ i18n.ts.themeForLightMode }}</SearchLabel></template>
-						<template #prefix><i class="ti ti-sun"></i></template>
-					</MkSelect>
+						<template #caption>{{ lightThemeName }}</template>
+
+						<div class="_gaps_m">
+							<FormSection v-if="instanceLightTheme != null" first>
+								<template #label>{{ i18n.ts._theme.instanceTheme }}</template>
+								<div :class="$style.themeSelect">
+									<div :class="$style.themeItemOuter">
+										<input
+											:id="`themeRadio_${instanceLightTheme.id}`"
+											v-model="lightThemeId"
+											type="radio"
+											name="lightTheme"
+											:class="$style.themeRadio"
+											:value="instanceLightTheme.id"
+										/>
+										<label :for="`themeRadio_${instanceLightTheme.id}`" :class="$style.themeItemRoot" class="_button">
+											<MkThemePreview :theme="instanceLightTheme" :class="$style.themeItemPreview"/>
+											<div :class="$style.themeItemCaption">{{ instanceLightTheme.name }}</div>
+										</label>
+									</div>
+								</div>
+							</FormSection>
+
+							<FormSection v-if="installedLightThemes.length > 0" :first="instanceLightTheme == null">
+								<template #label>{{ i18n.ts._theme.installedThemes }}</template>
+								<div :class="$style.themeSelect">
+									<div v-for="theme in installedLightThemes" :class="$style.themeItemOuter">
+										<input
+											:id="`themeRadio_${theme.id}`"
+											v-model="lightThemeId"
+											type="radio"
+											name="lightTheme"
+											:class="$style.themeRadio"
+											:value="theme.id"
+										/>
+										<label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button">
+											<MkThemePreview :theme="theme" :class="$style.themeItemPreview"/>
+											<div :class="$style.themeItemCaption">{{ theme.name }}</div>
+										</label>
+									</div>
+								</div>
+							</FormSection>
+
+							<FormSection :first="installedLightThemes.length === 0 && instanceLightTheme == null">
+								<template #label>{{ i18n.ts._theme.builtinThemes }}</template>
+								<div :class="$style.themeSelect">
+									<div v-for="theme in builtinLightThemes" :class="$style.themeItemOuter">
+										<input
+											:id="`themeRadio_${theme.id}`"
+											v-model="lightThemeId"
+											type="radio"
+											name="lightTheme"
+											:class="$style.themeRadio"
+											:value="theme.id"
+										/>
+										<label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button">
+											<MkThemePreview :theme="theme" :class="$style.themeItemPreview"/>
+											<div :class="$style.themeItemCaption">{{ theme.name }}</div>
+										</label>
+									</div>
+								</div>
+							</FormSection>
+						</div>
+					</MkFolder>
 				</SearchMarker>
-			</div>
-			<div class="select">
+			</template>
+			<template v-else>
 				<SearchMarker :keywords="['dark', 'theme']">
-					<MkSelect v-model="darkThemeId" large :items="darkThemeSelectorItems">
+					<MkFolder :defaultOpen="true" :max-height="500">
+						<template #icon><i class="ti ti-moon"></i></template>
 						<template #label><SearchLabel>{{ i18n.ts.themeForDarkMode }}</SearchLabel></template>
-						<template #prefix><i class="ti ti-moon"></i></template>
-					</MkSelect>
+						<template #caption>{{ darkThemeName }}</template>
+
+						<div class="_gaps_m">
+							<FormSection v-if="instanceDarkTheme != null" first>
+								<template #label>{{ i18n.ts._theme.instanceTheme }}</template>
+								<div :class="$style.themeSelect">
+									<div :class="$style.themeItemOuter">
+										<input
+											:id="`themeRadio_${instanceDarkTheme.id}`"
+											v-model="darkThemeId"
+											type="radio"
+											name="darkTheme"
+											:class="$style.themeRadio"
+											:value="instanceDarkTheme.id"
+										/>
+										<label :for="`themeRadio_${instanceDarkTheme.id}`" :class="$style.themeItemRoot" class="_button">
+											<MkThemePreview :theme="instanceDarkTheme" :class="$style.themeItemPreview"/>
+											<div :class="$style.themeItemCaption">{{ instanceDarkTheme.name }}</div>
+										</label>
+									</div>
+								</div>
+							</FormSection>
+
+							<FormSection v-if="installedDarkThemes.length > 0" :first="instanceDarkTheme == null">
+								<template #label>{{ i18n.ts._theme.installedThemes }}</template>
+								<div :class="$style.themeSelect">
+									<div v-for="theme in installedDarkThemes" :class="$style.themeItemOuter">
+										<input
+											:id="`themeRadio_${theme.id}`"
+											v-model="darkThemeId"
+											type="radio"
+											name="darkTheme"
+											:class="$style.themeRadio"
+											:value="theme.id"
+										/>
+										<label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button">
+											<MkThemePreview :theme="theme" :class="$style.themeItemPreview"/>
+											<div :class="$style.themeItemCaption">{{ theme.name }}</div>
+										</label>
+									</div>
+								</div>
+							</FormSection>
+
+							<FormSection :first="installedDarkThemes.length === 0 && instanceDarkTheme == null">
+								<template #label>{{ i18n.ts._theme.builtinThemes }}</template>
+								<div :class="$style.themeSelect">
+									<div v-for="theme in builtinDarkThemes" :class="$style.themeItemOuter">
+										<input
+											:id="`themeRadio_${theme.id}`"
+											v-model="darkThemeId"
+											type="radio"
+											name="darkTheme"
+											:class="$style.themeRadio"
+											:value="theme.id"
+										/>
+										<label :for="`themeRadio_${theme.id}`" :class="$style.themeItemRoot" class="_button">
+											<MkThemePreview :theme="theme" :class="$style.themeItemPreview"/>
+											<div :class="$style.themeItemCaption">{{ theme.name }}</div>
+										</label>
+									</div>
+								</div>
+							</FormSection>
+						</div>
+					</MkFolder>
 				</SearchMarker>
-			</div>
+			</template>
 		</div>
 
 		<FormSection>
@@ -75,133 +201,74 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, onActivated, ref, watch } from 'vue';
 import JSON5 from 'json5';
-import type { MkSelectItem } from '@/components/MkSelect.vue';
+import defaultLightTheme from '@@/themes/l-light.json5';
+import defaultDarkTheme from '@@/themes/d-green-lime.json5';
+import type { Theme } from '@/theme.js';
 import MkSwitch from '@/components/MkSwitch.vue';
-import MkSelect from '@/components/MkSelect.vue';
 import FormSection from '@/components/form/section.vue';
 import FormLink from '@/components/form/link.vue';
 import MkButton from '@/components/MkButton.vue';
-import { getBuiltinThemesRef } from '@/scripts/theme.js';
-import { selectFile } from '@/scripts/select-file.js';
-import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
-import { ColdDeviceStorage, defaultStore } from '@/store.js';
+import MkFolder from '@/components/MkFolder.vue';
+import MkThemePreview from '@/components/MkThemePreview.vue';
+import { getBuiltinThemesRef } from '@/theme.js';
+import { selectFile } from '@/utility/select-file.js';
+import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { uniqueBy } from '@/scripts/array.js';
-import { fetchThemes, getThemes } from '@/theme-store.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { uniqueBy } from '@/utility/array.js';
+import { getThemes } from '@/theme-store.js';
+import { definePage } from '@/page.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { reloadAsk } from '@/scripts/reload-ask.js';
-import * as os from '@/os.js';
+import { reloadAsk } from '@/utility/reload-ask.js';
+import { prefer } from '@/preferences.js';
 
 const installedThemes = ref(getThemes());
 const builtinThemes = getBuiltinThemesRef();
 
-const instanceDarkTheme = computed(() => instance.defaultDarkTheme ? JSON5.parse(instance.defaultDarkTheme) : null);
+const instanceDarkTheme = computed<Theme | null>(() => instance.defaultDarkTheme ? JSON5.parse(instance.defaultDarkTheme) : null);
 const installedDarkThemes = computed(() => installedThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
 const builtinDarkThemes = computed(() => builtinThemes.value.filter(t => t.base === 'dark' || t.kind === 'dark'));
-const instanceLightTheme = computed(() => instance.defaultLightTheme ? JSON5.parse(instance.defaultLightTheme) : null);
+const instanceLightTheme = computed<Theme | null>(() => instance.defaultLightTheme ? JSON5.parse(instance.defaultLightTheme) : null);
 const installedLightThemes = computed(() => installedThemes.value.filter(t => t.base === 'light' || t.kind === 'light'));
 const builtinLightThemes = computed(() => builtinThemes.value.filter(t => t.base === 'light' || t.kind === 'light'));
 const themes = computed(() => uniqueBy([instanceDarkTheme.value, instanceLightTheme.value, ...builtinThemes.value, ...installedThemes.value].filter(x => x != null), theme => theme.id));
 
-const lightThemeSelectorItems = computed(() => {
-	const items = [] as MkSelectItem[];
-	if (instanceLightTheme.value) {
-		items.push({
-			type: 'option',
-			value: instanceLightTheme.value.id,
-			label: instanceLightTheme.value.name,
-		});
-	}
-	if (installedLightThemes.value.length > 0) {
-		items.push({
-			type: 'group',
-			label: i18n.ts._theme.installedThemes,
-			items: installedLightThemes.value.map(x => ({
-				type: 'option',
-				value: x.id,
-				label: x.name,
-			})),
-		});
-	}
-	items.push({
-		type: 'group',
-		label: i18n.ts._theme.builtinThemes,
-		items: builtinLightThemes.value.map(x => ({
-			type: 'option',
-			value: x.id,
-			label: x.name,
-		})),
-	});
-	return items;
-});
-
-const darkThemeSelectorItems = computed(() => {
-	const items = [] as MkSelectItem[];
-	if (instanceDarkTheme.value) {
-		items.push({
-			type: 'option',
-			value: instanceDarkTheme.value.id,
-			label: instanceDarkTheme.value.name,
-		});
-	}
-	if (installedDarkThemes.value.length > 0) {
-		items.push({
-			type: 'group',
-			label: i18n.ts._theme.installedThemes,
-			items: installedDarkThemes.value.map(x => ({
-				type: 'option',
-				value: x.id,
-				label: x.name,
-			})),
-		});
-	}
-	items.push({
-		type: 'group',
-		label: i18n.ts._theme.builtinThemes,
-		items: builtinDarkThemes.value.map(x => ({
-			type: 'option',
-			value: x.id,
-			label: x.name,
-		})),
-	});
-	return items;
-});
-
-const darkTheme = ColdDeviceStorage.ref('darkTheme');
+const darkTheme = prefer.r.darkTheme;
+const darkThemeName = computed(() => darkTheme.value?.name ?? defaultDarkTheme.name);
 const darkThemeId = computed({
 	get() {
-		return darkTheme.value.id;
+		return darkTheme.value ? darkTheme.value.id : defaultDarkTheme.id;
 	},
 	set(id) {
 		const t = themes.value.find(x => x.id === id);
 		if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる
-			ColdDeviceStorage.set('darkTheme', t);
+			prefer.commit('darkTheme', t);
 		}
 	},
 });
-const lightTheme = ColdDeviceStorage.ref('lightTheme');
+const lightTheme = prefer.r.lightTheme;
+const lightThemeName = computed(() => lightTheme.value?.name ?? defaultLightTheme.name);
 const lightThemeId = computed({
 	get() {
-		return lightTheme.value.id;
+		return lightTheme.value ? lightTheme.value.id : defaultLightTheme.id;
 	},
 	set(id) {
 		const t = themes.value.find(x => x.id === id);
 		if (t) { // テーマエディタでテーマを作成したときなどは、themesに反映されないため undefined になる
-			ColdDeviceStorage.set('lightTheme', t);
+			prefer.commit('lightTheme', t);
 		}
 	},
 });
 
-const darkMode = computed(defaultStore.makeGetterSetter('darkMode'));
-const syncDeviceDarkMode = computed(ColdDeviceStorage.makeGetterSetter('syncDeviceDarkMode'));
+const darkMode = computed(store.makeGetterSetter('darkMode'));
+const syncDeviceDarkMode = prefer.model('syncDeviceDarkMode');
 const wallpaper = ref(miLocalStorage.getItem('wallpaper'));
 const themesCount = installedThemes.value.length;
 
 watch(syncDeviceDarkMode, () => {
 	if (syncDeviceDarkMode.value) {
-		defaultStore.set('darkMode', isDeviceDarkmode());
+		store.set('darkMode', isDeviceDarkmode());
 	}
 });
 
@@ -215,12 +282,6 @@ watch(wallpaper, async () => {
 });
 
 onActivated(() => {
-	fetchThemes().then(() => {
-		installedThemes.value = getThemes();
-	});
-});
-
-fetchThemes().then(() => {
 	installedThemes.value = getThemes();
 });
 
@@ -234,12 +295,63 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.theme,
 	icon: 'ti ti-palette',
 }));
 </script>
 
+<style module>
+.themeSelect {
+	display: grid;
+	grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
+	gap: var(--MI-margin);
+}
+
+.themeItemOuter {
+	position: relative;
+}
+
+.themeRadio {
+	position: absolute;
+	clip: rect(0, 0, 0, 0);
+	pointer-events: none;
+}
+
+.themeItemRoot {
+	position: relative;
+	display: block;
+	overflow: clip;
+	box-sizing: border-box;
+	border: 2px solid var(--MI_THEME-divider);
+	border-radius: var(--MI-radius);
+}
+
+.themeRadio:focus-visible + .themeItemRoot {
+	outline: 2px solid var(--MI_THEME-focus);
+	outline-offset: 2px;
+}
+
+.themeRadio:checked + .themeItemRoot {
+	border-color: var(--MI_THEME-accent);
+}
+
+.themeItemPreview {
+	display: block;
+	width: calc(100% + 2px);
+	height: auto;
+	margin-left: -1px;
+	border-bottom: 1px solid var(--MI_THEME-divider);
+}
+
+.themeItemCaption {
+	box-sizing: border-box;
+	padding: 8px 12px;
+	text-align: center;
+	font-size: 80%;
+}
+</style>
+
 <style lang="scss" scoped>
 .rfqxtzch {
 	border-radius: 6px;
@@ -475,17 +587,4 @@ definePageMetadata(() => ({
 		border-top: solid 0.5px var(--MI_THEME-divider);
 	}
 }
-
-.rsljpzjq {
-	> .selects {
-		display: flex;
-		gap: 1.5em var(--MI-margin);
-		flex-wrap: wrap;
-
-		> .select {
-			flex: 1;
-			min-width: 280px;
-		}
-	}
-}
 </style>
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index 22b008fb61..2de948c69d 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -76,9 +76,9 @@ import FormSection from '@/components/form/section.vue';
 import MkSwitch from '@/components/MkSwitch.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { useRouter } from '@/router/supplier.js';
 
 const router = useRouter();
@@ -155,7 +155,7 @@ const headerActions = computed(() => []);
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'Edit webhook',
 	icon: 'ti ti-webhook',
 }));
diff --git a/packages/frontend/src/pages/settings/webhook.new.vue b/packages/frontend/src/pages/settings/webhook.new.vue
index 727c4df2d6..e853f967cb 100644
--- a/packages/frontend/src/pages/settings/webhook.new.vue
+++ b/packages/frontend/src/pages/settings/webhook.new.vue
@@ -46,7 +46,7 @@ import MkSwitch from '@/components/MkSwitch.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const name = ref('');
 const url = ref('');
@@ -82,7 +82,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'Create new webhook',
 	icon: 'ti ti-webhook',
 }));
diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue
index af8b7ca945..bf8af8cdce 100644
--- a/packages/frontend/src/pages/settings/webhook.vue
+++ b/packages/frontend/src/pages/settings/webhook.vue
@@ -37,7 +37,7 @@ import { computed } from 'vue';
 import MkPagination from '@/components/MkPagination.vue';
 import FormSection from '@/components/form/section.vue';
 import FormLink from '@/components/form/link.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 
 const pagination = {
@@ -50,7 +50,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: 'Webhook',
 	icon: 'ti ti-webhook',
 }));
diff --git a/packages/frontend/src/pages/share.vue b/packages/frontend/src/pages/share.vue
index 37f6558d64..abd84c8590 100644
--- a/packages/frontend/src/pages/share.vue
+++ b/packages/frontend/src/pages/share.vue
@@ -37,9 +37,9 @@ import * as Misskey from 'misskey-js';
 import MkButton from '@/components/MkButton.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { postMessageToParentWindow } from '@/scripts/post-message.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
+import { postMessageToParentWindow } from '@/utility/post-message.js';
 import { i18n } from '@/i18n.js';
 
 const urlParams = new URLSearchParams(window.location.search);
@@ -199,7 +199,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.share,
 	icon: 'ti ti-share',
 }));
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index 503e6e0f73..5fe4de3c91 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -31,7 +31,7 @@ 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 '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 const submitting = ref(false);
 
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index b669e25179..1af69d82db 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -23,12 +23,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, ref } from 'vue';
 import MkNotes from '@/components/MkNotes.vue';
 import MkButton from '@/components/MkButton.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import * as os from '@/os.js';
-import { genEmbedCode } from '@/scripts/get-embed-code.js';
+import { genEmbedCode } from '@/utility/get-embed-code.js';
 
 const props = defineProps<{
 	tag: string;
@@ -44,11 +44,11 @@ const pagination = {
 const notes = ref<InstanceType<typeof MkNotes>>();
 
 async function post() {
-	defaultStore.set('postFormHashtags', props.tag);
-	defaultStore.set('postFormWithHashtags', true);
+	store.set('postFormHashtags', props.tag);
+	store.set('postFormWithHashtags', true);
 	await os.post();
-	defaultStore.set('postFormHashtags', '');
-	defaultStore.set('postFormWithHashtags', false);
+	store.set('postFormHashtags', '');
+	store.set('postFormWithHashtags', false);
 	notes.value?.pagingComponent?.reload();
 }
 
@@ -68,7 +68,7 @@ const headerActions = computed(() => [{
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: props.tag,
 	icon: 'ti ti-hash',
 }));
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index 76567cc403..11971756f4 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -78,24 +78,23 @@ import { toUnicode } from 'punycode.js';
 import tinycolor from 'tinycolor2';
 import { v4 as uuid } from 'uuid';
 import JSON5 from 'json5';
-
 import lightTheme from '@@/themes/_light.json5';
 import darkTheme from '@@/themes/_dark.json5';
+import { host } from '@@/js/config.js';
+import type { Theme } from '@/theme.js';
 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 { applyTheme } from '@/scripts/theme.js';
-import type { Theme } from '@/scripts/theme.js';
-import { host } from '@@/js/config.js';
+import { applyTheme } from '@/theme.js';
 import * as os from '@/os.js';
-import { ColdDeviceStorage, defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { addTheme } from '@/theme-store.js';
 import { i18n } from '@/i18n.js';
-import { useLeaveGuard } from '@/scripts/use-leave-guard.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { useLeaveGuard } from '@/utility/use-leave-guard.js';
+import { definePage } from '@/page.js';
+import { prefer } from '@/preferences.js';
 
 const bgColors = [
 	{ color: '#f5f5f5', kind: 'light', forPreview: '#f5f5f5' },
@@ -201,10 +200,10 @@ async function saveAs() {
 	if (description.value) theme.value.desc = description.value;
 	await addTheme(theme.value);
 	applyTheme(theme.value);
-	if (defaultStore.state.darkMode) {
-		ColdDeviceStorage.set('darkTheme', theme.value);
+	if (store.s.darkMode) {
+		prefer.commit('darkTheme', theme.value);
 	} else {
-		ColdDeviceStorage.set('lightTheme', theme.value);
+		prefer.commit('lightTheme', theme.value);
 	}
 	changed.value = false;
 	os.alert({
@@ -229,7 +228,7 @@ const headerActions = computed(() => [{
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.themeEditor,
 	icon: 'ti ti-palette',
 }));
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 2963b9365c..0cf5d8bbe9 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkSpacer :contentMax="800">
 		<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
 			<div :key="src" ref="rootEl">
-				<MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
+				<MkInfo v-if="isBasicTimeline(src) && !store.r.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
 					{{ i18n.ts._timelineDescription[src] }}
 				</MkInfo>
-				<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
+				<MkPostForm v-if="prefer.r.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
 				<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
 				<div :class="$style.tl">
 					<MkTimeline
@@ -45,16 +45,17 @@ import MkInfo from '@/components/MkInfo.vue';
 import MkPostForm from '@/components/MkPostForm.vue';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { antennasCache, userListsCache, favoritedChannelsCache } from '@/cache.js';
-import { deviceKind } from '@/scripts/device-kind.js';
-import { deepMerge } from '@/scripts/merge.js';
+import { deviceKind } from '@/utility/device-kind.js';
+import { deepMerge } from '@/utility/merge.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { availableBasicTimelines, hasWithReplies, isAvailableBasicTimeline, isBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
+import { prefer } from '@/preferences.js';
 
 provide('shouldOmitHeaderTitle', true);
 
@@ -66,18 +67,18 @@ type TimelinePageSrc = BasicTimelineType | `list:${string}`;
 const queue = ref(0);
 const srcWhenNotSignin = ref<'local' | 'global'>(isAvailableBasicTimeline('local') ? 'local' : 'global');
 const src = computed<TimelinePageSrc>({
-	get: () => ($i ? defaultStore.reactiveState.tl.value.src : srcWhenNotSignin.value),
+	get: () => ($i ? store.r.tl.value.src : srcWhenNotSignin.value),
 	set: (x) => saveSrc(x),
 });
 const withRenotes = computed<boolean>({
-	get: () => defaultStore.reactiveState.tl.value.filter.withRenotes,
+	get: () => store.r.tl.value.filter.withRenotes,
 	set: (x) => saveTlFilter('withRenotes', x),
 });
 
 // computed内での無限ループを防ぐためのフラグ
 const localSocialTLFilterSwitchStore = ref<'withReplies' | 'onlyFiles' | false>(
-	defaultStore.reactiveState.tl.value.filter.withReplies ? 'withReplies' :
-	defaultStore.reactiveState.tl.value.filter.onlyFiles ? 'onlyFiles' :
+	store.r.tl.value.filter.withReplies ? 'withReplies' :
+	store.r.tl.value.filter.onlyFiles ? 'onlyFiles' :
 	false,
 );
 
@@ -87,7 +88,7 @@ const withReplies = computed<boolean>({
 		if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'onlyFiles') {
 			return false;
 		} else {
-			return defaultStore.reactiveState.tl.value.filter.withReplies;
+			return store.r.tl.value.filter.withReplies;
 		}
 	},
 	set: (x) => saveTlFilter('withReplies', x),
@@ -97,7 +98,7 @@ const onlyFiles = computed<boolean>({
 		if (['local', 'social'].includes(src.value) && localSocialTLFilterSwitchStore.value === 'withReplies') {
 			return false;
 		} else {
-			return defaultStore.reactiveState.tl.value.filter.onlyFiles;
+			return store.r.tl.value.filter.onlyFiles;
 		}
 	},
 	set: (x) => saveTlFilter('onlyFiles', x),
@@ -114,7 +115,7 @@ watch([withReplies, onlyFiles], ([withRepliesTo, onlyFilesTo]) => {
 });
 
 const withSensitive = computed<boolean>({
-	get: () => defaultStore.reactiveState.tl.value.filter.withSensitive,
+	get: () => store.r.tl.value.filter.withSensitive,
 	set: (x) => saveTlFilter('withSensitive', x),
 });
 
@@ -195,23 +196,23 @@ async function chooseChannel(ev: MouseEvent): Promise<void> {
 }
 
 function saveSrc(newSrc: TimelinePageSrc): void {
-	const out = deepMerge({ src: newSrc }, defaultStore.state.tl);
+	const out = deepMerge({ src: newSrc }, store.s.tl);
 
 	if (newSrc.startsWith('userList:')) {
 		const id = newSrc.substring('userList:'.length);
-		out.userList = defaultStore.reactiveState.pinnedUserLists.value.find(l => l.id === id) ?? null;
+		out.userList = prefer.r.pinnedUserLists.value.find(l => l.id === id) ?? null;
 	}
 
-	defaultStore.set('tl', out);
+	store.set('tl', out);
 	if (['local', 'global'].includes(newSrc)) {
 		srcWhenNotSignin.value = newSrc as 'local' | 'global';
 	}
 }
 
-function saveTlFilter(key: keyof typeof defaultStore.state.tl.filter, newValue: boolean) {
+function saveTlFilter(key: keyof typeof store.s.tl.filter, newValue: boolean) {
 	if (key !== 'withReplies' || $i) {
-		const out = deepMerge({ filter: { [key]: newValue } }, defaultStore.state.tl);
-		defaultStore.set('tl', out);
+		const out = deepMerge({ filter: { [key]: newValue } }, store.s.tl);
+		store.set('tl', out);
 	}
 }
 
@@ -231,9 +232,9 @@ function focus(): void {
 
 function closeTutorial(): void {
 	if (!isBasicTimeline(src.value)) return;
-	const before = defaultStore.state.timelineTutorials;
+	const before = store.s.timelineTutorials;
 	before[src.value] = true;
-	defaultStore.set('timelineTutorials', before);
+	store.set('timelineTutorials', before);
 }
 
 function switchTlIfNeeded() {
@@ -299,7 +300,7 @@ const headerActions = computed(() => {
 	return tmp;
 });
 
-const headerTabs = computed(() => [...(defaultStore.reactiveState.pinnedUserLists.value.map(l => ({
+const headerTabs = computed(() => [...(prefer.r.pinnedUserLists.value.map(l => ({
 	key: 'list:' + l.id,
 	title: l.name,
 	icon: 'ti ti-star',
@@ -333,7 +334,7 @@ const headerTabsWhenNotLogin = computed(() => [...availableBasicTimelines().map(
 	iconOnly: true,
 }))] as Tab[]);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.timeline,
 	icon: isBasicTimeline(src.value) ? basicTimelineIconClass(src.value) : 'ti ti-home',
 }));
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index 3efeb46c0a..d4c7c9386d 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -28,8 +28,8 @@ import { computed, watch, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { scroll } from '@@/js/scroll.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { useRouter } from '@/router/supplier.js';
 
@@ -70,7 +70,7 @@ const headerActions = computed(() => list.value ? [{
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: list.value ? list.value.name : i18n.ts.lists,
 	icon: 'ti ti-list',
 }));
diff --git a/packages/frontend/src/pages/user-tag.vue b/packages/frontend/src/pages/user-tag.vue
index a77493fe47..fc9ff92080 100644
--- a/packages/frontend/src/pages/user-tag.vue
+++ b/packages/frontend/src/pages/user-tag.vue
@@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed } from 'vue';
 import MkUserList from '@/components/MkUserList.vue';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 
 const props = defineProps<{
 	tag: string;
@@ -34,7 +34,7 @@ const tagUsers = computed(() => ({
 	},
 }));
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: props.tag,
 	icon: 'ti ti-user-search',
 }));
diff --git a/packages/frontend/src/pages/user/achievements.vue b/packages/frontend/src/pages/user/achievements.vue
index 403e74904c..b78ac2dc17 100644
--- a/packages/frontend/src/pages/user/achievements.vue
+++ b/packages/frontend/src/pages/user/achievements.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import MkAchievements from '@/components/MkAchievements.vue';
-import { claimAchievement } from '@/scripts/achievements.js';
+import { claimAchievement } from '@/utility/achievements.js';
 import { $i } from '@/account.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/pages/user/activity.following.vue b/packages/frontend/src/pages/user/activity.following.vue
index 7b74ea67ca..a86387571a 100644
--- a/packages/frontend/src/pages/user/activity.following.vue
+++ b/packages/frontend/src/pages/user/activity.following.vue
@@ -19,12 +19,12 @@ import { Chart } from 'chart.js';
 import type { ChartDataset } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { initChart } from '@/scripts/init-chart.js';
-import { chartLegend } from '@/scripts/chart-legend.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
+import { useChartTooltip } from '@/utility/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';
 import MkChartLegend from '@/components/MkChartLegend.vue';
 
 initChart();
@@ -64,7 +64,7 @@ async function renderChart() {
 
 	const raw = await misskeyApi('charts/user/following', { userId: props.user.id, limit: chartLimit, span: 'day' });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const colorFollowLocal = '#008FFB';
 	const colorFollowRemote = '#008FFB88';
diff --git a/packages/frontend/src/pages/user/activity.notes.vue b/packages/frontend/src/pages/user/activity.notes.vue
index 8c7484ae08..d083fdebed 100644
--- a/packages/frontend/src/pages/user/activity.notes.vue
+++ b/packages/frontend/src/pages/user/activity.notes.vue
@@ -19,12 +19,12 @@ import { Chart } from 'chart.js';
 import type { ChartDataset } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { initChart } from '@/scripts/init-chart.js';
-import { chartLegend } from '@/scripts/chart-legend.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
+import { useChartTooltip } from '@/utility/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';
 import MkChartLegend from '@/components/MkChartLegend.vue';
 
 initChart();
@@ -64,7 +64,7 @@ async function renderChart() {
 
 	const raw = await misskeyApi('charts/user/notes', { userId: props.user.id, limit: chartLimit, span: 'day' });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const colorNormal = '#008FFB';
 	const colorReply = '#FEB019';
diff --git a/packages/frontend/src/pages/user/activity.pv.vue b/packages/frontend/src/pages/user/activity.pv.vue
index a073626cbb..d5e8f45608 100644
--- a/packages/frontend/src/pages/user/activity.pv.vue
+++ b/packages/frontend/src/pages/user/activity.pv.vue
@@ -19,12 +19,12 @@ import { Chart } from 'chart.js';
 import type { ChartDataset } from 'chart.js';
 import * as Misskey from 'misskey-js';
 import gradient from 'chartjs-plugin-gradient';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore } from '@/store.js';
-import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
-import { chartVLine } from '@/scripts/chart-vline.js';
-import { initChart } from '@/scripts/init-chart.js';
-import { chartLegend } from '@/scripts/chart-legend.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { store } from '@/store.js';
+import { useChartTooltip } from '@/utility/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';
 import MkChartLegend from '@/components/MkChartLegend.vue';
 
 initChart();
@@ -64,7 +64,7 @@ async function renderChart() {
 
 	const raw = await misskeyApi('charts/user/pv', { userId: props.user.id, limit: chartLimit, span: 'day' });
 
-	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 
 	const colorUser = '#3498db';
 	const colorVisitor = '#2ecc71';
diff --git a/packages/frontend/src/pages/user/followers.vue b/packages/frontend/src/pages/user/followers.vue
index 70883242e5..d9b2623e27 100644
--- a/packages/frontend/src/pages/user/followers.vue
+++ b/packages/frontend/src/pages/user/followers.vue
@@ -22,8 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XFollowList from './follow-list.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
@@ -52,7 +52,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.user,
 	icon: 'ti ti-user',
 	...user.value ? {
diff --git a/packages/frontend/src/pages/user/following.vue b/packages/frontend/src/pages/user/following.vue
index 37b25f694f..6f8dfa0124 100644
--- a/packages/frontend/src/pages/user/following.vue
+++ b/packages/frontend/src/pages/user/following.vue
@@ -22,8 +22,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XFollowList from './follow-list.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 
 const props = withDefaults(defineProps<{
@@ -52,7 +52,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.user,
 	icon: 'ti ti-user',
 	...user.value ? {
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 8ebcf975b7..6450f1e077 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -171,20 +171,20 @@ import MkTextarea from '@/components/MkTextarea.vue';
 import MkOmit from '@/components/MkOmit.vue';
 import MkInfo from '@/components/MkInfo.vue';
 import MkButton from '@/components/MkButton.vue';
-import { getUserMenu } from '@/scripts/get-user-menu.js';
+import { getUserMenu } from '@/utility/get-user-menu.js';
 import number from '@/filters/number.js';
 import { userPage } from '@/filters/user.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
 import { $i, iAmModerator } from '@/account.js';
 import { dateString } from '@/filters/date.js';
-import { confetti } from '@/scripts/confetti.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFfVisibleForMe.js';
+import { confetti } from '@/utility/confetti.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/utility/isFfVisibleForMe.js';
 import { useRouter } from '@/router/supplier.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
 import MkSparkle from '@/components/MkSparkle.vue';
+import { prefer } from '@/preferences.js';
 
 function calcAge(birthdate: string): number {
 	const date = new Date(birthdate);
@@ -236,7 +236,7 @@ watch(moderationNote, async () => {
 
 const style = computed(() => {
 	if (props.user.bannerUrl == null) return {};
-	if (defaultStore.state.disableShowingAnimatedImages) {
+	if (prefer.s.disableShowingAnimatedImages) {
 		return {
 			backgroundImage: `url(${ getStaticImageUrl(props.user.bannerUrl) })`,
 		};
diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue
index b5e5f29ade..6c3b8408fb 100644
--- a/packages/frontend/src/pages/user/index.files.vue
+++ b/packages/frontend/src/pages/user/index.files.vue
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { i18n } from '@/i18n.js';
 import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue';
diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue
index c43f6c76d9..b5127de390 100644
--- a/packages/frontend/src/pages/user/index.vue
+++ b/packages/frontend/src/pages/user/index.vue
@@ -35,8 +35,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent, computed, watch, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { acct as getAcct } from '@/filters/user.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { definePage } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
 import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
@@ -147,7 +147,7 @@ const headerTabs = computed(() => user.value ? [{
 	icon: 'ti ti-code',
 }] : []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: i18n.ts.user,
 	icon: 'ti ti-user',
 	...user.value ? {
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index 34c5c3ce6c..ebbe27e3d4 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -42,9 +42,9 @@ import XTimeline from './welcome.timeline.vue';
 import MarqueeText from '@/components/MkMarquee.vue';
 import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue';
 import misskeysvg from '/client-assets/misskey.svg';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
-import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrl } from '@/utility/media-proxy.js';
 import { instance as meta } from '@/instance.js';
 
 const instances = ref<Misskey.entities.FederationInstance[]>();
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 33cc139a45..939ca934e8 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -44,7 +44,7 @@ import { host, version } from '@@/js/config.js';
 import MkButton from '@/components/MkButton.vue';
 import MkInput from '@/components/MkInput.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { login } from '@/account.js';
 import { i18n } from '@/i18n.js';
 import MkAnimBg from '@/components/MkAnimBg.vue';
diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue
index 9be3a80a9e..180b2e5f77 100644
--- a/packages/frontend/src/pages/welcome.timeline.vue
+++ b/packages/frontend/src/pages/welcome.timeline.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import * as Misskey from 'misskey-js';
 import { onUpdated, ref, shallowRef } from 'vue';
 import XNote from '@/pages/welcome.timeline.note.vue';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { getScrollContainer } from '@@/js/scroll.js';
 
 const notes = ref<Misskey.entities.Note[]>([]);
diff --git a/packages/frontend/src/pages/welcome.vue b/packages/frontend/src/pages/welcome.vue
index 38d257506c..d3e571c053 100644
--- a/packages/frontend/src/pages/welcome.vue
+++ b/packages/frontend/src/pages/welcome.vue
@@ -16,7 +16,7 @@ import * as Misskey from 'misskey-js';
 import XSetup from './welcome.setup.vue';
 import XEntrance from './welcome.entrance.a.vue';
 import { instanceName } from '@@/js/config.js';
-import { definePageMetadata } from '@/scripts/page-metadata.js';
+import { definePage } from '@/page.js';
 import { fetchInstance } from '@/instance.js';
 
 const instance = ref<Misskey.entities.MetaDetailed | null>(null);
@@ -29,7 +29,7 @@ const headerActions = computed(() => []);
 
 const headerTabs = computed(() => []);
 
-definePageMetadata(() => ({
+definePage(() => ({
 	title: instanceName,
 	icon: null,
 }));
diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts
index 918b81b204..f55b1e93cf 100644
--- a/packages/frontend/src/pizzax.ts
+++ b/packages/frontend/src/pizzax.ts
@@ -9,12 +9,12 @@ import { onUnmounted, ref, watch } from 'vue';
 import { BroadcastChannel } from 'broadcast-channel';
 import type { Ref } from 'vue';
 import { $i } from '@/account.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { get, set } from '@/scripts/idb-proxy.js';
-import { defaultStore } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { get, set } from '@/utility/idb-proxy.js';
+import { store } from '@/store.js';
 import { useStream } from '@/stream.js';
-import { deepClone } from '@/scripts/clone.js';
-import { deepMerge } from '@/scripts/merge.js';
+import { deepClone } from '@/utility/clone.js';
+import { deepMerge } from '@/utility/merge.js';
 
 type StateDef = Record<string, {
 	where: 'account' | 'device' | 'deviceAccount';
@@ -45,8 +45,15 @@ export class Storage<T extends StateDef> {
 	public readonly def: T;
 
 	// TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487
-	public readonly state: State<T>;
-	public readonly reactiveState: ReactiveState<T>;
+	/**
+	 * static / state の略 (static が予約語のため)
+	 */
+	public readonly s: State<T>;
+
+	/**
+	 * reactive の略
+	 */
+	public readonly r: ReactiveState<T>;
 
 	private pizzaxChannel: BroadcastChannel<PizzaxChannelMessage<T>>;
 
@@ -70,12 +77,12 @@ export class Storage<T extends StateDef> {
 
 		this.pizzaxChannel = new BroadcastChannel(`pizzax::${key}`);
 
-		this.state = {} as State<T>;
-		this.reactiveState = {} as ReactiveState<T>;
+		this.s = {} as State<T>;
+		this.r = {} as ReactiveState<T>;
 
 		for (const [k, v] of Object.entries(def) as [keyof T, T[keyof T]['default']][]) {
-			this.state[k] = v.default;
-			this.reactiveState[k] = ref(v.default);
+			this.s[k] = v.default;
+			this.r[k] = ref(v.default);
 		}
 
 		this.ready = this.init();
@@ -106,13 +113,13 @@ export class Storage<T extends StateDef> {
 
 		for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
 			if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
-				this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], v.default);
+				this.r[k].value = this.s[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], v.default);
 			} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
-				this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], v.default);
+				this.r[k].value = this.s[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], v.default);
 			} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
-				this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
+				this.r[k].value = this.s[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
 			} else {
-				this.reactiveState[k].value = this.state[k] = v.default;
+				this.r[k].value = this.s[k] = v.default;
 			}
 		}
 
@@ -120,7 +127,7 @@ export class Storage<T extends StateDef> {
 			// アカウント変更すればunisonReloadが効くため、このreturnが発火することは
 			// まずないと思うけど一応弾いておく
 			if (where === 'deviceAccount' && !($i && userId !== $i.id)) return;
-			this.reactiveState[key].value = this.state[key] = value;
+			this.r[key].value = this.s[key] = value;
 		});
 
 		if ($i) {
@@ -128,9 +135,9 @@ export class Storage<T extends StateDef> {
 
 			// streamingのuser storage updateイベントを監視して更新
 			connection.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => {
-				if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return;
+				if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.s[key] === value) return;
 
-				this.reactiveState[key].value = this.state[key] = value;
+				this.r[key].value = this.s[key] = value;
 
 				this.addIdbSetJob(async () => {
 					const cache = await get(this.registryCacheKeyName);
@@ -148,7 +155,7 @@ export class Storage<T extends StateDef> {
 			if ($i) {
 				// api関数と循環参照なので一応setTimeoutしておく
 				window.setTimeout(async () => {
-					await defaultStore.ready;
+					await store.ready;
 
 					misskeyApi('i/registry/get-all', { scope: ['client', this.key] })
 						.then(kvs => {
@@ -156,10 +163,10 @@ export class Storage<T extends StateDef> {
 							for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
 								if (v.where === 'account') {
 									if (Object.prototype.hasOwnProperty.call(kvs, k)) {
-										this.reactiveState[k].value = this.state[k] = (kvs as Partial<T>)[k];
+										this.r[k].value = this.s[k] = (kvs as Partial<T>)[k];
 										cache[k] = (kvs as Partial<T>)[k];
 									} else {
-										this.reactiveState[k].value = this.state[k] = v.default;
+										this.r[k].value = this.s[k] = v.default;
 									}
 								}
 							}
@@ -179,7 +186,7 @@ export class Storage<T extends StateDef> {
 		// (JSON.parse(JSON.stringify(value))の代わり)
 		const rawValue = deepClone(value);
 
-		this.reactiveState[key].value = this.state[key] = rawValue;
+		this.r[key].value = this.s[key] = rawValue;
 
 		return this.addIdbSetJob(async () => {
 			switch (this.def[key].where) {
@@ -224,7 +231,7 @@ export class Storage<T extends StateDef> {
 	}
 
 	public push<K extends keyof T>(key: K, value: ArrayElement<T[K]['default']>): void {
-		const currentState = this.state[key];
+		const currentState = this.s[key];
 		this.set(key, [...currentState, value]);
 	}
 
@@ -237,6 +244,7 @@ export class Storage<T extends StateDef> {
 	 * 特定のキーの、簡易的なgetter/setterを作ります
 	 * 主にvue上で設定コントロールのmodelとして使う用
 	 */
+	// TODO: 廃止
 	public makeGetterSetter<K extends keyof T, R = T[K]['default']>(
 		key: K,
 		getter?: (v: T[K]['default']) => R,
@@ -245,9 +253,9 @@ export class Storage<T extends StateDef> {
 			get: () => R;
 			set: (value: R) => void;
 		} {
-		const valueRef = ref(this.state[key]);
+		const valueRef = ref(this.s[key]);
 
-		const stop = watch(this.reactiveState[key], val => {
+		const stop = watch(this.r[key], val => {
 			valueRef.value = val;
 		});
 
diff --git a/packages/frontend/src/plugin.ts b/packages/frontend/src/plugin.ts
index e319a8c398..1b51850e77 100644
--- a/packages/frontend/src/plugin.ts
+++ b/packages/frontend/src/plugin.ts
@@ -3,180 +3,430 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { ref } from 'vue';
+import { ref, defineAsyncComponent } from 'vue';
 import { Interpreter, Parser, utils, values } from '@syuilo/aiscript';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
-import { inputText } from '@/os.js';
-import { noteActions, notePostInterruptors, noteViewInterruptors, postFormActions, userActions, pageViewInterruptors } from '@/store.js';
-import type { Plugin } from '@/store.js';
+import { compareVersions } from 'compare-versions';
+import { v4 as uuid } from 'uuid';
+import * as Misskey from 'misskey-js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
+import { store } from '@/store.js';
+import * as os from '@/os.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
+
+export type Plugin = {
+	installId: string;
+	name: string;
+	active: boolean;
+	config?: Record<string, { default: any }>;
+	configData: Record<string, any>;
+	src: string | null;
+	version: string;
+	author?: string;
+	description?: string;
+	permissions?: string[];
+};
+
+export type AiScriptPluginMeta = {
+	name: string;
+	version: string;
+	author: string;
+	description?: string;
+	permissions?: string[];
+	config?: Record<string, any>;
+};
 
 const parser = new Parser();
-const pluginContexts = new Map<string, Interpreter>();
-export const pluginLogs = ref(new Map<string, string[]>());
 
-export async function install(plugin: Plugin): Promise<void> {
+export function isSupportedAiScriptVersion(version: string): boolean {
+	try {
+		return (compareVersions(version, '0.12.0') >= 0);
+	} catch (err) {
+		return false;
+	}
+}
+
+export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta> {
+	if (!code) {
+		throw new Error('code is required');
+	}
+
+	const lv = utils.getLangVersion(code);
+	if (lv == null) {
+		throw new Error('No language version annotation found');
+	} else if (!isSupportedAiScriptVersion(lv)) {
+		throw new Error(`Aiscript version '${lv}' is not supported`);
+	}
+
+	let ast;
+	try {
+		ast = parser.parse(code);
+	} catch (err) {
+		throw new Error('Aiscript syntax error');
+	}
+
+	const meta = Interpreter.collectMetadata(ast);
+	if (meta == null) {
+		throw new Error('Meta block not found');
+	}
+
+	const metadata = meta.get(null);
+	if (metadata == null) {
+		throw new Error('Metadata not found');
+	}
+
+	const { name, version, author, description, permissions, config } = metadata;
+	if (name == null || version == null || author == null) {
+		throw new Error('Required property not found');
+	}
+
+	return {
+		name,
+		version,
+		author,
+		description,
+		permissions,
+		config,
+	};
+}
+
+export async function authorizePlugin(plugin: Plugin) {
+	if (plugin.permissions == null || plugin.permissions.length === 0) return;
+	if (Object.hasOwn(store.s.pluginTokens, plugin.installId)) return;
+
+	const token = await new Promise<string>((res, rej) => {
+		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
+			title: i18n.ts.tokenRequested,
+			information: i18n.ts.pluginTokenRequestedDescription,
+			initialName: plugin.name,
+			initialPermissions: plugin.permissions,
+		}, {
+			done: async result => {
+				const { name, permissions } = result;
+				const { token } = await misskeyApi('miauth/gen-token', {
+					session: null,
+					name: name,
+					permission: permissions,
+				});
+				res(token);
+			},
+			closed: () => dispose(),
+		});
+	});
+
+	store.set('pluginTokens', {
+		...store.s.pluginTokens,
+		[plugin.installId]: token,
+	});
+}
+
+export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
+	if (!code) return;
+
+	let realMeta: AiScriptPluginMeta;
+	if (!meta) {
+		realMeta = await parsePluginMeta(code);
+	} else {
+		realMeta = meta;
+	}
+
+	if (prefer.s.plugins.some(x => x.name === realMeta.name)) {
+		throw new Error('Plugin already installed');
+	}
+
+	const installId = uuid();
+
+	const plugin = {
+		...realMeta,
+		installId,
+		active: true,
+		configData: {},
+		src: code,
+	};
+
+	prefer.commit('plugins', prefer.s.plugins.concat(plugin));
+
+	await authorizePlugin(plugin);
+
+	await launchPlugin(installId);
+}
+
+export async function uninstallPlugin(plugin: Plugin) {
+	abortPlugin(plugin);
+	prefer.commit('plugins', prefer.s.plugins.filter(x => x.installId !== plugin.installId));
+	if (Object.hasOwn(store.s.pluginTokens, plugin.installId)) {
+		await os.apiWithDialog('i/revoke-token', {
+			token: store.s.pluginTokens[plugin.installId],
+		});
+		const pluginTokens = { ...store.s.pluginTokens };
+		delete pluginTokens[plugin.installId];
+		store.set('pluginTokens', pluginTokens);
+	}
+}
+
+const pluginContexts = new Map<Plugin['installId'], Interpreter>();
+
+export const pluginLogs = ref(new Map<Plugin['installId'], {
+	at: number;
+	message: string;
+	isSystem?: boolean;
+	isError?: boolean;
+}[]>());
+
+type HandlerDef = {
+	post_form_action: {
+		title: string,
+		handler: <T>(form: T, update: (key: unknown, value: unknown) => void) => void;
+	};
+	user_action: {
+		title: string,
+		handler: (user: Misskey.entities.UserDetailed) => void;
+	};
+	note_action: {
+		title: string,
+		handler: (note: Misskey.entities.Note) => void;
+	};
+	note_view_interruptor: {
+		handler: (note: Misskey.entities.Note) => unknown;
+	};
+	note_post_interruptor: {
+		handler: (note: FIXME) => unknown;
+	};
+	page_view_interruptor: {
+		handler: (page: Misskey.entities.Page) => unknown;
+	};
+};
+
+type PluginHandler<K extends keyof HandlerDef> = {
+	pluginInstallId: string;
+	type: K;
+	ctx: HandlerDef[K];
+};
+
+let pluginHandlers: PluginHandler<keyof HandlerDef>[] = [];
+
+function addPluginHandler<K extends keyof HandlerDef>(installId: Plugin['installId'], type: K, ctx: PluginHandler<K>['ctx']) {
+	pluginLogs.value.get(installId)!.push({
+		at: Date.now(),
+		isSystem: true,
+		message: `Handler registered: ${type}`,
+	});
+	pluginHandlers.push({ pluginInstallId: installId, type, ctx });
+}
+
+export function launchPlugins() {
+	for (const plugin of prefer.s.plugins) {
+		if (plugin.active) {
+			launchPlugin(plugin.installId);
+		}
+	}
+}
+
+async function launchPlugin(id: Plugin['installId']): Promise<void> {
+	const plugin = prefer.s.plugins.find(x => x.installId === id);
+	if (!plugin) return;
+
 	// 後方互換性のため
 	if (plugin.src == null) return;
 
+	pluginLogs.value.set(plugin.installId, []);
+
+	function systemLog(message: string, isError = false): void {
+		pluginLogs.value.get(plugin.installId)?.push({
+			at: Date.now(),
+			isSystem: true,
+			message,
+			isError,
+		});
+	}
+
+	systemLog('Starting plugin...');
+
+	await authorizePlugin(plugin);
+
 	const aiscript = new Interpreter(createPluginEnv({
 		plugin: plugin,
-		storageKey: 'plugins:' + plugin.id,
+		storageKey: 'plugins:' + plugin.installId,
 	}), {
 		in: aiScriptReadline,
 		out: (value): void => {
-			console.log(value);
-			pluginLogs.value.get(plugin.id).push(utils.reprValue(value));
+			pluginLogs.value.get(plugin.installId)!.push({
+				at: Date.now(),
+				message: utils.reprValue(value),
+			});
 		},
 		log: (): void => {
 		},
 		err: (err): void => {
-			pluginLogs.value.get(plugin.id).push(`${err}`);
+			pluginLogs.value.get(plugin.installId)!.push({
+				at: Date.now(),
+				message: `${err}`,
+				isError: true,
+			});
 			throw err; // install時のtry-catchに反応させる
 		},
 	});
 
-	initPlugin({ plugin, aiscript });
+	pluginContexts.set(plugin.installId, aiscript);
 
 	aiscript.exec(parser.parse(plugin.src)).then(
 		() => {
 			console.info('Plugin installed:', plugin.name, 'v' + plugin.version);
+			systemLog('Plugin started');
 		},
 		(err) => {
 			console.error('Plugin install failed:', plugin.name, 'v' + plugin.version);
+			systemLog(`${err}`, true);
 			throw err;
 		},
 	);
 }
 
+export function abortPlugin(plugin: Plugin): void {
+	const pluginContext = pluginContexts.get(plugin.installId);
+	if (!pluginContext) return;
+
+	pluginContext.abort();
+	pluginContexts.delete(plugin.installId);
+	pluginLogs.value.delete(plugin.installId);
+	pluginHandlers = pluginHandlers.filter(x => x.pluginInstallId !== plugin.installId);
+}
+
+export function reloadPlugin(plugin: Plugin): void {
+	abortPlugin(plugin);
+	launchPlugin(plugin.installId);
+}
+
+export async function configPlugin(plugin: Plugin) {
+	if (plugin.config == null) {
+		throw new Error('This plugin does not have a config');
+	}
+
+	const config = plugin.config;
+	for (const key in plugin.configData) {
+		config[key].default = plugin.configData[key];
+	}
+
+	const { canceled, result } = await os.form(plugin.name, config);
+	if (canceled) return;
+
+	prefer.commit('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, configData: result } : x));
+
+	reloadPlugin(plugin);
+}
+
+export function changePluginActive(plugin: Plugin, active: boolean) {
+	prefer.commit('plugins', prefer.s.plugins.map(x => x.installId === plugin.installId ? { ...x, active } : x));
+
+	if (active) {
+		launchPlugin(plugin.installId);
+	} else {
+		abortPlugin(plugin);
+	}
+}
+
 function createPluginEnv(opts: { plugin: Plugin; storageKey: string }): Record<string, values.Value> {
+	const id = opts.plugin.installId;
+
 	const config = new Map<string, values.Value>();
 	for (const [k, v] of Object.entries(opts.plugin.config ?? {})) {
 		config.set(k, utils.jsToVal(typeof opts.plugin.configData[k] !== 'undefined' ? opts.plugin.configData[k] : v.default));
 	}
 
-	return {
-		...createAiScriptEnv({ ...opts, token: opts.plugin.token }),
-		//#region Deprecated
-		'Mk:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
+	function withContext<T>(fn: (ctx: Interpreter) => T): T {
+		const ctx = pluginContexts.get(id);
+		if (!ctx) throw new Error('Plugin context not found');
+		return fn(ctx);
+	}
+
+	const env: Record<string, values.Value> = {
+		...createAiScriptEnv({ ...opts, token: store.s.pluginTokens[id] }),
+
+		'Plugin:register:post_form_action': values.FN_NATIVE(([title, handler]) => {
 			utils.assertString(title);
-			registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler });
+			utils.assertFunction(handler);
+			addPluginHandler(id, 'post_form_action', {
+				title: title.value,
+				handler: withContext(ctx => (form, update) => {
+					ctx.execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
+						if (!key || !value) {
+							return;
+						}
+						update(utils.valToJs(key), utils.valToJs(value));
+					})]);
+				}),
+			});
 		}),
-		'Mk:register_user_action': values.FN_NATIVE(([title, handler]) => {
+
+		'Plugin:register:user_action': values.FN_NATIVE(([title, handler]) => {
 			utils.assertString(title);
-			registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler });
+			utils.assertFunction(handler);
+			addPluginHandler(id, 'user_action', {
+				title: title.value,
+				handler: withContext(ctx => (user) => {
+					ctx.execFn(handler, [utils.jsToVal(user)]);
+				}),
+			});
 		}),
-		'Mk:register_note_action': values.FN_NATIVE(([title, handler]) => {
+
+		'Plugin:register:note_action': values.FN_NATIVE(([title, handler]) => {
 			utils.assertString(title);
-			registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler });
+			utils.assertFunction(handler);
+			addPluginHandler(id, 'note_action', {
+				title: title.value,
+				handler: withContext(ctx => (note) => {
+					ctx.execFn(handler, [utils.jsToVal(note)]);
+				}),
+			});
 		}),
-		//#endregion
-		'Plugin:register_post_form_action': values.FN_NATIVE(([title, handler]) => {
-			utils.assertString(title);
-			registerPostFormAction({ pluginId: opts.plugin.id, title: title.value, handler });
+
+		'Plugin:register:note_view_interruptor': values.FN_NATIVE(([handler]) => {
+			utils.assertFunction(handler);
+			addPluginHandler(id, 'note_view_interruptor', {
+				handler: withContext(ctx => async (note) => {
+					return utils.valToJs(await ctx.execFn(handler, [utils.jsToVal(note)]));
+				}),
+			});
 		}),
-		'Plugin:register_user_action': values.FN_NATIVE(([title, handler]) => {
-			utils.assertString(title);
-			registerUserAction({ pluginId: opts.plugin.id, title: title.value, handler });
+
+		'Plugin:register:note_post_interruptor': values.FN_NATIVE(([handler]) => {
+			utils.assertFunction(handler);
+			addPluginHandler(id, 'note_post_interruptor', {
+				handler: withContext(ctx => async (note) => {
+					return utils.valToJs(await ctx.execFn(handler, [utils.jsToVal(note)]));
+				}),
+			});
 		}),
-		'Plugin:register_note_action': values.FN_NATIVE(([title, handler]) => {
-			utils.assertString(title);
-			registerNoteAction({ pluginId: opts.plugin.id, title: title.value, handler });
-		}),
-		'Plugin:register_note_view_interruptor': values.FN_NATIVE(([handler]) => {
-			registerNoteViewInterruptor({ pluginId: opts.plugin.id, handler });
-		}),
-		'Plugin:register_note_post_interruptor': values.FN_NATIVE(([handler]) => {
-			registerNotePostInterruptor({ pluginId: opts.plugin.id, handler });
-		}),
-		'Plugin:register_page_view_interruptor': values.FN_NATIVE(([handler]) => {
-			registerPageViewInterruptor({ pluginId: opts.plugin.id, handler });
+
+		'Plugin:register:page_view_interruptor': values.FN_NATIVE(([handler]) => {
+			utils.assertFunction(handler);
+			addPluginHandler(id, 'page_view_interruptor', {
+				handler: withContext(ctx => async (page) => {
+					return utils.valToJs(await ctx.execFn(handler, [utils.jsToVal(page)]));
+				}),
+			});
 		}),
+
 		'Plugin:open_url': values.FN_NATIVE(([url]) => {
 			utils.assertString(url);
 			window.open(url.value, '_blank', 'noopener');
 		}),
+
 		'Plugin:config': values.OBJ(config),
 	};
+
+	// 後方互換性のため
+	env['Plugin:register_post_form_action'] = env['Plugin:register:post_form_action'];
+	env['Plugin:register_user_action'] = env['Plugin:register:user_action'];
+	env['Plugin:register_note_action'] = env['Plugin:register:note_action'];
+	env['Plugin:register_note_view_interruptor'] = env['Plugin:register:note_view_interruptor'];
+	env['Plugin:register_note_post_interruptor'] = env['Plugin:register:note_post_interruptor'];
+	env['Plugin:register_page_view_interruptor'] = env['Plugin:register:page_view_interruptor'];
+
+	return env;
 }
 
-function initPlugin({ plugin, aiscript }): void {
-	pluginContexts.set(plugin.id, aiscript);
-	pluginLogs.value.set(plugin.id, []);
-}
-
-function registerPostFormAction({ pluginId, title, handler }): void {
-	postFormActions.push({
-		title, handler: (form, update) => {
-			const pluginContext = pluginContexts.get(pluginId);
-			if (!pluginContext) {
-				return;
-			}
-			pluginContext.execFn(handler, [utils.jsToVal(form), values.FN_NATIVE(([key, value]) => {
-				if (!key || !value) {
-					return;
-				}
-				update(utils.valToJs(key), utils.valToJs(value));
-			})]);
-		},
-	});
-}
-
-function registerUserAction({ pluginId, title, handler }): void {
-	userActions.push({
-		title, handler: (user) => {
-			const pluginContext = pluginContexts.get(pluginId);
-			if (!pluginContext) {
-				return;
-			}
-			pluginContext.execFn(handler, [utils.jsToVal(user)]);
-		},
-	});
-}
-
-function registerNoteAction({ pluginId, title, handler }): void {
-	noteActions.push({
-		title, handler: (note) => {
-			const pluginContext = pluginContexts.get(pluginId);
-			if (!pluginContext) {
-				return;
-			}
-			pluginContext.execFn(handler, [utils.jsToVal(note)]);
-		},
-	});
-}
-
-function registerNoteViewInterruptor({ pluginId, handler }): void {
-	noteViewInterruptors.push({
-		handler: async (note) => {
-			const pluginContext = pluginContexts.get(pluginId);
-			if (!pluginContext) {
-				return;
-			}
-			return utils.valToJs(await pluginContext.execFn(handler, [utils.jsToVal(note)]));
-		},
-	});
-}
-
-function registerNotePostInterruptor({ pluginId, handler }): void {
-	notePostInterruptors.push({
-		handler: async (note) => {
-			const pluginContext = pluginContexts.get(pluginId);
-			if (!pluginContext) {
-				return;
-			}
-			return utils.valToJs(await pluginContext.execFn(handler, [utils.jsToVal(note)]));
-		},
-	});
-}
-
-function registerPageViewInterruptor({ pluginId, handler }): void {
-	pageViewInterruptors.push({
-		handler: async (page) => {
-			const pluginContext = pluginContexts.get(pluginId);
-			if (!pluginContext) {
-				return;
-			}
-			return utils.valToJs(await pluginContext.execFn(handler, [utils.jsToVal(page)]));
-		},
-	});
+export function getPluginHandlers<K extends keyof HandlerDef>(type: K): HandlerDef[K][] {
+	return pluginHandlers.filter((x): x is PluginHandler<K> => x.type === type).map(x => x.ctx);
 }
diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts
new file mode 100644
index 0000000000..a38f1a2a33
--- /dev/null
+++ b/packages/frontend/src/preferences.ts
@@ -0,0 +1,81 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { v4 as uuid } from 'uuid';
+import type { PreferencesProfile } from '@/preferences/profile.js';
+import { cloudBackup } from '@/preferences/utility.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { ProfileManager } from '@/preferences/profile.js';
+import { store } from '@/store.js';
+import { $i } from '@/account.js';
+
+const TAB_ID = uuid();
+
+function createProfileManager() {
+	let profile: PreferencesProfile;
+
+	const savedProfileRaw = miLocalStorage.getItem('preferences');
+	if (savedProfileRaw == null) {
+		profile = ProfileManager.newProfile();
+		miLocalStorage.setItem('preferences', JSON.stringify(profile));
+	} else {
+		profile = ProfileManager.normalizeProfile(JSON.parse(savedProfileRaw));
+	}
+
+	return new ProfileManager(profile);
+}
+
+export const profileManager = createProfileManager();
+profileManager.addListener('updated', ({ profile: p }) => {
+	miLocalStorage.setItem('preferences', JSON.stringify(p));
+	miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`);
+});
+export const prefer = profileManager.store;
+
+let latestSyncedAt = Date.now();
+
+function syncBetweenTabs() {
+	const latest = miLocalStorage.getItem('latestPreferencesUpdate');
+	if (latest == null) return;
+
+	const latestTab = latest.split('/')[0];
+	const latestAt = parseInt(latest.split('/')[1]);
+
+	if (latestTab === TAB_ID) return;
+	if (latestAt <= latestSyncedAt) return;
+
+	profileManager.rewriteProfile(ProfileManager.normalizeProfile(JSON.parse(miLocalStorage.getItem('preferences')!)));
+
+	latestSyncedAt = Date.now();
+
+	if (_DEV_) console.log('prefer:synced');
+}
+
+window.setInterval(syncBetweenTabs, 5000);
+
+document.addEventListener('visibilitychange', () => {
+	if (document.visibilityState === 'visible') {
+		syncBetweenTabs();
+	}
+});
+
+let latestBackupAt = 0;
+
+window.setInterval(() => {
+	if ($i == null) return;
+	if (!store.s.enablePreferencesAutoCloudBackup) return;
+	if (document.visibilityState !== 'visible') return; // 同期されていない古い値がバックアップされるのを防ぐ
+	if (profileManager.profile.modifiedAt <= latestBackupAt) return;
+
+	cloudBackup().then(() => {
+		latestBackupAt = Date.now();
+	});
+}, 1000 * 60 * 3);
+
+if (_DEV_) {
+	(window as any).profileManager = profileManager;
+	(window as any).prefer = prefer;
+	(window as any).cloudBackup = cloudBackup;
+}
diff --git a/packages/frontend/src/preferences/def.ts b/packages/frontend/src/preferences/def.ts
new file mode 100644
index 0000000000..9ceea6b79b
--- /dev/null
+++ b/packages/frontend/src/preferences/def.ts
@@ -0,0 +1,321 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { hemisphere } from '@@/js/intl-const.js';
+import type { Theme } from '@/theme.js';
+import type { SoundType } from '@/utility/sound.js';
+import type { Plugin } from '@/plugin.js';
+import type { DeviceKind } from '@/utility/device-kind.js';
+import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
+
+/** サウンド設定 */
+export type SoundStore = {
+	type: Exclude<SoundType, '_driveFile_'>;
+	volume: number;
+} | {
+	type: '_driveFile_';
+
+	/** ドライブのファイルID */
+	fileId: string;
+
+	/** ファイルURL(こちらが優先される) */
+	fileUrl: string;
+
+	volume: number;
+};
+
+export const PREF_DEF = {
+	pinnedUserLists: {
+		accountDependent: true,
+		default: [] as Misskey.entities.UserList[],
+	},
+	uploadFolder: {
+		accountDependent: true,
+		default: null as string | null,
+	},
+	widgets: {
+		accountDependent: true,
+		default: [] as {
+			name: string;
+			id: string;
+			place: string | null;
+			data: Record<string, any>;
+		}[],
+	},
+
+	overridedDeviceKind: {
+		default: null as DeviceKind | null,
+	},
+	themes: {
+		default: [] as Theme[],
+	},
+	lightTheme: {
+		default: null as Theme | null,
+	},
+	darkTheme: {
+		default: null as Theme | null,
+	},
+	syncDeviceDarkMode: {
+		default: true,
+	},
+	defaultNoteVisibility: {
+		default: 'public' as (typeof Misskey.noteVisibilities)[number],
+	},
+	defaultNoteLocalOnly: {
+		default: false,
+	},
+	keepCw: {
+		default: true,
+	},
+	keepOriginalUploading: {
+		default: false,
+	},
+	rememberNoteVisibility: {
+		default: false,
+	},
+	reportError: {
+		default: false,
+	},
+	collapseRenotes: {
+		default: true,
+	},
+	menu: {
+		default: [
+			'notifications',
+			'clips',
+			'drive',
+			'followRequests',
+			'-',
+			'explore',
+			'announcements',
+			'search',
+			'-',
+			'ui',
+		],
+	},
+	statusbars: {
+		default: [] as {
+			name: string;
+			id: string;
+			type: string;
+			size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
+			black: boolean;
+			props: Record<string, any>;
+		}[],
+	},
+	serverDisconnectedBehavior: {
+		default: 'quiet' as 'quiet' | 'reload' | 'dialog',
+	},
+	nsfw: {
+		default: 'respect' as 'respect' | 'force' | 'ignore',
+	},
+	highlightSensitiveMedia: {
+		default: false,
+	},
+	animation: {
+		default: !window.matchMedia('(prefers-reduced-motion)').matches,
+	},
+	animatedMfm: {
+		default: !window.matchMedia('(prefers-reduced-motion)').matches,
+	},
+	advancedMfm: {
+		default: true,
+	},
+	showReactionsCount: {
+		default: false,
+	},
+	enableQuickAddMfmFunction: {
+		default: false,
+	},
+	loadRawImages: {
+		default: false,
+	},
+	imageNewTab: {
+		default: false,
+	},
+	disableShowingAnimatedImages: {
+		default: window.matchMedia('(prefers-reduced-motion)').matches,
+	},
+	emojiStyle: {
+		default: 'twemoji', // twemoji / fluentEmoji / native
+	},
+	menuStyle: {
+		default: 'auto' as 'auto' | 'popup' | 'drawer',
+	},
+	useBlurEffectForModal: {
+		default: DEFAULT_DEVICE_KIND === 'desktop',
+	},
+	useBlurEffect: {
+		default: DEFAULT_DEVICE_KIND === 'desktop',
+	},
+	showFixedPostForm: {
+		default: false,
+	},
+	showFixedPostFormInChannel: {
+		default: false,
+	},
+	enableInfiniteScroll: {
+		default: true,
+	},
+	useReactionPickerForContextMenu: {
+		default: false,
+	},
+	showGapBetweenNotesInTimeline: {
+		default: false,
+	},
+	instanceTicker: {
+		default: 'remote' as 'none' | 'remote' | 'always',
+	},
+	emojiPickerScale: {
+		default: 1,
+	},
+	emojiPickerWidth: {
+		default: 1,
+	},
+	emojiPickerHeight: {
+		default: 2,
+	},
+	emojiPickerStyle: {
+		default: 'auto' as 'auto' | 'popup' | 'drawer',
+	},
+	squareAvatars: {
+		default: false,
+	},
+	showAvatarDecorations: {
+		default: true,
+	},
+	numberOfPageCache: {
+		default: 3,
+	},
+	showNoteActionsOnlyHover: {
+		default: false,
+	},
+	showClipButtonInNoteFooter: {
+		default: false,
+	},
+	reactionsDisplaySize: {
+		default: 'medium' as 'small' | 'medium' | 'large',
+	},
+	limitWidthOfReaction: {
+		default: true,
+	},
+	forceShowAds: {
+		default: false,
+	},
+	aiChanMode: {
+		default: false,
+	},
+	devMode: {
+		default: false,
+	},
+	mediaListWithOneImageAppearance: {
+		default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3',
+	},
+	notificationPosition: {
+		default: 'rightBottom' as 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom',
+	},
+	notificationStackAxis: {
+		default: 'horizontal' as 'vertical' | 'horizontal',
+	},
+	enableCondensedLine: {
+		default: true,
+	},
+	keepScreenOn: {
+		default: false,
+	},
+	disableStreamingTimeline: {
+		default: false,
+	},
+	useGroupedNotifications: {
+		default: true,
+	},
+	dataSaver: {
+		default: {
+			media: false,
+			avatar: false,
+			urlPreview: false,
+			code: false,
+		} as Record<string, boolean>,
+	},
+	hemisphere: {
+		default: hemisphere as 'N' | 'S',
+	},
+	enableSeasonalScreenEffect: {
+		default: false,
+	},
+	enableHorizontalSwipe: {
+		default: true,
+	},
+	useNativeUiForVideoAudioPlayer: {
+		default: false,
+	},
+	keepOriginalFilename: {
+		default: true,
+	},
+	alwaysConfirmFollow: {
+		default: true,
+	},
+	confirmWhenRevealingSensitiveMedia: {
+		default: false,
+	},
+	contextMenu: {
+		default: 'app' as 'app' | 'appWithShift' | 'native',
+	},
+	skipNoteRender: {
+		default: true,
+	},
+	showSoftWordMutedWord: {
+		default: false,
+	},
+	confirmOnReact: {
+		default: false,
+	},
+	plugins: {
+		default: [] as Plugin[],
+	},
+	'sound.masterVolume': {
+		default: 0.3,
+	},
+	'sound.notUseSound': {
+		default: false,
+	},
+	'sound.useSoundOnlyWhenActive': {
+		default: false,
+	},
+	'sound.on.note': {
+		default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore,
+	},
+	'sound.on.noteMy': {
+		default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
+	},
+	'sound.on.notification': {
+		default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
+	},
+	'sound.on.reaction': {
+		default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
+	},
+	'deck.alwaysShowMainColumn': {
+		default: true,
+	},
+	'deck.navWindow': {
+		default: true,
+	},
+	'deck.useSimpleUiForNonRootPages': {
+		default: true,
+	},
+	'deck.columnAlign': {
+		default: 'left' as 'left' | 'right' | 'center',
+	},
+	'game.dropAndFusion': {
+		default: {
+			bgmVolume: 0.25,
+			sfxVolume: 1,
+		},
+	},
+} satisfies Record<string, {
+	default: any;
+	accountDependent?: boolean;
+}>;
diff --git a/packages/frontend/src/preferences/profile.ts b/packages/frontend/src/preferences/profile.ts
new file mode 100644
index 0000000000..c1320b0dcc
--- /dev/null
+++ b/packages/frontend/src/preferences/profile.ts
@@ -0,0 +1,236 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref, watch } from 'vue';
+import { v4 as uuid } from 'uuid';
+import { host, version } from '@@/js/config.js';
+import { EventEmitter } from 'eventemitter3';
+import { PREF_DEF } from './def.js';
+import { Store } from './store.js';
+import type { MenuItem } from '@/types/menu.js';
+import { $i } from '@/account.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { i18n } from '@/i18n.js';
+
+// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
+
+//type DottedToNested<T extends Record<string, any>> = {
+//	[K in keyof T as K extends string ? K extends `${infer A}.${infer B}` ? A : K : K]: K extends `${infer A}.${infer B}` ? DottedToNested<{ [key in B]: T[K] }> : T[K];
+//};
+
+type PREF = typeof PREF_DEF;
+type ValueOf<K extends keyof PREF> = PREF[K]['default'];
+type Account = string; // <host>/<userId>
+
+type Cond = {
+	server: string | null; // 将来のため
+	account: Account | null;
+	device: string | null; // 将来のため
+};
+
+export type PreferencesProfile = {
+	id: string;
+	version: string;
+	type: 'main';
+	modifiedAt: number;
+	name: string;
+	preferences: {
+		[K in keyof PREF]: [Cond, ValueOf<K>][];
+	};
+	syncByAccount: [Account, keyof PREF][],
+};
+
+export class ProfileManager extends EventEmitter<{
+	updated: (ctx: {
+		profile: PreferencesProfile
+	}) => void;
+}> {
+	public profile: PreferencesProfile;
+	public store: Store<{
+		[K in keyof PREF]: ValueOf<K>;
+	}>;
+
+	constructor(profile: PreferencesProfile) {
+		super();
+		this.profile = profile;
+
+		const states = this.genStates();
+
+		this.store = new Store(states);
+		this.store.addListener('updated', ({ key, value }) => {
+			console.log('prefer:set', key, value);
+
+			const record = this.getMatchedRecord(key);
+			if (record[0].account == null && PREF_DEF[key].accountDependent) {
+				this.profile.preferences[key].push([{
+					server: null,
+					account: `${host}/${$i!.id}`,
+					device: null,
+				}, value]);
+				this.save();
+				return;
+			}
+
+			record[1] = value;
+			this.save();
+		});
+	}
+
+	private genStates() {
+		const states = {} as { [K in keyof PREF]: ValueOf<K> };
+		let key: keyof PREF;
+		for (key in PREF_DEF) {
+			const record = this.getMatchedRecord(key);
+			states[key] = record[1];
+		}
+
+		return states;
+	}
+
+	public static newProfile(): PreferencesProfile {
+		const data = {} as PreferencesProfile['preferences'];
+		let key: keyof PREF;
+		for (key in PREF_DEF) {
+			data[key] = [[{
+				server: null,
+				account: null,
+				device: null,
+			}, PREF_DEF[key].default]];
+		}
+		return {
+			id: uuid(),
+			version: version,
+			type: 'main',
+			modifiedAt: Date.now(),
+			name: '',
+			preferences: data,
+			syncByAccount: [],
+		};
+	}
+
+	public static normalizeProfile(profile: any): PreferencesProfile {
+		const data = {} as PreferencesProfile['preferences'];
+		let key: keyof PREF;
+		for (key in PREF_DEF) {
+			const records = profile.preferences[key];
+			if (records == null || records.length === 0) {
+				data[key] = [[{
+					server: null,
+					account: null,
+					device: null,
+				}, PREF_DEF[key].default]];
+				continue;
+			} else {
+				data[key] = records;
+			}
+		}
+
+		return {
+			...profile,
+			preferences: data,
+		};
+	}
+
+	public save() {
+		this.profile.modifiedAt = Date.now();
+		this.profile.version = version;
+		this.emit('updated', { profile: this.profile });
+	}
+
+	public getMatchedRecord<K extends keyof PREF>(key: K): [Cond, ValueOf<K>] {
+		const records = this.profile.preferences[key];
+
+		if ($i == null) return records.find(([cond, v]) => cond.account == null)!;
+
+		const accountOverrideRecord = records.find(([cond, v]) => cond.account === `${host}/${$i!.id}`);
+		if (accountOverrideRecord) return accountOverrideRecord;
+
+		const record = records.find(([cond, v]) => cond.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]) => cond.account === `${host}/${$i!.id}`) ?? false;
+	}
+
+	public setAccountOverride<K extends keyof PREF>(key: K) {
+		if ($i == null) return;
+		if (PREF_DEF[key].accountDependent) throw new Error('already account-dependent');
+		if (this.isAccountOverrided(key)) return;
+
+		const records = this.profile.preferences[key];
+		records.push([{
+			server: null,
+			account: `${host}/${$i!.id}`,
+			device: null,
+		}, this.store.s[key]]);
+
+		this.save();
+	}
+
+	public clearAccountOverride<K extends keyof PREF>(key: K) {
+		if ($i == null) return;
+		if (PREF_DEF[key].accountDependent) throw new Error('cannot clear override for this account-dependent property');
+
+		const records = this.profile.preferences[key];
+
+		const index = records.findIndex(([cond, v]) => cond.account === `${host}/${$i!.id}`);
+		if (index === -1) return;
+
+		records.splice(index, 1);
+
+		this.store.rewrite(key, this.getMatchedRecord(key)[1]);
+
+		this.save();
+	}
+
+	public renameProfile(name: string) {
+		this.profile.name = name;
+		this.save();
+	}
+
+	public rewriteProfile(profile: PreferencesProfile) {
+		this.profile = profile;
+		const states = this.genStates();
+		for (const key in states) {
+			this.store.rewrite(key, states[key]);
+		}
+	}
+
+	public getPerPrefMenu<K extends keyof PREF>(key: K): MenuItem[] {
+		const overrideByAccount = ref(this.isAccountOverrided(key));
+
+		watch(overrideByAccount, () => {
+			if (overrideByAccount.value) {
+				this.setAccountOverride(key);
+			} else {
+				this.clearAccountOverride(key);
+			}
+		});
+
+		return [{
+			icon: 'ti ti-copy',
+			text: i18n.ts.copyPreferenceId,
+			action: () => {
+				copyToClipboard(key);
+			},
+		}, {
+			icon: 'ti ti-refresh',
+			text: i18n.ts.resetToDefaultValue,
+			danger: true,
+			action: () => {
+				this.store.commit(key, PREF_DEF[key].default);
+			},
+		}, {
+			type: 'divider',
+		}, {
+			type: 'switch',
+			icon: 'ti ti-user-cog',
+			text: i18n.ts.overrideByAccount,
+			ref: overrideByAccount,
+		}];
+	}
+}
diff --git a/packages/frontend/src/preferences/store.ts b/packages/frontend/src/preferences/store.ts
new file mode 100644
index 0000000000..e061021be3
--- /dev/null
+++ b/packages/frontend/src/preferences/store.ts
@@ -0,0 +1,94 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { computed, onUnmounted, ref, watch } from 'vue';
+import { EventEmitter } from 'eventemitter3';
+import type { Ref, WritableComputedRef } from 'vue';
+
+// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
+
+//type DottedToNested<T extends Record<string, any>> = {
+//	[K in keyof T as K extends string ? K extends `${infer A}.${infer B}` ? A : K : K]: K extends `${infer A}.${infer B}` ? DottedToNested<{ [key in B]: T[K] }> : T[K];
+//};
+
+type StoreEvent<Data extends Record<string, any>> = {
+	updated: <K extends keyof Data>(ctx: {
+		key: K;
+		value: Data[K];
+	}) => void;
+};
+
+export class Store<Data extends Record<string, any>> extends EventEmitter<StoreEvent<Data>> {
+	/**
+	 * static / state の略 (static が予約語のため)
+	 */
+	public s = {} as {
+		[K in keyof Data]: Data[K];
+	};
+
+	/**
+	 * reactive の略
+	 */
+	public r = {} as {
+		[K in keyof Data]: Ref<Data[K]>;
+	};
+
+	constructor(data: { [K in keyof Data]: Data[K] }) {
+		super();
+
+		for (const key in data) {
+			this.s[key] = data[key];
+			this.r[key] = ref(this.s[key]);
+		}
+	}
+
+	public commit<K extends keyof Data>(key: K, value: Data[K]) {
+		const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
+		this.r[key].value = this.s[key] = v;
+		this.emit('updated', { key, value: v });
+	}
+
+	public rewrite<K extends keyof Data>(key: K, value: Data[K]) {
+		const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
+		this.r[key].value = this.s[key] = v;
+	}
+
+	/**
+	 * 特定のキーの、簡易的なcomputed refを作ります
+	 * 主にvue上で設定コントロールのmodelとして使う用
+	 */
+	public model<K extends keyof Data, V extends Data[K] = Data[K]>(
+		key: K,
+		getter?: (v: Data[K]) => V,
+		setter?: (v: V) => Data[K],
+	): WritableComputedRef<V> {
+		const valueRef = ref(this.s[key]);
+
+		const stop = watch(this.r[key], val => {
+			valueRef.value = val;
+		});
+
+		// NOTE: vueコンポーネント内で呼ばれない限りは、onUnmounted は無意味なのでメモリリークする
+		onUnmounted(() => {
+			stop();
+		});
+
+		// TODO: VueのcustomRef使うと良い感じになるかも
+		return computed({
+			get: () => {
+				if (getter) {
+					return getter(valueRef.value);
+				} else {
+					return valueRef.value;
+				}
+			},
+			set: (value) => {
+				const val = setter ? setter(value) : value;
+				this.commit(key, val);
+				valueRef.value = val;
+			},
+		});
+	}
+}
diff --git a/packages/frontend/src/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts
new file mode 100644
index 0000000000..64b2bde4de
--- /dev/null
+++ b/packages/frontend/src/preferences/utility.ts
@@ -0,0 +1,226 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { ref, watch } from 'vue';
+import type { PreferencesProfile } from './profile.js';
+import type { MenuItem } from '@/types/menu.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { i18n } from '@/i18n.js';
+import { miLocalStorage } from '@/local-storage.js';
+import { prefer, profileManager } from '@/preferences.js';
+import * as os from '@/os.js';
+import { store } from '@/store.js';
+import { $i } from '@/account.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { unisonReload } from '@/utility/unison-reload.js';
+
+function canAutoBackup() {
+	return profileManager.profile.name != null && profileManager.profile.name.trim() !== '';
+}
+
+export function getPreferencesProfileMenu(): MenuItem[] {
+	const autoBackupEnabled = ref(store.s.enablePreferencesAutoCloudBackup);
+
+	watch(autoBackupEnabled, () => {
+		if (autoBackupEnabled.value) {
+			if (!canAutoBackup()) {
+				autoBackupEnabled.value = false;
+				os.alert({
+					type: 'warning',
+					title: i18n.ts._preferencesBackup.youNeedToNameYourProfileToEnableAutoBackup,
+				});
+				return;
+			}
+
+			store.set('enablePreferencesAutoCloudBackup', true);
+		} else {
+			store.set('enablePreferencesAutoCloudBackup', false);
+		}
+	});
+
+	const menu: MenuItem[] = [{
+		type: 'label',
+		text: profileManager.profile.name || `(${i18n.ts.noName})`,
+	}, {
+		text: i18n.ts.rename,
+		icon: 'ti ti-pencil',
+		action: () => {
+			renameProfile();
+		},
+	}, {
+		type: 'switch',
+		icon: 'ti ti-cloud-up',
+		text: i18n.ts._preferencesBackup.autoBackup,
+		ref: autoBackupEnabled,
+	}, {
+		text: i18n.ts.export,
+		icon: 'ti ti-download',
+		action: () => {
+			exportCurrentProfile();
+		},
+	}, {
+		type: 'divider',
+	}, {
+		text: i18n.ts._preferencesBackup.restoreFromBackup,
+		icon: 'ti ti-cloud-down',
+		action: () => {
+			restoreFromCloudBackup();
+		},
+	}, {
+		text: i18n.ts.import,
+		icon: 'ti ti-upload',
+		action: () => {
+			importProfile();
+		},
+	}];
+
+	if (prefer.s.devMode) {
+		menu.push({
+			type: 'divider',
+		}, {
+			text: 'Copy profile as text',
+			icon: 'ti ti-clipboard',
+			action: () => {
+				copyToClipboard(JSON.stringify(profileManager.profile, null, '\t'));
+			},
+		});
+	}
+
+	return menu;
+}
+
+async function renameProfile() {
+	const { canceled, result: name } = await os.inputText({
+		title: i18n.ts._preferencesProfile.profileName,
+		text: i18n.ts._preferencesProfile.profileNameDescription + '\n' + i18n.ts._preferencesProfile.profileNameDescription2,
+		placeholder: profileManager.profile.name || null,
+		default: profileManager.profile.name || null,
+	});
+	if (canceled || name == null || name.trim() === '') return;
+
+	profileManager.renameProfile(name);
+}
+
+function exportCurrentProfile() {
+	const p = profileManager.profile;
+	const txtBlob = new Blob([JSON.stringify(p)], { type: 'text/plain' });
+	const dummya = document.createElement('a');
+	dummya.href = URL.createObjectURL(txtBlob);
+	dummya.download = `${p.name || p.id}.misskeypreferences`;
+	dummya.click();
+}
+
+function importProfile() {
+	const input = document.createElement('input');
+	input.type = 'file';
+	input.accept = '.misskeypreferences';
+	input.onchange = async () => {
+		if (input.files == null || input.files.length === 0) return;
+
+		const file = input.files[0];
+		const txt = await file.text();
+		const profile = JSON.parse(txt) as PreferencesProfile;
+
+		miLocalStorage.setItem('preferences', JSON.stringify(profile));
+		miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
+		shouldSuggestRestoreBackup.value = false;
+		unisonReload();
+	};
+
+	input.click();
+}
+
+export async function cloudBackup() {
+	if ($i == null) return;
+	if (!canAutoBackup()) {
+		throw new Error('Profile name is not set');
+	}
+
+	await misskeyApi('i/registry/set', {
+		scope: ['client', 'preferences', 'backups'],
+		key: profileManager.profile.name,
+		value: profileManager.profile,
+	});
+}
+
+export async function restoreFromCloudBackup() {
+	if ($i == null) return;
+
+	// TODO: 更新日時でソートして取得したい
+	const keys = await misskeyApi('i/registry/keys', {
+		scope: ['client', 'preferences', 'backups'],
+	});
+
+	console.log(keys);
+
+	if (keys.length === 0) {
+		os.alert({
+			type: 'warning',
+			title: i18n.ts._preferencesBackup.noBackupsFoundTitle,
+			text: i18n.ts._preferencesBackup.noBackupsFoundDescription,
+		});
+		return;
+	}
+
+	const select = await os.select({
+		title: i18n.ts._preferencesBackup.selectBackupToRestore,
+		items: keys.map(k => ({
+			text: k,
+			value: k,
+		})),
+	});
+	if (select.canceled) return;
+	if (select.result == null) return;
+
+	const profile = await misskeyApi('i/registry/get', {
+		scope: ['client', 'preferences', 'backups'],
+		key: select.result,
+	});
+
+	console.log(profile);
+
+	miLocalStorage.setItem('preferences', JSON.stringify(profile));
+	miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
+	store.set('enablePreferencesAutoCloudBackup', true);
+	shouldSuggestRestoreBackup.value = false;
+	unisonReload();
+}
+
+export async function enableAutoBackup() {
+	if (!canAutoBackup()) {
+		await renameProfile();
+	}
+
+	if (!canAutoBackup()) {
+		return;
+	}
+
+	store.set('enablePreferencesAutoCloudBackup', true);
+}
+
+export const shouldSuggestRestoreBackup = ref(false);
+
+if ($i != null) {
+	if (new Date($i.createdAt).getTime() > (Date.now() - 1000 * 60 * 30)) { // アカウント作成直後は意味ないので除外
+		miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
+	} else {
+		if (miLocalStorage.getItem('hidePreferencesRestoreSuggestion') !== 'true') {
+			misskeyApi('i/registry/keys', {
+				scope: ['client', 'preferences', 'backups'],
+			}).then(keys => {
+				if (keys.length === 0) {
+					miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
+				} else {
+					shouldSuggestRestoreBackup.value = true;
+				}
+			});
+		}
+	}
+}
+
+export function hideRestoreBackupSuggestion() {
+	miLocalStorage.setItem('hidePreferencesRestoreSuggestion', 'true');
+	shouldSuggestRestoreBackup.value = false;
+}
diff --git a/packages/frontend/src/router/definition.ts b/packages/frontend/src/router/definition.ts
index 41f8d05f42..56d0593594 100644
--- a/packages/frontend/src/router/definition.ts
+++ b/packages/frontend/src/router/definition.ts
@@ -169,10 +169,6 @@ const routes: RouteDef[] = [{
 		path: '/deck',
 		name: 'deck',
 		component: page(() => import('@/pages/settings/deck.vue')),
-	}, {
-		path: '/preferences-backups',
-		name: 'preferences-backups',
-		component: page(() => import('@/pages/settings/preferences-backups.vue')),
 	}, {
 		path: '/custom-css',
 		name: 'preferences',
diff --git a/packages/frontend/src/router/supplier.ts b/packages/frontend/src/router/supplier.ts
index 87f8829854..191dd49ebb 100644
--- a/packages/frontend/src/router/supplier.ts
+++ b/packages/frontend/src/router/supplier.ts
@@ -4,16 +4,17 @@
  */
 
 import { inject } from 'vue';
-import { Router } from '@/nirax.js';
 import type { IRouter } from '@/nirax.js';
+import { Router } from '@/nirax.js';
 import { mainRouter } from '@/router/main.js';
+import { DI } from '@/di.js';
 
 /**
  * メインの{@link Router}を取得する。
  * あらかじめ{@link setupRouter}を実行しておく必要がある({@link provide}により{@link IRouter}のインスタンスを注入可能であるならばこの限りではない)
  */
 export function useRouter(): IRouter {
-	return inject<Router | null>('router', null) ?? mainRouter;
+	return inject<Router | null>(DI.router, null) ?? mainRouter;
 }
 
 /**
diff --git a/packages/frontend/src/scripts/autospacing.ts b/packages/frontend/src/scripts/autospacing.ts
deleted file mode 100644
index 528d596e33..0000000000
--- a/packages/frontend/src/scripts/autospacing.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import * as misskey from 'misskey-js';
-import { defaultStore } from '@/store.js';
-
-const NO_SPACEING_LIST = [
-    'A股',
-    'B股',
-    'H股',
-    'SIM卡',
-    'PC端',
-    'T恤',
-    'A站',
-    'B站',
-    'C站',
-    'N卡',
-    'A卡',
-    'UP主',
-    'X光',
-    'B超',
-    'Q弹',
-];
-
-const LIST_WINDOW = NO_SPACEING_LIST.reduce((a, b) => Math.max(a, b.length), 0) + 1;
-
-const hashtagMap = new Map<string, string>();
-let placeholderCounter = 0;
-
-function preserveHashtags(text: string): string {
-    placeholderCounter = 0;
-    hashtagMap.clear();
-
-    return text.replace(/#[^\s]+/g, (match) => {
-        const placeholder = `__HASHTAG_${placeholderCounter}__`;
-        hashtagMap.set(placeholder, match);
-        placeholderCounter++;
-        return placeholder;
-    });
-}
-
-function restoreHashtags(text: string): string {
-    let result = text;
-    for (const [placeholder, hashtag] of hashtagMap) {
-        result = result.replace(placeholder, hashtag);
-    }
-    return result;
-}
-
-export function autoSpacing(plainText: string) {
-    if (defaultStore.reactiveState.autoSpacingBehaviour.value == null) return plainText;
-
-    const textWithPlaceholders = preserveHashtags(plainText);
-
-    const rep = (matched: string, c1: string, c2: string, position: number) => {
-        if (defaultStore.reactiveState.autoSpacingBehaviour.value === 'all') return `${c1} ${c2}`;
-        const context = plainText
-            .slice(Math.max(0, position - LIST_WINDOW), position + LIST_WINDOW)
-            .toUpperCase();
-        if (NO_SPACEING_LIST.some((text) => context.includes(text))) {
-            return matched;
-        } else {
-            return `${c1} ${c2}`;
-        }
-    };
-
-    const spacedText = textWithPlaceholders
-        .replace(/([\u4e00-\u9fa5\u0800-\u4e00\uac00-\ud7ff])([a-zA-Z0-9])/g, rep)
-        .replace(/([a-zA-Z0-9,\.:])([\u4e00-\u9fa5\u0800-\u4e00\uac00-\ud7ff])/g, rep);
-
-    return restoreHashtags(spacedText);
-}
-
-export function spacingNote(note: misskey.entities.Note) {
-    const noteAsRecord = note as unknown as Record<string, string | null | undefined>;
-    if (!noteAsRecord.__autospacing_raw_text) {
-        noteAsRecord.__autospacing_raw_text = note.text;
-    }
-    if (!noteAsRecord.__autospacing_raw_cw) {
-        noteAsRecord.__autospacing_raw_cw = note.cw;
-    }
-    note.text = noteAsRecord.__autospacing_raw_text
-        ? autoSpacing(noteAsRecord.__autospacing_raw_text)
-        : null;
-    note.cw = noteAsRecord.__autospacing_raw_cw
-        ? autoSpacing(noteAsRecord.__autospacing_raw_cw)
-        : null;
-    return note;
-}
diff --git a/packages/frontend/src/scripts/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts
deleted file mode 100644
index 37f473b6de..0000000000
--- a/packages/frontend/src/scripts/install-plugin.ts
+++ /dev/null
@@ -1,131 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import { defineAsyncComponent } from 'vue';
-import { compareVersions } from 'compare-versions';
-import { v4 as uuid } from 'uuid';
-import { Interpreter, Parser, utils } from '@syuilo/aiscript';
-import type { Plugin } from '@/store.js';
-import { ColdDeviceStorage } from '@/store.js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { i18n } from '@/i18n.js';
-
-export type AiScriptPluginMeta = {
-	name: string;
-	version: string;
-	author: string;
-	description?: string;
-	permissions?: string[];
-	config?: Record<string, any>;
-};
-
-const parser = new Parser();
-
-export function savePlugin({ id, meta, src, token }: {
-	id: string;
-	meta: AiScriptPluginMeta;
-	src: string;
-	token: string;
-}) {
-	ColdDeviceStorage.set('plugins', ColdDeviceStorage.get('plugins').concat({
-		...meta,
-		id,
-		active: true,
-		configData: {},
-		token: token,
-		src: src,
-	} as Plugin));
-}
-
-export function isSupportedAiScriptVersion(version: string): boolean {
-	try {
-		return (compareVersions(version, '0.12.0') >= 0);
-	} catch (err) {
-		return false;
-	}
-}
-
-export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta> {
-	if (!code) {
-		throw new Error('code is required');
-	}
-
-	const lv = utils.getLangVersion(code);
-	if (lv == null) {
-		throw new Error('No language version annotation found');
-	} else if (!isSupportedAiScriptVersion(lv)) {
-		throw new Error(`Aiscript version '${lv}' is not supported`);
-	}
-
-	let ast;
-	try {
-		ast = parser.parse(code);
-	} catch (err) {
-		throw new Error('Aiscript syntax error');
-	}
-
-	const meta = Interpreter.collectMetadata(ast);
-	if (meta == null) {
-		throw new Error('Meta block not found');
-	}
-
-	const metadata = meta.get(null);
-	if (metadata == null) {
-		throw new Error('Metadata not found');
-	}
-
-	const { name, version, author, description, permissions, config } = metadata;
-	if (name == null || version == null || author == null) {
-		throw new Error('Required property not found');
-	}
-
-	return {
-		name,
-		version,
-		author,
-		description,
-		permissions,
-		config,
-	};
-}
-
-export async function installPlugin(code: string, meta?: AiScriptPluginMeta) {
-	if (!code) return;
-
-	let realMeta: AiScriptPluginMeta;
-	if (!meta) {
-		realMeta = await parsePluginMeta(code);
-	} else {
-		realMeta = meta;
-	}
-
-	const token = realMeta.permissions == null || realMeta.permissions.length === 0 ? null : await new Promise((res, rej) => {
-		const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkTokenGenerateWindow.vue')), {
-			title: i18n.ts.tokenRequested,
-			information: i18n.ts.pluginTokenRequestedDescription,
-			initialName: realMeta.name,
-			initialPermissions: realMeta.permissions,
-		}, {
-			done: async result => {
-				const { name, permissions } = result;
-				const { token } = await misskeyApi('miauth/gen-token', {
-					session: null,
-					name: name,
-					permission: permissions,
-				});
-				res(token);
-			},
-			closed: () => dispose(),
-		});
-	});
-
-	savePlugin({
-		id: uuid(),
-		meta: realMeta,
-		token,
-		src: code,
-	});
-}
diff --git a/packages/frontend/src/scripts/install-theme.ts b/packages/frontend/src/scripts/install-theme.ts
deleted file mode 100644
index cc32adcc6a..0000000000
--- a/packages/frontend/src/scripts/install-theme.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-import JSON5 from 'json5';
-import { addTheme, getThemes } from '@/theme-store.js';
-import { applyTheme, validateTheme } from '@/scripts/theme.js';
-import type { Theme } from '@/scripts/theme.js';
-
-export function parseThemeCode(code: string): Theme {
-	let theme;
-
-	try {
-		theme = JSON5.parse(code);
-	} catch (err) {
-		throw new Error('Failed to parse theme json');
-	}
-	if (!validateTheme(theme)) {
-		throw new Error('This theme is invaild');
-	}
-	if (getThemes().some(t => t.id === theme.id)) {
-		throw new Error('This theme is already installed');
-	}
-
-	return theme;
-}
-
-export function previewTheme(code: string): void {
-	const theme = parseThemeCode(code);
-	if (theme) applyTheme(theme, false);
-}
-
-export async function installTheme(code: string): Promise<void> {
-	const theme = parseThemeCode(code);
-	if (!theme) return;
-	await addTheme(theme);
-}
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 71ead26d56..611e64a809 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -5,69 +5,20 @@
 
 import { markRaw, ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import { hemisphere } from '@@/js/intl-const.js';
 import lightTheme from '@@/themes/p-light2.json5';
 import darkTheme from '@@/themes/p-dark2.json5';
-import type { SoundType } from '@/scripts/sound.js';
-import type { Ast } from '@syuilo/aiscript';
-import type { DeviceKind } from '@/scripts/device-kind.js';
-import { DEFAULT_DEVICE_KIND } from '@/scripts/device-kind.js';
+import { hemisphere } from '@@/js/intl-const.js';
+import type { DeviceKind } from '@/utility/device-kind.js';
+import type { Plugin } from '@/plugin.js';
+import type { Column } from '@/deck.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { Storage } from '@/pizzax.js';
+import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
 
-interface PostFormAction {
-	title: string,
-	handler: <T>(form: T, update: (key: unknown, value: unknown) => void) => void;
-}
-
-interface UserAction {
-	title: string,
-	handler: (user: Misskey.entities.UserDetailed) => void;
-}
-
-interface NoteAction {
-	title: string,
-	handler: (note: Misskey.entities.Note) => void;
-}
-
-interface NoteViewInterruptor {
-	handler: (note: Misskey.entities.Note) => unknown;
-}
-
-interface NotePostInterruptor {
-	handler: (note: FIXME) => unknown;
-}
-
-interface PageViewInterruptor {
-	handler: (page: Misskey.entities.Page) => unknown;
-}
-
-/** サウンド設定 */
-export type SoundStore = {
-	type: Exclude<SoundType, '_driveFile_'>;
-	volume: number;
-} | {
-	type: '_driveFile_';
-
-	/** ドライブのファイルID */
-	fileId: string;
-
-	/** ファイルURL(こちらが優先される) */
-	fileUrl: string;
-
-	volume: number;
-};
-
-export const postFormActions: PostFormAction[] = [];
-export const userActions: UserAction[] = [];
-export const noteActions: NoteAction[] = [];
-export const noteViewInterruptors: NoteViewInterruptor[] = [];
-export const notePostInterruptors: NotePostInterruptor[] = [];
-export const pageViewInterruptors: PageViewInterruptor[] = [];
-
-// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
-//       あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
-export const defaultStore = markRaw(new Storage('base', {
+/**
+ * 「状態」を管理するストア(not「設定」)
+ */
+export const store = markRaw(new Storage('base', {
 	accountSetupWizard: {
 		where: 'account',
 		default: 0,
@@ -85,38 +36,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'account',
 		default: false,
 	},
-	keepCw: {
-		where: 'account',
-		default: true,
-	},
-	collapseRenotes: {
-		where: 'account',
-		default: true,
-	},
-	rememberNoteVisibility: {
-		where: 'account',
-		default: false,
-	},
-	defaultNoteVisibility: {
-		where: 'account',
-		default: 'public' as (typeof Misskey.noteVisibilities)[number],
-	},
-	defaultNoteLocalOnly: {
-		where: 'account',
-		default: false,
-	},
-	uploadFolder: {
-		where: 'account',
-		default: null as string | null,
-	},
-	pastedFileName: {
-		where: 'account',
-		default: 'yyyy-MM-dd HH-mm-ss [{{number}}]',
-	},
-	keepOriginalUploading: {
-		where: 'account',
-		default: false,
-	},
 	memo: {
 		where: 'account',
 		default: null,
@@ -141,22 +60,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'account',
 		default: [] as string[],
 	},
-
-	menu: {
-		where: 'deviceAccount',
-		default: [
-			'notifications',
-			'clips',
-			'drive',
-			'followRequests',
-			'-',
-			'explore',
-			'announcements',
-			'search',
-			'-',
-			'ui',
-		],
-	},
 	visibility: {
 		where: 'deviceAccount',
 		default: 'public' as (typeof Misskey.noteVisibilities)[number],
@@ -169,26 +72,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: false,
 	},
-	statusbars: {
-		where: 'deviceAccount',
-		default: [] as {
-			name: string;
-			id: string;
-			type: string;
-			size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
-			black: boolean;
-			props: Record<string, any>;
-		}[],
-	},
-	widgets: {
-		where: 'account',
-		default: [] as {
-			name: string;
-			id: string;
-			place: string | null;
-			data: Record<string, any>;
-		}[],
-	},
 	tl: {
 		where: 'deviceAccount',
 		default: {
@@ -202,15 +85,129 @@ export const defaultStore = markRaw(new Storage('base', {
 			},
 		},
 	},
-	pinnedUserLists: {
+	darkMode: {
+		where: 'device',
+		default: false,
+	},
+	recentlyUsedEmojis: {
+		where: 'device',
+		default: [] as string[],
+	},
+	recentlyUsedUsers: {
+		where: 'device',
+		default: [] as string[],
+	},
+	menuDisplay: {
+		where: 'device',
+		default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top',
+	},
+	postFormWithHashtags: {
+		where: 'device',
+		default: false,
+	},
+	postFormHashtags: {
+		where: 'device',
+		default: '',
+	},
+	additionalUnicodeEmojiIndexes: {
+		where: 'device',
+		default: {} as Record<string, Record<string, string[]>>,
+	},
+	defaultWithReplies: {
+		where: 'account',
+		default: false,
+	},
+	pluginTokens: {
 		where: 'deviceAccount',
-		default: [] as Misskey.entities.UserList[],
+		default: {} as Record<string, string>, // plugin id, token
+	},
+	'deck.profile': {
+		where: 'deviceAccount',
+		default: 'default',
+	},
+	'deck.columns': {
+		where: 'deviceAccount',
+		default: [] as Column[],
+	},
+	'deck.layout': {
+		where: 'deviceAccount',
+		default: [] as Column['id'][][],
 	},
 
+	enablePreferencesAutoCloudBackup: {
+		where: 'device',
+		default: false,
+	},
+	showPreferencesAutoCloudBackupSuggestion: {
+		where: 'device',
+		default: true,
+	},
+
+	//#region TODO: そのうち消す (preferに移行済み)
+	widgets: {
+		where: 'account',
+		default: [] as {
+			name: string;
+			id: string;
+			place: string | null;
+			data: Record<string, any>;
+		}[],
+	},
 	overridedDeviceKind: {
 		where: 'device',
 		default: null as DeviceKind | null,
 	},
+	defaultSideView: {
+		where: 'device',
+		default: false,
+	},
+	defaultNoteVisibility: {
+		where: 'account',
+		default: 'public' as (typeof Misskey.noteVisibilities)[number],
+	},
+	defaultNoteLocalOnly: {
+		where: 'account',
+		default: false,
+	},
+	keepCw: {
+		where: 'account',
+		default: true,
+	},
+	collapseRenotes: {
+		where: 'account',
+		default: true,
+	},
+	rememberNoteVisibility: {
+		where: 'account',
+		default: false,
+	},
+	uploadFolder: {
+		where: 'account',
+		default: null as string | null,
+	},
+	keepOriginalUploading: {
+		where: 'account',
+		default: false,
+	},
+	menu: {
+		where: 'deviceAccount',
+		default: [],
+	},
+	statusbars: {
+		where: 'deviceAccount',
+		default: [] as {
+			name: string;
+			id: string;
+			type: string;
+			size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
+			black: boolean;
+			props: Record<string, any>;
+		}[],
+	},
+	pinnedUserLists: {
+		where: 'deviceAccount',
+		default: [] as Misskey.entities.UserList[],
+	},
 	serverDisconnectedBehavior: {
 		where: 'device',
 		default: 'null' as 'null' | 'quiet' | 'reload' | 'dialog',
@@ -291,10 +288,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: true,
 	},
-	darkMode: {
-		where: 'device',
-		default: false,
-	},
 	instanceTicker: {
 		where: 'device',
 		default: 'remote' as 'none' | 'remote' | 'always',
@@ -315,22 +308,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: 'auto' as 'auto' | 'popup' | 'drawer',
 	},
-	recentlyUsedEmojis: {
-		where: 'device',
-		default: [] as string[],
-	},
-	recentlyUsedUsers: {
-		where: 'device',
-		default: [] as string[],
-	},
-	defaultSideView: {
-		where: 'device',
-		default: false,
-	},
-	menuDisplay: {
-		where: 'device',
-		default: 'sideFull' as 'sideFull' | 'sideIcon' | 'top',
-	},
 	reportError: {
 		where: 'device',
 		default: false,
@@ -343,18 +320,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: true,
 	},
-	postFormWithHashtags: {
-		where: 'device',
-		default: false,
-	},
-	postFormHashtags: {
-		where: 'device',
-		default: '',
-	},
-	themeInitial: {
-		where: 'device',
-		default: true,
-	},
 	numberOfPageCache: {
 		where: 'device',
 		default: 3,
@@ -407,18 +372,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: false,
 	},
-	additionalUnicodeEmojiIndexes: {
-		where: 'device',
-		default: {} as Record<string, Record<string, string[]>>,
-	},
 	keepScreenOn: {
 		where: 'device',
 		default: false,
 	},
-	defaultWithReplies: {
-		where: 'account',
-		default: true,
-	},
 	disableStreamingTimeline: {
 		where: 'device',
 		default: false,
@@ -440,17 +397,6 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: false,
 	},
-	dropAndFusion: {
-		where: 'device',
-		default: {
-			bgmVolume: 0.25,
-			sfxVolume: 1,
-		},
-	},
-	hemisphere: {
-		where: 'device',
-		default: hemisphere as 'N' | 'S',
-	},
 	enableHorizontalSwipe: {
 		where: 'device',
 		default: true,
@@ -487,7 +433,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: false,
 	},
-
+	hemisphere: {
+		where: 'device',
+		default: hemisphere as 'N' | 'S',
+	},
 	sound_masterVolume: {
 		where: 'device',
 		default: 0.3,
@@ -502,19 +451,19 @@ export const defaultStore = markRaw(new Storage('base', {
 	},
 	sound_note: {
 		where: 'device',
-		default: { type: 'syuilo/n-aec', volume: 1 } as SoundStore,
+		default: { type: 'syuilo/n-aec', volume: 1 },
 	},
 	sound_noteMy: {
 		where: 'device',
-		default: { type: 'syuilo/n-cea-4va', volume: 1 } as SoundStore,
+		default: { type: 'syuilo/n-cea-4va', volume: 1 },
 	},
 	sound_notification: {
 		where: 'device',
-		default: { type: 'syuilo/n-ea', volume: 1 } as SoundStore,
+		default: { type: 'syuilo/n-ea', volume: 1 },
 	},
 	sound_reaction: {
 		where: 'device',
-		default: { type: 'syuilo/bubble1', volume: 1 } as SoundStore,
+		default: { type: 'syuilo/bubble2', volume: 1 },
 	},
 	enableRenderingOptimization: {
 		where: 'device',
@@ -568,42 +517,35 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: true,
 	},
+	dropAndFusion: {
+		where: 'device',
+		default: {
+			bgmVolume: 0.25,
+			sfxVolume: 1,
+		},
+	},
+	//#endregion
 }));
 
 // TODO: 他のタブと永続化されたstateを同期
 
 const PREFIX = 'miux:' as const;
 
-export type Plugin = {
-	id: string;
-	name: string;
-	active: boolean;
-	config?: Record<string, { default: any }>;
-	configData: Record<string, any>;
-	token: string;
-	src: string | null;
-	version: string;
-	ast: Ast.Node[];
-	author?: string;
-	description?: string;
-	permissions?: string[];
-};
-
 interface Watcher {
 	key: string;
 	callback: (value: unknown) => void;
 }
 
+// TODO: 消す(preferに移行済みのため)
 /**
  * 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
  */
-
 export class ColdDeviceStorage {
 	public static default = {
-		lightTheme,
-		darkTheme,
-		syncDeviceDarkMode: true,
-		plugins: [] as Plugin[],
+		lightTheme, // TODO: 消す(preferに移行済みのため)
+		darkTheme, // TODO: 消す(preferに移行済みのため)
+		syncDeviceDarkMode: true, // TODO: 消す(preferに移行済みのため)
+		plugins: [] as Plugin[], // TODO: 消す(preferに移行済みのため)
 	};
 
 	public static watchers: Watcher[] = [];
diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index 687ccb948d..e7367e45d8 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -8,7 +8,7 @@ import { markRaw } from 'vue';
 import { $i } from '@/account.js';
 import { wsOrigin } from '@@/js/config.js';
 // TODO: No WebsocketモードでStreamMockが使えそう
-//import { StreamMock } from '@/scripts/stream-mock.js';
+//import { StreamMock } from '@/utility/stream-mock.js';
 
 // heart beat interval in ms
 const HEART_BEAT_INTERVAL = 1000 * 30;
diff --git a/packages/frontend/src/theme-store.ts b/packages/frontend/src/theme-store.ts
index fb010ae426..5d09ec27f9 100644
--- a/packages/frontend/src/theme-store.ts
+++ b/packages/frontend/src/theme-store.ts
@@ -3,29 +3,14 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { getBuiltinThemes } from '@/scripts/theme.js';
-import type { Theme } from '@/scripts/theme.js';
-import { miLocalStorage } from '@/local-storage.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import type { Theme } from '@/theme.js';
+import { getBuiltinThemes } from '@/theme.js';
 import { $i } from '@/account.js';
-
-const lsCacheKey = $i ? `themes:${$i.id}` as const : null;
+import { prefer } from '@/preferences.js';
 
 export function getThemes(): Theme[] {
 	if ($i == null) return [];
-	return JSON.parse(miLocalStorage.getItem(lsCacheKey!) ?? '[]');
-}
-
-export async function fetchThemes(): Promise<void> {
-	if ($i == null) return;
-
-	try {
-		const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' });
-		miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
-	} catch (err) {
-		if (err.code === 'NO_SUCH_KEY') return;
-		throw err;
-	}
+	return prefer.s.themes;
 }
 
 export async function addTheme(theme: Theme): Promise<void> {
@@ -34,15 +19,15 @@ export async function addTheme(theme: Theme): Promise<void> {
 	if (builtinThemes.some(t => t.id === theme.id)) {
 		throw new Error('builtin theme');
 	}
-	await fetchThemes();
-	const themes = getThemes().concat(theme);
-	await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
-	miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
+	const themes = getThemes();
+	if (themes.some(t => t.id === theme.id)) {
+		throw new Error('already exists');
+	}
+	prefer.commit('themes', [...themes, theme]);
 }
 
 export async function removeTheme(theme: Theme): Promise<void> {
 	if ($i == null) return;
 	const themes = getThemes().filter(t => t.id !== theme.id);
-	await misskeyApi('i/registry/set', { scope: ['client'], key: 'themes', value: themes });
-	miLocalStorage.setItem(lsCacheKey!, JSON.stringify(themes));
+	prefer.commit('themes', themes);
 }
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/theme.ts
similarity index 80%
rename from packages/frontend/src/scripts/theme.ts
rename to packages/frontend/src/theme.ts
index 695d256621..4ded1ff668 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/theme.ts
@@ -7,10 +7,12 @@ import { ref } from 'vue';
 import tinycolor from 'tinycolor2';
 import lightTheme from '@@/themes/_light.json5';
 import darkTheme from '@@/themes/_dark.json5';
-import { deepClone } from './clone.js';
+import JSON5 from 'json5';
 import type { BundledTheme } from 'shiki/themes';
+import { deepClone } from '@/utility/clone.js';
 import { globalEvents } from '@/events.js';
 import { miLocalStorage } from '@/local-storage.js';
+import { addTheme, getThemes } from '@/theme-store.js';
 
 export type Theme = {
 	id: string;
@@ -78,6 +80,9 @@ export function applyTheme(theme: Theme, persist = true) {
 
 	timeout = window.setTimeout(() => {
 		document.documentElement.classList.remove('_themeChanging_');
+
+		// 色計算など再度行えるようにクライアント全体に通知
+		globalEvents.emit('themeChanged');
 	}, 1000);
 
 	const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
@@ -109,14 +114,15 @@ export function applyTheme(theme: Theme, persist = true) {
 
 	if (persist) {
 		miLocalStorage.setItem('theme', JSON.stringify(props));
+		miLocalStorage.setItem('themeId', theme.id);
 		miLocalStorage.setItem('colorScheme', colorScheme);
 	}
 
 	// 色計算など再度行えるようにクライアント全体に通知
-	globalEvents.emit('themeChanged');
+	globalEvents.emit('themeChanging');
 }
 
-function compile(theme: Theme): Record<string, string> {
+export function compile(theme: Theme): Record<string, string> {
 	function getColor(val: string): tinycolor.Instance {
 		if (val[0] === '@') { // ref (prop)
 			return getColor(theme.props[val.substring(1)]);
@@ -163,3 +169,32 @@ export function validateTheme(theme: Record<string, any>): boolean {
 	if (theme.props == null || typeof theme.props !== 'object') return false;
 	return true;
 }
+
+export function parseThemeCode(code: string): Theme {
+	let theme;
+
+	try {
+		theme = JSON5.parse(code);
+	} catch (err) {
+		throw new Error('Failed to parse theme json');
+	}
+	if (!validateTheme(theme)) {
+		throw new Error('This theme is invaild');
+	}
+	if (getThemes().some(t => t.id === theme.id)) {
+		throw new Error('This theme is already installed');
+	}
+
+	return theme;
+}
+
+export function previewTheme(code: string): void {
+	const theme = parseThemeCode(code);
+	if (theme) applyTheme(theme, false);
+}
+
+export async function installTheme(code: string): Promise<void> {
+	const theme = parseThemeCode(code);
+	if (!theme) return;
+	await addTheme(theme);
+}
diff --git a/packages/frontend/src/ui/_common_/PreferenceRestore.vue b/packages/frontend/src/ui/_common_/PreferenceRestore.vue
new file mode 100644
index 0000000000..c70b82cd0e
--- /dev/null
+++ b/packages/frontend/src/ui/_common_/PreferenceRestore.vue
@@ -0,0 +1,64 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.root">
+	<span :class="$style.icon">
+		<i class="ti ti-info-circle"></i>
+	</span>
+	<span :class="$style.title">{{ i18n.ts._preferencesBackup.backupFound }}</span>
+	<span :class="$style.body"><button class="_textButton" @click="restore">{{ i18n.ts.restore }}</button> | <button class="_textButton" @click="skip">{{ i18n.ts.skip }}</button></span>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { $i } from '@/account.js';
+import { i18n } from '@/i18n.js';
+import { hideRestoreBackupSuggestion, restoreFromCloudBackup } from '@/preferences/utility.js';
+
+function restore() {
+	restoreFromCloudBackup();
+}
+
+function skip() {
+	hideRestoreBackupSuggestion();
+}
+</script>
+
+<style lang="scss" module>
+.root {
+	--height: 24px;
+	font-size: 0.85em;
+	display: flex;
+	vertical-align: bottom;
+	width: 100%;
+	line-height: var(--height);
+	height: var(--height);
+	overflow: clip;
+	contain: strict;
+	background: var(--MI_THEME-panel);
+}
+
+.icon {
+	margin-left: 10px;
+}
+
+.title {
+	padding: 0 10px;
+	font-weight: bold;
+
+	&:empty {
+		display: none;
+	}
+}
+
+.body {
+	min-width: 0;
+	flex: 1;
+	overflow: clip;
+	white-space: nowrap;
+	text-overflow: ellipsis;
+}
+</style>
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index 2afad11939..4a700d10b4 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -17,19 +17,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 <TransitionGroup
 	tag="div"
 	:class="[$style.notifications, {
-		[$style.notificationsPosition_leftTop]: defaultStore.state.notificationPosition === 'leftTop',
-		[$style.notificationsPosition_leftBottom]: defaultStore.state.notificationPosition === 'leftBottom',
-		[$style.notificationsPosition_rightTop]: defaultStore.state.notificationPosition === 'rightTop',
-		[$style.notificationsPosition_rightBottom]: defaultStore.state.notificationPosition === 'rightBottom',
-		[$style.notificationsPosition_close]: defaultStore.state.notificationPosition === 'close',
-		[$style.notificationsStackAxis_vertical]: defaultStore.state.notificationStackAxis === 'vertical',
-		[$style.notificationsStackAxis_horizontal]: defaultStore.state.notificationStackAxis === 'horizontal',
+		[$style.notificationsPosition_leftTop]: prefer.s.notificationPosition === 'leftTop',
+		[$style.notificationsPosition_leftBottom]: prefer.s.notificationPosition === 'leftBottom',
+		[$style.notificationsPosition_rightTop]: prefer.s.notificationPosition === 'rightTop',
+		[$style.notificationsPosition_rightBottom]: prefer.s.notificationPosition === 'rightBottom',
+		[$style.notificationsStackAxis_vertical]: prefer.s.notificationStackAxis === 'vertical',
+		[$style.notificationsStackAxis_horizontal]: prefer.s.notificationStackAxis === 'horizontal',
 	}]"
-	:moveClass="defaultStore.state.animation ? $style.transition_notification_move : ''"
-	:enterActiveClass="defaultStore.state.animation ? $style.transition_notification_enterActive : ''"
-	:leaveActiveClass="defaultStore.state.animation ? $style.transition_notification_leaveActive : ''"
-	:enterFromClass="defaultStore.state.animation ? $style.transition_notification_enterFrom : ''"
-	:leaveToClass="defaultStore.state.animation ? $style.transition_notification_leaveTo : ''"
+	:moveClass="prefer.s.animation ? $style.transition_notification_move : ''"
+	:enterActiveClass="prefer.s.animation ? $style.transition_notification_enterActive : ''"
+	:leaveActiveClass="prefer.s.animation ? $style.transition_notification_leaveActive : ''"
+	:enterFromClass="prefer.s.animation ? $style.transition_notification_enterFrom : ''"
+	:leaveToClass="prefer.s.animation ? $style.transition_notification_leaveTo : ''"
 >
 	<div
 		v-for="notification in notifications" :key="notification.id" :class="$style.notification" :style="{
@@ -55,13 +54,13 @@ import * as Misskey from 'misskey-js';
 import { swInject } from './sw-inject.js';
 import XNotification from './notification.vue';
 import { popups } from '@/os.js';
-import { pendingApiRequestsCount } from '@/scripts/misskey-api.js';
-import { uploads } from '@/scripts/upload.js';
-import * as sound from '@/scripts/sound.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 { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { globalEvents } from '@/events.js';
 
 const XStreamIndicator = defineAsyncComponent(() => import('./stream-indicator.vue'));
@@ -103,7 +102,7 @@ if ($i) {
 }
 
 function getPointerEvents() {
-	return defaultStore.state.notificationClickable ? undefined : 'none';
+	return prefer.s.notificationClickable ? undefined : 'none';
 }
 </script>
 
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index 44253e93bd..698e9d8d47 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -54,11 +54,11 @@ 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 { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 
-const menu = toRef(defaultStore.state, 'menu');
+const menu = toRef(prefer.s, 'menu');
 const otherMenuItemIndicated = computed(() => {
 	for (const def in navbarItemDef) {
 		if (menu.value.includes(def)) continue;
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index fec8666dc1..234972e76d 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -9,12 +9,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div :class="$style.top">
 			<div :class="$style.banner" :style="{ backgroundImage: `url(${ instance.bannerUrl })` }"></div>
 			<button v-tooltip.noDelay.right="instance.name ?? i18n.ts.instance" class="_button" :class="$style.instance" @click="openInstanceMenu">
-				<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
+				<img :src="instance.iconUrl || instance.faviconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon" style="viewTransitionName: navbar-serverIcon;"/>
 			</button>
 		</div>
 		<div :class="$style.middle">
 			<MkA v-tooltip.noDelay.right="i18n.ts.timeline" :class="$style.item" :activeClass="$style.active" to="/" exact>
-				<i :class="$style.itemIcon" class="ti ti-home ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
+				<i :class="$style.itemIcon" class="ti ti-home ti-fw" style="viewTransitionName: navbar-homeIcon;"></i><span :class="$style.itemText">{{ i18n.ts.timeline }}</span>
 			</MkA>
 			<template v-for="item in menu">
 				<div v-if="item === '-'" :class="$style.divider"></div>
@@ -28,7 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					:to="navbarItemDef[item].to"
 					v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
 				>
-					<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
+					<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]" :style="{ viewTransitionName: 'navbar-item-' + item }"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
 					<span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator" class="_blink">
 						<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
 						<i v-else class="_indicatorCircle"></i>
@@ -37,14 +37,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</template>
 			<div :class="$style.divider"></div>
 			<MkA v-if="$i != null && ($i.isAdmin || $i.isModerator)" v-tooltip.noDelay.right="i18n.ts.controlPanel" :class="$style.item" :activeClass="$style.active" to="/admin">
-				<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
+				<i :class="$style.itemIcon" class="ti ti-dashboard ti-fw" style="viewTransitionName: navbar-controlPanel;"></i><span :class="$style.itemText">{{ i18n.ts.controlPanel }}</span>
 			</MkA>
 			<button class="_button" :class="$style.item" @click="more">
-				<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
+				<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw" style="viewTransitionName: navbar-more;"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
 				<span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
 			</button>
 			<MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings">
-				<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
+				<i :class="$style.itemIcon" class="ti ti-settings ti-fw" style="viewTransitionName: navbar-settings;"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
 			</MkA>
 		</div>
 		<div :class="$style.bottom">
@@ -52,7 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 				<i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span>
 			</button>
 			<button v-if="$i != null" v-tooltip.noDelay.right="`${i18n.ts.account}: @${$i.username}`" class="_button" :class="[$style.account]" @click="openAccountMenu">
-				<MkAvatar :user="$i" :class="$style.avatar"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/>
+				<MkAvatar :user="$i" :class="$style.avatar" style="viewTransitionName: navbar-avatar;"/><MkAcct class="_nowrap" :class="$style.acct" :user="$i"/>
 			</button>
 		</div>
 	</div>
@@ -94,20 +94,21 @@ 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 { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
-import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
+import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
 import { useRouter } from '@/router/supplier.js';
+import { prefer } from '@/preferences.js';
 
 const router = useRouter();
 
 const forceIconOnly = ref(window.innerWidth <= 1279);
 const iconOnly = computed(() => {
-	return forceIconOnly.value || (defaultStore.reactiveState.menuDisplay.value === 'sideIcon');
+	return forceIconOnly.value || (store.r.menuDisplay.value === 'sideIcon');
 });
 
-const menu = computed(() => defaultStore.state.menu);
+const menu = computed(() => prefer.s.menu);
 const otherMenuItemIndicated = computed(() => {
 	for (const def in navbarItemDef) {
 		if (menu.value.includes(def)) continue;
@@ -122,12 +123,18 @@ function calcViewState() {
 
 window.addEventListener('resize', calcViewState);
 
-watch(defaultStore.reactiveState.menuDisplay, () => {
+watch(store.r.menuDisplay, () => {
 	calcViewState();
 });
 
 function toggleIconOnly() {
-	defaultStore.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon');
+	if (document.startViewTransition && prefer.s.animation) {
+		document.startViewTransition(() => {
+			store.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon');
+		});
+	} else {
+		store.set('menuDisplay', iconOnly.value ? 'sideFull' : 'sideIcon');
+	}
 }
 
 function openAccountMenu(ev: MouseEvent) {
diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue
index e234bb3a33..16e72fa227 100644
--- a/packages/frontend/src/ui/_common_/statusbar-federation.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue
@@ -34,9 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MarqueeText from '@/components/MkMarquee.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 
 const props = defineProps<{
 	display?: 'marquee' | 'oneByOne';
diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue
index da8fa8bb21..4da89a181e 100644
--- a/packages/frontend/src/ui/_common_/statusbar-rss.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue
@@ -31,7 +31,7 @@ import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import MarqueeText from '@/components/MkMarquee.vue';
 import { useInterval } from '@@/js/use-interval.js';
-import { shuffle } from '@/scripts/shuffle.js';
+import { shuffle } from '@/utility/shuffle.js';
 
 const props = defineProps<{
 	url?: string;
diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
index 078b595dca..c5bee51162 100644
--- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
@@ -34,9 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import MarqueeText from '@/components/MkMarquee.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
-import { getNoteSummary } from '@/scripts/get-note-summary.js';
+import { getNoteSummary } from '@/utility/get-note-summary.js';
 import { notePage } from '@/filters/note.js';
 
 const props = defineProps<{
diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue
index ed881bef22..a8d87599e6 100644
--- a/packages/frontend/src/ui/_common_/statusbars.vue
+++ b/packages/frontend/src/ui/_common_/statusbars.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <div :class="$style.root">
 	<div
-		v-for="x in defaultStore.reactiveState.statusbars.value" :key="x.id" :class="[$style.item, { [$style.black]: x.black,
+		v-for="x in prefer.r.statusbars.value" :key="x.id" :class="[$style.item, { [$style.black]: x.black,
 			[$style.verySmall]: x.size === 'verySmall',
 			[$style.small]: x.size === 'small',
 			[$style.large]: x.size === 'large',
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent } from 'vue';
 import { instance } from '@/instance.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 const XRss = defineAsyncComponent(() => import('./statusbar-rss.vue'));
 const XFederation = defineAsyncComponent(() => import('./statusbar-federation.vue'));
 const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue'));
diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue
index cc62a28b14..1eb809d198 100644
--- a/packages/frontend/src/ui/_common_/stream-indicator.vue
+++ b/packages/frontend/src/ui/_common_/stream-indicator.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<div v-if="hasDisconnected && defaultStore.state.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected">
+<div v-if="hasDisconnected && prefer.s.serverDisconnectedBehavior === 'quiet'" :class="$style.root" class="_panel _shadow" @click="resetDisconnected">
 	<div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.disconnectedFromServer }}</div>
 	<div :class="$style.command" class="_buttons">
 		<MkButton small primary @click="reload">{{ i18n.ts.reload }}</MkButton>
@@ -19,7 +19,7 @@ import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const zIndex = os.claimZIndex('high');
 
diff --git a/packages/frontend/src/ui/_common_/sw-inject.ts b/packages/frontend/src/ui/_common_/sw-inject.ts
index ff851ad99f..df392c6532 100644
--- a/packages/frontend/src/ui/_common_/sw-inject.ts
+++ b/packages/frontend/src/ui/_common_/sw-inject.ts
@@ -4,10 +4,10 @@
  */
 
 import { post } from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { $i, login } from '@/account.js';
-import { getAccountFromId } from '@/scripts/get-account-from-id.js';
-import { deepClone } from '@/scripts/clone.js';
+import { getAccountFromId } from '@/utility/get-account-from-id.js';
+import { deepClone } from '@/utility/clone.js';
 import { mainRouter } from '@/router/main.js';
 
 export function swInject() {
diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue
index c7d1387eae..3e5653e46d 100644
--- a/packages/frontend/src/ui/_common_/upload.vue
+++ b/packages/frontend/src/ui/_common_/upload.vue
@@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { } from 'vue';
 import * as os from '@/os.js';
-import { uploads } from '@/scripts/upload.js';
+import { uploads } from '@/utility/upload.js';
 import { i18n } from '@/i18n.js';
 
 const zIndex = os.claimZIndex('high');
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index f4633314ae..39b40754ff 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -53,15 +53,16 @@ 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 { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
 
 const WINDOW_THRESHOLD = 1400;
 
 const settingsWindowed = ref(window.innerWidth > WINDOW_THRESHOLD);
-const menu = ref(defaultStore.state.menu);
-// const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
+const menu = ref(prefer.s.menu);
+// const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
 const otherNavItemIndicated = computed<boolean>(() => {
 	for (const def in navbarItemDef) {
 		if (menu.value.includes(def)) continue;
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 5acef0bef8..259aad7401 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -56,17 +56,18 @@ 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 '@/scripts/sticky-sidebar.js';
+// import { StickySidebar } from '@/utility/sticky-sidebar.js';
 // import { mainRouter } from '@/router.js';
 //import MisskeyLogo from '@assets/client/misskey.svg';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { instance } from '@/instance.js';
 import { i18n } from '@/i18n.js';
+import { prefer } from '@/preferences.js';
 
 const WINDOW_THRESHOLD = 1400;
 
-const menu = ref(defaultStore.state.menu);
-const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
+const menu = ref(prefer.s.menu);
+const menuDisplay = computed(store.makeGetterSetter('menuDisplay'));
 const otherNavItemIndicated = computed<boolean>(() => {
 	for (const def in navbarItemDef) {
 		if (menu.value.includes(def)) continue;
@@ -99,7 +100,7 @@ function openAccountMenu(ev: MouseEvent) {
 	}, ev);
 }
 
-watch(defaultStore.reactiveState.menuDisplay, () => {
+watch(store.r.menuDisplay, () => {
 	calcViewState();
 });
 
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index f9e477c1ba..6493cb6cbc 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 	</div>
 
-	<Transition :name="defaultStore.state.animation ? 'tray-back' : ''">
+	<Transition :name="prefer.s.animation ? 'tray-back' : ''">
 		<div
 			v-if="widgetsShowing"
 			class="tray-back _modalBg"
@@ -35,11 +35,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 		></div>
 	</Transition>
 
-	<Transition :name="defaultStore.state.animation ? 'tray' : ''">
+	<Transition :name="prefer.s.animation ? 'tray' : ''">
 		<XWidgets v-if="widgetsShowing" class="tray"/>
 	</Transition>
 
-	<iframe v-if="defaultStore.state.aiChanMode" ref="live2d" class="ivnzpscs" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe>
+	<iframe v-if="prefer.s.aiChanMode" ref="live2d" class="ivnzpscs" src="https://misskey-dev.github.io/mascot-web/?scale=2&y=1.4"></iframe>
 
 	<XCommon/>
 </div>
@@ -47,18 +47,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { defineAsyncComponent, onMounted, provide, ref, computed, shallowRef } from 'vue';
+import { instanceName } from '@@/js/config.js';
+import { isLink } from '@@/js/is-link.js';
 import XSidebar from './classic.sidebar.vue';
 import XCommon from './_common_/common.vue';
-import { instanceName } from '@@/js/config.js';
-import { StickySidebar } from '@/scripts/sticky-sidebar.js';
+import type { PageMetadata } from '@/page.js';
+import { StickySidebar } from '@/utility/sticky-sidebar.js';
 import * as os from '@/os.js';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
-import { defaultStore } from '@/store.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { mainRouter } from '@/router/main.js';
-import { isLink } from '@@/js/is-link.js';
+import { prefer } from '@/preferences.js';
+import { DI } from '@/di.js';
 
 const XHeaderMenu = defineAsyncComponent(() => import('./classic.header.vue'));
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
@@ -74,12 +76,12 @@ const widgetsShowing = ref(false);
 const fullView = ref(false);
 const globalHeaderHeight = ref(0);
 const wallpaper = miLocalStorage.getItem('wallpaper') != null;
-const showMenuOnTop = computed(() => defaultStore.state.menuDisplay === 'top');
+const showMenuOnTop = computed(() => store.s.menuDisplay === 'top');
 const live2d = shallowRef<HTMLIFrameElement>();
 const widgetsLeft = ref<HTMLElement>();
 const widgetsRight = ref<HTMLElement>();
 
-provide('router', mainRouter);
+provide(DI.router, mainRouter);
 provideMetadataReceiver((metadataGetter) => {
 	const info = metadataGetter();
 	pageMetadata.value = info;
@@ -96,7 +98,7 @@ provide('shouldHeaderThin', showMenuOnTop.value);
 provide('forceSpacerMin', true);
 
 function attachSticky(el: HTMLElement) {
-	const sticky = new StickySidebar(el, 0, defaultStore.state.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す
+	const sticky = new StickySidebar(el, 0, store.s.menuDisplay === 'top' ? 60 : 0); // TODO: ヘッダーの高さを60pxと決め打ちしているのを直す
 	window.addEventListener('scroll', () => {
 		sticky.calc(window.scrollY);
 	}, { passive: true });
@@ -142,27 +144,25 @@ if (window.innerWidth < 1024) {
 
 document.documentElement.style.overflowY = 'scroll';
 
-defaultStore.loaded.then(() => {
-	if (defaultStore.state.widgets.length === 0) {
-		defaultStore.set('widgets', [{
-			name: 'calendar',
-			id: 'a', place: null, data: {},
-		}, {
-			name: 'notifications',
-			id: 'b', place: null, data: {},
-		}, {
-			name: 'trends',
-			id: 'c', place: null, data: {},
-		}]);
-	}
-});
+if (prefer.s.widgets.length === 0) {
+	prefer.commit('widgets', [{
+		name: 'calendar',
+		id: 'a', place: null, data: {},
+	}, {
+		name: 'notifications',
+		id: 'b', place: null, data: {},
+	}, {
+		name: 'trends',
+		id: 'c', place: null, data: {},
+	}]);
+}
 
 onMounted(() => {
 	window.addEventListener('resize', () => {
 		isDesktop.value = (window.innerWidth >= DESKTOP_THRESHOLD);
 	}, { passive: true });
 
-	if (defaultStore.state.aiChanMode) {
+	if (prefer.s.aiChanMode) {
 		const iframeRect = live2d.value.getBoundingClientRect();
 		window.addEventListener('mousemove', ev => {
 			live2d.value.contentWindow.postMessage({
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index 0b870fc185..0c810d03c6 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<div :class="$style.main">
 		<XAnnouncements v-if="$i"/>
 		<XStatusBars/>
-		<div ref="columnsEl" :class="[$style.sections, { [$style.center]: deckStore.reactiveState.columnAlign.value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel">
+		<div ref="columnsEl" :class="[$style.sections, { [$style.center]: prefer.r['deck.columnAlign'].value === 'center', [$style.snapScroll]: snapScroll }]" @contextmenu.self.prevent="onContextmenu" @wheel.self="onWheel">
 			<!-- sectionを利用しているのは、deck.vue側でcolumnに対してfirst-of-typeを効かせるため -->
 			<section
 				v-for="ids in layout"
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			</div>
 			<div :class="$style.sideMenu">
 				<div :class="$style.sideMenuTop">
-					<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${deckStore.state.profile}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button>
+					<button v-tooltip.noDelay.left="`${i18n.ts._deck.profile}: ${store.s['deck.profile']}`" :class="$style.sideMenuButton" class="_button" @click="changeProfile"><i class="ti ti-caret-down"></i></button>
 					<button v-tooltip.noDelay.left="i18n.ts._deck.deleteProfile" :class="$style.sideMenuButton" class="_button" @click="deleteProfile"><i class="ti ti-trash"></i></button>
 				</div>
 				<div :class="$style.sideMenuMiddle">
@@ -67,10 +67,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
 	>
 		<div
 			v-if="drawerMenuShowing"
@@ -82,10 +82,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</Transition>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''"
 	>
 		<div v-if="drawerMenuShowing" :class="$style.menu">
 			<XDrawerMenu/>
@@ -100,8 +100,6 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed, defineAsyncComponent, ref, watch, shallowRef } from 'vue';
 import { v4 as uuid } from 'uuid';
 import XCommon from './_common_/common.vue';
-import { deckStore, columnTypes, addColumn as addColumnToStore, forceSaveDeck, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
-import type { ColumnType } from './deck/deck-store.js';
 import type { MenuItem } from '@/types/menu.js';
 import XSidebar from '@/ui/_common_/navbar.vue';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
@@ -110,9 +108,9 @@ import * as os from '@/os.js';
 import { navbarItemDef } from '@/navbar.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
-import { deviceKind } from '@/scripts/device-kind.js';
-import { defaultStore } from '@/store.js';
+import { unisonReload } from '@/utility/unison-reload.js';
+import { deviceKind } from '@/utility/device-kind.js';
+import { prefer } from '@/preferences.js';
 import XMainColumn from '@/ui/deck/main-column.vue';
 import XTlColumn from '@/ui/deck/tl-column.vue';
 import XAntennaColumn from '@/ui/deck/antenna-column.vue';
@@ -124,6 +122,8 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
 import XDirectColumn from '@/ui/deck/direct-column.vue';
 import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
 import { mainRouter } from '@/router/main.js';
+import { store } from '@/store.js';
+import { columnTypes, forceSaveDeck, getProfiles, loadDeck, addColumn as addColumnToStore, deleteProfile as deleteProfile_ } from '@/deck.js';
 const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
 const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
 
@@ -142,8 +142,8 @@ const columnComponents = {
 
 mainRouter.navHook = (path, flag): boolean => {
 	if (flag === 'forcePage') return false;
-	const noMainColumn = !deckStore.state.columns.some(x => x.type === 'main');
-	if (deckStore.state.navWindow || noMainColumn) {
+	const noMainColumn = !store.s['deck.columns'].some(x => x.type === 'main');
+	if (prefer.s['deck.navWindow'] || noMainColumn) {
 		os.pageWindow(path);
 		return true;
 	}
@@ -165,8 +165,8 @@ watch(route, () => {
 });
 */
 
-const columns = deckStore.reactiveState.columns;
-const layout = deckStore.reactiveState.layout;
+const columns = store.r['deck.columns'];
+const layout = store.r['deck.layout'];
 const menuIndicated = computed(() => {
 	if ($i == null) return false;
 	for (const def in navbarItemDef) {
@@ -219,15 +219,15 @@ loadDeck();
 
 function changeProfile(ev: MouseEvent) {
 	let items: MenuItem[] = [{
-		text: deckStore.state.profile,
+		text: store.s['deck.profile'],
 		active: true,
 		action: () => {},
 	}];
 	getProfiles().then(profiles => {
-		items.push(...(profiles.filter(k => k !== deckStore.state.profile).map(k => ({
+		items.push(...(profiles.filter(k => k !== store.s['deck.profile']).map(k => ({
 			text: k,
 			action: () => {
-				deckStore.set('profile', k);
+				store.set('deck.profile', k);
 				unisonReload();
 			},
 		}))), { type: 'divider' as const }, {
@@ -242,7 +242,7 @@ function changeProfile(ev: MouseEvent) {
 				if (canceled || name == null) return;
 
 				os.promiseDialog((async () => {
-					await deckStore.set('profile', name);
+					await store.set('deck.profile', name);
 					await forceSaveDeck();
 				})(), () => {
 					unisonReload();
@@ -257,19 +257,19 @@ function changeProfile(ev: MouseEvent) {
 async function deleteProfile() {
 	const { canceled } = await os.confirm({
 		type: 'warning',
-		text: i18n.tsx.deleteAreYouSure({ x: deckStore.state.profile }),
+		text: i18n.tsx.deleteAreYouSure({ x: store.s['deck.profile'] }),
 	});
 	if (canceled) return;
 
 	os.promiseDialog((async () => {
-		if (deckStore.state.profile === 'default') {
-			await deckStore.set('columns', []);
-			await deckStore.set('layout', []);
+		if (store.s['deck.profile'] === 'default') {
+			await store.set('deck.columns', []);
+			await store.set('deck.layout', []);
 			await forceSaveDeck();
 		} else {
-			await deleteProfile_(deckStore.state.profile);
+			await deleteProfile_(store.s['deck.profile']);
 		}
-		await deckStore.set('profile', 'default');
+		await store.set('deck.profile', 'default');
 	})(), () => {
 		unisonReload();
 	});
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index b79cd8408b..ae282ba324 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -15,19 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, ref, shallowRef, watch, defineAsyncComponent } from 'vue';
-import type { entities as MisskeyEntities } from 'misskey-js';
 import XColumn from './column.vue';
-import { updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import type { entities as MisskeyEntities } from 'misskey-js';
+import type { Column } from '@/deck.js';
+import type { MenuItem } from '@/types/menu.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { updateColumn } from '@/deck.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import type { MenuItem } from '@/types/menu.js';
 import { antennasCache } from '@/cache.js';
-import type { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 
 const props = defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index 9e07c06639..964b42874e 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -22,18 +22,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, ref, shallowRef, watch } from 'vue';
 import * as Misskey from 'misskey-js';
 import XColumn from './column.vue';
-import { updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import type { Column } from '@/deck.js';
+import type { MenuItem } from '@/types/menu.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { updateColumn } from '@/deck.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os.js';
 import { favoritedChannelsCache } from '@/cache.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import type { MenuItem } from '@/types/menu.js';
-import type { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 
 const props = defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index f23e33c748..fc208197a0 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -43,11 +43,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onBeforeUnmount, onMounted, provide, watch, shallowRef, ref, computed } from 'vue';
-import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from './deck-store.js';
+import type { Column } from '@/deck.js';
+import type { MenuItem } from '@/types/menu.js';
+import { updateColumn, swapLeftColumn, swapRightColumn, swapUpColumn, swapDownColumn, stackLeftColumn, popRightColumn, removeColumn, swapColumn } from '@/deck.js';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
-import type { Column } from './deck-store.js';
-import type { MenuItem } from '@/types/menu.js';
 
 provide('shouldHeaderThin', true);
 provide('shouldOmitHeaderTitle', true);
diff --git a/packages/frontend/src/ui/deck/deck-store.ts b/packages/frontend/src/ui/deck/deck-store.ts
index 4782a2fec5..bdca513a7a 100644
--- a/packages/frontend/src/ui/deck/deck-store.ts
+++ b/packages/frontend/src/ui/deck/deck-store.ts
@@ -3,57 +3,11 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { throttle } from 'throttle-debounce';
-import { computed, markRaw, Ref } from 'vue';
-import { notificationTypes } from 'misskey-js';
-import type { BasicTimelineType } from '@/timelines.js';
+import { markRaw } from 'vue';
+import type { Column } from '@/deck.js';
 import { Storage } from '@/pizzax.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { deepClone } from '@/scripts/clone.js';
-import type { SoundStore } from '@/store.js';
-
-type ColumnWidget = {
-	name: string;
-	id: string;
-	data: Record<string, any>;
-};
-
-export const columnTypes = [
-	'main',
-	'widgets',
-	'notifications',
-	'tl',
-	'antenna',
-	'list',
-	'channel',
-	'mentions',
-	'direct',
-	'roleTimeline',
-] as const;
-
-export type ColumnType = typeof columnTypes[number];
-
-export type Column = {
-	id: string;
-	type: ColumnType;
-	name: string | null;
-	width: number;
-	widgets?: ColumnWidget[];
-	active?: boolean;
-	flexible?: boolean;
-	antennaId?: string;
-	listId?: string;
-	channelId?: string;
-	roleId?: string;
-	excludeTypes?: typeof notificationTypes[number][];
-	tl?: BasicTimelineType;
-	withRenotes?: boolean;
-	withReplies?: boolean;
-	withSensitive?: boolean;
-	onlyFiles?: boolean;
-	soundSetting?: SoundStore;
-};
 
+// TODO: 消す(移行済みのため)
 export const deckStore = markRaw(new Storage('deck', {
 	profile: {
 		where: 'deviceAccount',
@@ -67,284 +21,4 @@ export const deckStore = markRaw(new Storage('deck', {
 		where: 'deviceAccount',
 		default: [] as Column['id'][][],
 	},
-	columnAlign: {
-		where: 'deviceAccount',
-		default: 'left' as 'left' | 'right' | 'center',
-	},
-	alwaysShowMainColumn: {
-		where: 'deviceAccount',
-		default: true,
-	},
-	navWindow: {
-		where: 'deviceAccount',
-		default: true,
-	},
-	useSimpleUiForNonRootPages: {
-		where: 'deviceAccount',
-		default: true,
-	},
 }));
-
-export const loadDeck = async () => {
-	let deck;
-
-	try {
-		deck = await misskeyApi('i/registry/get', {
-			scope: ['client', 'deck', 'profiles'],
-			key: deckStore.state.profile,
-		});
-	} catch (err) {
-		if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') {
-			// 後方互換性のため
-			if (deckStore.state.profile === 'default') {
-				saveDeck();
-				return;
-			}
-
-			deckStore.set('columns', []);
-			deckStore.set('layout', []);
-			return;
-		}
-		throw err;
-	}
-
-	deckStore.set('columns', deck.columns);
-	deckStore.set('layout', deck.layout);
-};
-
-export async function forceSaveDeck() {
-	await misskeyApi('i/registry/set', {
-		scope: ['client', 'deck', 'profiles'],
-		key: deckStore.state.profile,
-		value: {
-			columns: deckStore.reactiveState.columns.value,
-			layout: deckStore.reactiveState.layout.value,
-		},
-	});
-}
-
-// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
-export const saveDeck = throttle(1000, () => {
-	forceSaveDeck();
-});
-
-export async function getProfiles(): Promise<string[]> {
-	return await misskeyApi('i/registry/keys', {
-		scope: ['client', 'deck', 'profiles'],
-	});
-}
-
-export async function deleteProfile(key: string): Promise<void> {
-	return await misskeyApi('i/registry/remove', {
-		scope: ['client', 'deck', 'profiles'],
-		key: key,
-	});
-}
-
-export function addColumn(column: Column) {
-	if (column.name === undefined) column.name = null;
-	deckStore.push('columns', column);
-	deckStore.push('layout', [column.id]);
-	saveDeck();
-}
-
-export function removeColumn(id: Column['id']) {
-	deckStore.set('columns', deckStore.state.columns.filter(c => c.id !== id));
-	deckStore.set('layout', deckStore.state.layout
-		.map(ids => ids.filter(_id => _id !== id))
-		.filter(ids => ids.length > 0));
-	saveDeck();
-}
-
-export function swapColumn(a: Column['id'], b: Column['id']) {
-	const aX = deckStore.state.layout.findIndex(ids => ids.indexOf(a) !== -1);
-	const aY = deckStore.state.layout[aX].findIndex(id => id === a);
-	const bX = deckStore.state.layout.findIndex(ids => ids.indexOf(b) !== -1);
-	const bY = deckStore.state.layout[bX].findIndex(id => id === b);
-	const layout = deepClone(deckStore.state.layout);
-	layout[aX][aY] = b;
-	layout[bX][bY] = a;
-	deckStore.set('layout', layout);
-	saveDeck();
-}
-
-export function swapLeftColumn(id: Column['id']) {
-	const layout = deepClone(deckStore.state.layout);
-	deckStore.state.layout.some((ids, i) => {
-		if (ids.includes(id)) {
-			const left = deckStore.state.layout[i - 1];
-			if (left) {
-				layout[i - 1] = deckStore.state.layout[i];
-				layout[i] = left;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-		return false;
-	});
-	saveDeck();
-}
-
-export function swapRightColumn(id: Column['id']) {
-	const layout = deepClone(deckStore.state.layout);
-	deckStore.state.layout.some((ids, i) => {
-		if (ids.includes(id)) {
-			const right = deckStore.state.layout[i + 1];
-			if (right) {
-				layout[i + 1] = deckStore.state.layout[i];
-				layout[i] = right;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-		return false;
-	});
-	saveDeck();
-}
-
-export function swapUpColumn(id: Column['id']) {
-	const layout = deepClone(deckStore.state.layout);
-	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	const ids = deepClone(deckStore.state.layout[idsIndex]);
-	ids.some((x, i) => {
-		if (x === id) {
-			const up = ids[i - 1];
-			if (up) {
-				ids[i - 1] = id;
-				ids[i] = up;
-
-				layout[idsIndex] = ids;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-		return false;
-	});
-	saveDeck();
-}
-
-export function swapDownColumn(id: Column['id']) {
-	const layout = deepClone(deckStore.state.layout);
-	const idsIndex = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	const ids = deepClone(deckStore.state.layout[idsIndex]);
-	ids.some((x, i) => {
-		if (x === id) {
-			const down = ids[i + 1];
-			if (down) {
-				ids[i + 1] = id;
-				ids[i] = down;
-
-				layout[idsIndex] = ids;
-				deckStore.set('layout', layout);
-			}
-			return true;
-		}
-		return false;
-	});
-	saveDeck();
-}
-
-export function stackLeftColumn(id: Column['id']) {
-	let layout = deepClone(deckStore.state.layout);
-	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	layout = layout.map(ids => ids.filter(_id => _id !== id));
-	layout[i - 1].push(id);
-	layout = layout.filter(ids => ids.length > 0);
-	deckStore.set('layout', layout);
-	saveDeck();
-}
-
-export function popRightColumn(id: Column['id']) {
-	let layout = deepClone(deckStore.state.layout);
-	const i = deckStore.state.layout.findIndex(ids => ids.includes(id));
-	const affected = layout[i];
-	layout = layout.map(ids => ids.filter(_id => _id !== id));
-	layout.splice(i + 1, 0, [id]);
-	layout = layout.filter(ids => ids.length > 0);
-	deckStore.set('layout', layout);
-
-	const columns = deepClone(deckStore.state.columns);
-	for (const column of columns) {
-		if (affected.includes(column.id)) {
-			column.active = true;
-		}
-	}
-	deckStore.set('columns', columns);
-
-	saveDeck();
-}
-
-export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
-	const columns = deepClone(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	if (column.widgets == null) column.widgets = [];
-	column.widgets.unshift(widget);
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
-	const columns = deepClone(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	if (column.widgets == null) column.widgets = [];
-	column.widgets = column.widgets.filter(w => w.id !== widget.id);
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
-	const columns = deepClone(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	column.widgets = widgets;
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export function updateColumnWidget(id: Column['id'], widgetId: string, widgetData: any) {
-	const columns = deepClone(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const column = deepClone(deckStore.state.columns[columnIndex]);
-	if (column == null) return;
-	if (column.widgets == null) column.widgets = [];
-	column.widgets = column.widgets.map(w => w.id === widgetId ? {
-		...w,
-		data: widgetData,
-	} : w);
-	columns[columnIndex] = column;
-	deckStore.set('columns', columns);
-	saveDeck();
-}
-
-export async function updateColumn<TColumn>(id: Column['id'], column: Partial<TColumn>) {
-	const columns = deepClone(deckStore.state.columns);
-	const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
-	const currentColumn = deepClone(deckStore.state.columns[columnIndex]);
-	if (currentColumn == null) return;
-	for (const [k, v] of Object.entries(column)) {
-		currentColumn[k] = v;
-	}
-	columns[columnIndex] = currentColumn;
-	await Promise.all([
-		deckStore.set('columns', columns),
-		saveDeck(),
-	]);
-}
-
-export function getColumn<TColumn extends Column>(id: Column['id']): TColumn {
-	return deckStore.state.columns.find(c => c.id === id) as TColumn;
-}
-
-export function getReactiveColumn<TColumn extends Column>(id: Column['id']): Ref<TColumn> {
-	return computed(() => {
-		return deckStore.reactiveState.columns.value.find(c => c.id === id) as TColumn;
-	});
-}
diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue
index 2cecd6c669..772188d773 100644
--- a/packages/frontend/src/ui/deck/direct-column.vue
+++ b/packages/frontend/src/ui/deck/direct-column.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import XColumn from './column.vue';
-import type { Column } from './deck-store.js';
+import type { Column } from '@/deck.js';
 import MkNotes from '@/components/MkNotes.vue';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue
index 83961d02bc..2b4e86b8a2 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -15,19 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { watch, shallowRef, ref, onMounted } from 'vue';
-import type { entities as MisskeyEntities } from 'misskey-js';
 import XColumn from './column.vue';
-import { updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import type { entities as MisskeyEntities } from 'misskey-js';
+import type { Column } from '@/deck.js';
+import type { MenuItem } from '@/types/menu.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { updateColumn } from '@/deck.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import type { MenuItem } from '@/types/menu.js';
-import type { SoundStore } from '@/store.js';
 import { userListsCache } from '@/cache.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 
 const props = defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue
index 45c39a5cad..1ba01a4c8d 100644
--- a/packages/frontend/src/ui/deck/main-column.vue
+++ b/packages/frontend/src/ui/deck/main-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn v-if="deckStore.state.alwaysShowMainColumn || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked">
+<XColumn v-if="prefer.s['deck.alwaysShowMainColumn'] || mainRouter.currentRoute.value.name !== 'index'" :column="column" :isStacked="isStacked">
 	<template #header>
 		<template v-if="pageMetadata">
 			<i :class="pageMetadata.icon"></i>
@@ -20,17 +20,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { provide, shallowRef, ref } from 'vue';
-import XColumn from './column.vue';
-import { deckStore } from '@/ui/deck/deck-store.js';
-import type { Column } from '@/ui/deck/deck-store.js';
-import * as os from '@/os.js';
-import { i18n } from '@/i18n.js';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
-import { useScrollPositionManager } from '@/nirax.js';
 import { getScrollContainer } from '@@/js/scroll.js';
 import { isLink } from '@@/js/is-link.js';
+import XColumn from './column.vue';
+import type { Column } from '@/deck.js';
+import type { PageMetadata } from '@/page.js';
+import * as os from '@/os.js';
+import { i18n } from '@/i18n.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
+import { useScrollPositionManager } from '@/nirax.js';
 import { mainRouter } from '@/router/main.js';
+import { prefer } from '@/preferences.js';
+import { DI } from '@/di.js';
 
 defineProps<{
 	column: Column;
@@ -40,7 +41,7 @@ defineProps<{
 const contents = shallowRef<HTMLElement>();
 const pageMetadata = ref<null | PageMetadata>(null);
 
-provide('router', mainRouter);
+provide(DI.router, mainRouter);
 provideMetadataReceiver((metadataGetter) => {
 	const info = metadataGetter();
 	pageMetadata.value = info;
diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue
index 233fba554b..ffd0307940 100644
--- a/packages/frontend/src/ui/deck/mentions-column.vue
+++ b/packages/frontend/src/ui/deck/mentions-column.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import XColumn from './column.vue';
-import type { Column } from './deck-store.js';
+import type { Column } from '@/deck.js';
 import MkNotes from '@/components/MkNotes.vue';
 import { i18n } from '../../i18n.js';
 
diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue
index c0303e86dc..0a2c0e9952 100644
--- a/packages/frontend/src/ui/deck/notifications-column.vue
+++ b/packages/frontend/src/ui/deck/notifications-column.vue
@@ -14,8 +14,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { defineAsyncComponent, shallowRef } from 'vue';
 import XColumn from './column.vue';
-import { updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import { updateColumn } from '@/deck.js';
+import type { Column } from '@/deck.js';
 import XNotifications from '@/components/MkNotifications.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index 5b1420570d..5d07440017 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -16,16 +16,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { computed, onMounted, ref, shallowRef, watch } from 'vue';
 import XColumn from './column.vue';
-import { updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import type { Column } from '@/deck.js';
+import type { MenuItem } from '@/types/menu.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { updateColumn } from '@/deck.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import type { MenuItem } from '@/types/menu.js';
-import type { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 
 const props = defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index b9b3746abf..2dec7bf5aa 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -34,17 +34,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { onMounted, watch, ref, shallowRef, computed } from 'vue';
 import XColumn from './column.vue';
-import { removeColumn, updateColumn } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import type { Column } from '@/deck.js';
 import type { MenuItem } from '@/types/menu.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { removeColumn, updateColumn } from '@/deck.js';
 import MkTimeline from '@/components/MkTimeline.vue';
 import * as os from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { hasWithReplies, isAvailableBasicTimeline, basicTimelineIconClass } from '@/timelines.js';
-import { instance } from '@/instance.js';
-import type { SoundStore } from '@/store.js';
 import { soundSettingsButton } from '@/ui/deck/tl-note-notification.js';
-import * as sound from '@/scripts/sound.js';
+import * as sound from '@/utility/sound.js';
 
 const props = defineProps<{
 	column: Column;
diff --git a/packages/frontend/src/ui/deck/tl-note-notification.ts b/packages/frontend/src/ui/deck/tl-note-notification.ts
index 03d4b3a580..728c0d0d29 100644
--- a/packages/frontend/src/ui/deck/tl-note-notification.ts
+++ b/packages/frontend/src/ui/deck/tl-note-notification.ts
@@ -5,9 +5,9 @@
 
 import * as Misskey from 'misskey-js';
 import type { Ref } from 'vue';
-import type { SoundStore } from '@/store.js';
-import type { SoundType } from '@/scripts/sound.js';
-import { getSoundDuration, playMisskeySfxFile, soundsTypes } from '@/scripts/sound.js';
+import type { SoundType } from '@/utility/sound.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { getSoundDuration, playMisskeySfxFile, soundsTypes } from '@/utility/sound.js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
 
diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue
index 20284d8c9f..4e84ef0ba0 100644
--- a/packages/frontend/src/ui/deck/widgets-column.vue
+++ b/packages/frontend/src/ui/deck/widgets-column.vue
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import XColumn from './column.vue';
-import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from './deck-store.js';
-import type { Column } from './deck-store.js';
+import { addColumnWidget, removeColumnWidget, setColumnWidgets, updateColumnWidget } from '@/deck.js';
+import type { Column } from '@/deck.js';
 import XWidgets from '@/components/MkWidgets.vue';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue
index 5cbcd69b2c..95d564f5a3 100644
--- a/packages/frontend/src/ui/minimum.vue
+++ b/packages/frontend/src/ui/minimum.vue
@@ -15,17 +15,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, provide, ref } from 'vue';
-import XCommon from './_common_/common.vue';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
 import { instanceName } from '@@/js/config.js';
+import XCommon from './_common_/common.vue';
+import type { PageMetadata } from '@/page.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
 import { mainRouter } from '@/router/main.js';
+import { DI } from '@/di.js';
 
 const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
 
 const pageMetadata = ref<null | PageMetadata>(null);
 
-provide('router', mainRouter);
+provide(DI.router, mainRouter);
 provideMetadataReceiver((metadataGetter) => {
 	const info = metadataGetter();
 	pageMetadata.value = info;
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 5037e3f93b..38bd07b7f8 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -10,6 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<MkStickyContainer ref="contents" :class="$style.contents" style="container-type: inline-size;" @contextmenu.stop="onContextmenu">
 		<template #header>
 			<div>
+				<XPreferenceRestore v-if="shouldSuggestRestoreBackup"/>
 				<XAnnouncements v-if="$i"/>
 				<XStatusBars :class="$style.statusbars"/>
 			</div>
@@ -38,10 +39,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_menuDrawerBg_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_menuDrawerBg_leaveTo : ''"
 	>
 		<div
 			v-if="drawerMenuShowing"
@@ -53,10 +54,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</Transition>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_menuDrawer_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_menuDrawer_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_menuDrawer_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_menuDrawer_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_menuDrawer_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_menuDrawer_leaveTo : ''"
 	>
 		<div v-if="drawerMenuShowing" :class="$style.menuDrawer">
 			<XDrawerMenu/>
@@ -64,10 +65,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</Transition>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawerBg_leaveTo : ''"
 	>
 		<div
 			v-if="widgetsShowing"
@@ -79,10 +80,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</Transition>
 
 	<Transition
-		:enterActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterActive : ''"
-		:leaveActiveClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveActive : ''"
-		:enterFromClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_enterFrom : ''"
-		:leaveToClass="defaultStore.state.animation ? $style.transition_widgetsDrawer_leaveTo : ''"
+		:enterActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterActive : ''"
+		:leaveActiveClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveActive : ''"
+		:enterFromClass="prefer.s.animation ? $style.transition_widgetsDrawer_enterFrom : ''"
+		:leaveToClass="prefer.s.animation ? $style.transition_widgetsDrawer_leaveTo : ''"
 	>
 		<div v-if="widgetsShowing" :class="$style.widgetsDrawer">
 			<button class="_button" :class="$style.widgetsCloseButton" @click="widgetsShowing = false"><i class="ti ti-x"></i></button>
@@ -102,23 +103,26 @@ import { isLink } from '@@/js/is-link.js';
 import XCommon from './_common_/common.vue';
 import type { Ref } from 'vue';
 import type MkStickyContainer from '@/components/global/MkStickyContainer.vue';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import type { PageMetadata } from '@/page.js';
 import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
 import * as os from '@/os.js';
-import { defaultStore } from '@/store.js';
 import { navbarItemDef } from '@/navbar.js';
 import { i18n } from '@/i18n.js';
 import { $i } from '@/account.js';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
+import { deviceKind } from '@/utility/device-kind.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { useScrollPositionManager } from '@/nirax.js';
 import { mainRouter } from '@/router/main.js';
+import { prefer } from '@/preferences.js';
+import { shouldSuggestRestoreBackup } from '@/preferences/utility.js';
+import { DI } from '@/di.js';
 
 const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
 const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
 const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
 const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
+const XPreferenceRestore = defineAsyncComponent(() => import('@/ui/_common_/PreferenceRestore.vue'));
 
 const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
 
@@ -137,7 +141,7 @@ const widgetsShowing = ref(false);
 const navFooter = shallowRef<HTMLElement>();
 const contents = shallowRef<InstanceType<typeof MkStickyContainer>>();
 
-provide('router', mainRouter);
+provide(DI.router, mainRouter);
 provideMetadataReceiver((metadataGetter) => {
 	const info = metadataGetter();
 	pageMetadata.value = info;
@@ -174,20 +178,18 @@ if (window.innerWidth > 1024) {
 	}
 }
 
-defaultStore.loaded.then(() => {
-	if (defaultStore.state.widgets.length === 0) {
-		defaultStore.set('widgets', [{
-			name: 'calendar',
-			id: 'a', place: 'right', data: {},
-		}, {
-			name: 'notifications',
-			id: 'b', place: 'right', data: {},
-		}, {
-			name: 'trends',
-			id: 'c', place: 'right', data: {},
-		}]);
-	}
-});
+if (prefer.s.widgets.length === 0) {
+	prefer.commit('widgets', [{
+		name: 'calendar',
+		id: 'a', place: 'right', data: {},
+	}, {
+		name: 'notifications',
+		id: 'b', place: 'right', data: {},
+	}, {
+		name: 'trends',
+		id: 'c', place: 'right', data: {},
+	}]);
+}
 
 onMounted(() => {
 	if (!isDesktop.value) {
diff --git a/packages/frontend/src/ui/universal.widgets.vue b/packages/frontend/src/ui/universal.widgets.vue
index fc0a4475d2..1a6d62e19b 100644
--- a/packages/frontend/src/ui/universal.widgets.vue
+++ b/packages/frontend/src/ui/universal.widgets.vue
@@ -19,7 +19,7 @@ const editMode = ref(false);
 <script lang="ts" setup>
 import XWidgets from '@/components/MkWidgets.vue';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const props = withDefaults(defineProps<{
 	// null = 全てのウィジェットを表示
@@ -31,24 +31,24 @@ const props = withDefaults(defineProps<{
 });
 
 const widgets = computed(() => {
-	if (props.place === null) return defaultStore.reactiveState.widgets.value;
-	if (props.place === 'left') return defaultStore.reactiveState.widgets.value.filter(w => w.place === 'left');
-	return defaultStore.reactiveState.widgets.value.filter(w => w.place !== 'left');
+	if (props.place === null) return prefer.r.widgets.value;
+	if (props.place === 'left') return prefer.r.widgets.value.filter(w => w.place === 'left');
+	return prefer.r.widgets.value.filter(w => w.place !== 'left');
 });
 
 function addWidget(widget) {
-	defaultStore.set('widgets', [{
+	prefer.commit('widgets', [{
 		...widget,
 		place: props.place,
-	}, ...defaultStore.state.widgets]);
+	}, ...prefer.s.widgets]);
 }
 
 function removeWidget(widget) {
-	defaultStore.set('widgets', defaultStore.state.widgets.filter(w => w.id !== widget.id));
+	prefer.commit('widgets', prefer.s.widgets.filter(w => w.id !== widget.id));
 }
 
 function updateWidget({ id, data }) {
-	defaultStore.set('widgets', defaultStore.state.widgets.map(w => w.id === id ? {
+	prefer.commit('widgets', prefer.s.widgets.map(w => w.id === id ? {
 		...w,
 		data,
 		place: props.place,
@@ -57,18 +57,18 @@ function updateWidget({ id, data }) {
 
 function updateWidgets(thisWidgets) {
 	if (props.place === null) {
-		defaultStore.set('widgets', thisWidgets);
+		prefer.commit('widgets', thisWidgets);
 		return;
 	}
 	if (props.place === 'left') {
-		defaultStore.set('widgets', [
+		prefer.commit('widgets', [
 			...thisWidgets.map(w => ({ ...w, place: 'left' })),
-			...defaultStore.state.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)),
+			...prefer.s.widgets.filter(w => w.place !== 'left' && !thisWidgets.some(t => w.id === t.id)),
 		]);
 		return;
 	}
-	defaultStore.set('widgets', [
-		...defaultStore.state.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)),
+	prefer.commit('widgets', [
+		...prefer.s.widgets.filter(w => w.place === 'left' && !thisWidgets.some(t => w.id === t.id)),
 		...thisWidgets.map(w => ({ ...w, place: 'right' })),
 	]);
 }
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index 217e10fede..5bb3b3aba8 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -68,18 +68,18 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onMounted, provide, ref, computed } from 'vue';
-import XCommon from './_common_/common.vue';
 import { instanceName } from '@@/js/config.js';
+import XCommon from './_common_/common.vue';
+import type { PageMetadata } from '@/page.js';
 import * as os from '@/os.js';
 import { instance } from '@/instance.js';
 import XSigninDialog from '@/components/MkSigninDialog.vue';
 import XSignupDialog from '@/components/MkSignupDialog.vue';
-import { ColdDeviceStorage, defaultStore } from '@/store.js';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import MkVisitorDashboard from '@/components/MkVisitorDashboard.vue';
 import { mainRouter } from '@/router/main.js';
+import { DI } from '@/di.js';
 
 const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
 
@@ -87,7 +87,7 @@ const DESKTOP_THRESHOLD = 1100;
 
 const pageMetadata = ref<null | PageMetadata>(null);
 
-provide('router', mainRouter);
+provide(DI.router, mainRouter);
 provideMetadataReceiver((metadataGetter) => {
 	const info = metadataGetter();
 	pageMetadata.value = info;
@@ -106,7 +106,7 @@ const announcements = {
 	limit: 10,
 };
 
-const isTimelineAvailable = ref(instance.policies?.ltlAvailable || instance.policies?.gtlAvailable);
+const isTimelineAvailable = ref(instance.policies.ltlAvailable || instance.policies.gtlAvailable);
 
 const showMenu = ref(false);
 const isDesktop = ref(window.innerWidth >= DESKTOP_THRESHOLD);
@@ -114,10 +114,6 @@ const narrow = ref(window.innerWidth < 1280);
 
 const keymap = computed(() => {
 	return {
-		'd': () => {
-			if (ColdDeviceStorage.get('syncDeviceDarkMode')) return;
-			defaultStore.set('darkMode', !defaultStore.state.darkMode);
-		},
 		's': () => {
 			mainRouter.push('/search');
 		},
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index 2e31d056c1..99248abecf 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -23,12 +23,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { computed, provide, ref } from 'vue';
-import XCommon from './_common_/common.vue';
-import { provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
-import type { PageMetadata } from '@/scripts/page-metadata.js';
 import { instanceName, ui } from '@@/js/config.js';
+import XCommon from './_common_/common.vue';
+import type { PageMetadata } from '@/page.js';
+import { provideMetadataReceiver, provideReactiveMetadata } from '@/page.js';
 import { i18n } from '@/i18n.js';
 import { mainRouter } from '@/router/main.js';
+import { DI } from '@/di.js';
 
 const isRoot = computed(() => mainRouter.currentRoute.value.name === 'index');
 
@@ -36,7 +37,7 @@ const pageMetadata = ref<null | PageMetadata>(null);
 
 const showBottom = !(new URLSearchParams(location.search)).has('zen') && ui === 'deck';
 
-provide('router', mainRouter);
+provide(DI.router, mainRouter);
 provideMetadataReceiver((metadataGetter) => {
 	const info = metadataGetter();
 	pageMetadata.value = info;
diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/utility/achievements.ts
similarity index 99%
rename from packages/frontend/src/scripts/achievements.ts
rename to packages/frontend/src/utility/achievements.ts
index f5d0ab559f..3025a985ba 100644
--- a/packages/frontend/src/scripts/achievements.ts
+++ b/packages/frontend/src/utility/achievements.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { $i } from '@/account.js';
 
 export const ACHIEVEMENT_TYPES = [
diff --git a/packages/frontend/src/scripts/admin-lookup.ts b/packages/frontend/src/utility/admin-lookup.ts
similarity index 97%
rename from packages/frontend/src/scripts/admin-lookup.ts
rename to packages/frontend/src/utility/admin-lookup.ts
index 1b57b853c9..7405e229fe 100644
--- a/packages/frontend/src/scripts/admin-lookup.ts
+++ b/packages/frontend/src/utility/admin-lookup.ts
@@ -6,7 +6,7 @@
 import * as Misskey from 'misskey-js';
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 export async function lookupUser() {
 	const { canceled, result } = await os.inputText({
diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/utility/array.ts
similarity index 100%
rename from packages/frontend/src/scripts/array.ts
rename to packages/frontend/src/utility/array.ts
diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/utility/autocomplete.ts
similarity index 100%
rename from packages/frontend/src/scripts/autocomplete.ts
rename to packages/frontend/src/utility/autocomplete.ts
diff --git a/packages/frontend/src/scripts/autogen/settings-search-index.ts b/packages/frontend/src/utility/autogen/settings-search-index.ts
similarity index 92%
rename from packages/frontend/src/scripts/autogen/settings-search-index.ts
rename to packages/frontend/src/utility/autogen/settings-search-index.ts
index c62272b271..db4459bf06 100644
--- a/packages/frontend/src/scripts/autogen/settings-search-index.ts
+++ b/packages/frontend/src/utility/autogen/settings-search-index.ts
@@ -33,12 +33,12 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['light', 'theme'],
 			},
 			{
-				id: 'eLOwK5Ia2',
+				id: 'CsSVILKpX',
 				label: i18n.ts.themeForDarkMode,
 				keywords: ['dark', 'theme'],
 			},
 			{
-				id: 'ujvMfyzUr',
+				id: '8wcoRp76b',
 				label: i18n.ts.setWallpaper,
 				keywords: ['wallpaper'],
 			},
@@ -57,12 +57,12 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['mute'],
 			},
 			{
-				id: 'xy5OOBB4A',
+				id: 'oALW4ja7U',
 				label: i18n.ts.useSoundOnlyWhenActive,
 				keywords: ['active', 'mute'],
 			},
 			{
-				id: '9MxYVIf7k',
+				id: 'BbJK2SKT2',
 				label: i18n.ts.masterVolume,
 				keywords: ['volume', 'master'],
 			},
@@ -253,7 +253,7 @@ export const searchIndexes: SearchIndexItem[] = [
 						keywords: ['follower', i18n.ts._accountSettings.makeNotesFollowersOnlyBeforeDescription],
 					},
 					{
-						id: '2prkeWRSd',
+						id: 'ebJ9IUbik',
 						label: i18n.ts._accountSettings.makeNotesHiddenBefore,
 						keywords: ['hidden', i18n.ts._accountSettings.makeNotesHiddenBeforeDescription],
 					},
@@ -261,21 +261,6 @@ export const searchIndexes: SearchIndexItem[] = [
 				label: i18n.ts.lockdown,
 				keywords: ['lockdown'],
 			},
-			{
-				id: '37QLEyrtk',
-				label: i18n.ts.rememberNoteVisibility,
-				keywords: ['remember', 'keep', 'note', 'visibility'],
-			},
-			{
-				id: 'rhKwScbVS',
-				label: i18n.ts.defaultNoteVisibility,
-				keywords: ['default', 'note', 'visibility'],
-			},
-			{
-				id: '3EmXVyevo',
-				label: i18n.ts.keepCw,
-				keywords: ['remember', 'keep', 'note', 'cw'],
-			},
 		],
 		label: i18n.ts.privacy,
 		keywords: ['privacy'],
@@ -301,50 +286,60 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['post', 'form', 'timeline'],
 			},
 			{
-				id: '9ra14w32V',
+				id: 'snyCQ5oKE',
 				label: i18n.ts.showFixedPostFormInChannel,
 				keywords: ['post', 'form', 'timeline', 'channel'],
 			},
 			{
-				id: '84MdeDWL1',
+				id: '8j36S4Ev6',
 				label: i18n.ts.pinnedList,
 				keywords: ['pinned', 'list'],
 			},
 			{
-				id: 'fYdWhBbrN',
+				id: 'CWpyT9vLK',
 				label: i18n.ts.enableQuickAddMfmFunction,
 				keywords: ['mfm', 'enable', 'show', 'advanced', 'picker', 'form', 'function', 'fn'],
 			},
 			{
-				id: '4huRldNp5',
+				id: '1yhown1Xc',
+				label: i18n.ts.rememberNoteVisibility,
+				keywords: ['remember', 'keep', 'note', 'visibility'],
+			},
+			{
+				id: 'wUeAI5QBV',
+				label: i18n.ts.defaultNoteVisibility,
+				keywords: ['default', 'note', 'visibility'],
+			},
+			{
+				id: '6kMj4HVOg',
 				children: [
 					{
-						id: 'puIqj1a8b',
+						id: 'DQIcvf64G',
 						label: i18n.ts.collapseRenotes,
 						keywords: ['renote', i18n.ts.collapseRenotesDescription],
 					},
 					{
-						id: 'wqpOC22Zm',
+						id: 'igFN7RIUa',
 						label: i18n.ts.showNoteActionsOnlyHover,
 						keywords: ['hover', 'show', 'footer', 'action'],
 					},
 					{
-						id: 'cjfAtxMzP',
+						id: '9uxocbLO0',
 						label: i18n.ts.showClipButtonInNoteFooter,
 						keywords: ['footer', 'action', 'clip', 'show'],
 					},
 					{
-						id: 'khzxoCjtp',
+						id: 'eaT1O1Fao',
 						label: i18n.ts.enableAdvancedMfm,
 						keywords: ['mfm', 'enable', 'show', 'advanced'],
 					},
 					{
-						id: 'uJkoVjTmF',
+						id: 'omxZk3eET',
 						label: i18n.ts.showReactionsCount,
 						keywords: ['reaction', 'count', 'show'],
 					},
 					{
-						id: '9gTCaLkIf',
+						id: 'epvi2Nv2G',
 						label: i18n.ts.loadRawImages,
 						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'quality', 'raw', 'attachment'],
 					},
@@ -353,10 +348,10 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['note'],
 			},
 			{
-				id: '5G6O6qdis',
+				id: 'jb3HUeyrx',
 				children: [
 					{
-						id: 'sYTvqUbhP',
+						id: 'ykifk3NHS',
 						label: i18n.ts.useGroupedNotifications,
 						keywords: ['group'],
 					},
@@ -365,55 +360,60 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['notification'],
 			},
 			{
-				id: 'c3xhLyXZ5',
+				id: 'abEAdSpYY',
 				children: [
 					{
-						id: 'FbhoeuRAD',
+						id: 'lBbtAg0Hm',
 						label: i18n.ts.openImageInNewTab,
 						keywords: ['image', 'photo', 'picture', 'media', 'thumbnail', 'new', 'tab'],
 					},
 					{
-						id: 'qixh85g2N',
+						id: 'E9whefUtX',
 						label: i18n.ts.useReactionPickerForContextMenu,
 						keywords: ['reaction', 'picker', 'contextmenu', 'open'],
 					},
 					{
-						id: 'd2H4E5ys6',
+						id: 'iQaBbJBva',
 						label: i18n.ts.enableInfiniteScroll,
 						keywords: ['load', 'auto', 'more'],
 					},
 					{
-						id: 'jC7LtTnmc',
+						id: 'hgEVGgJa1',
 						label: i18n.ts.disableStreamingTimeline,
 						keywords: ['disable', 'streaming', 'timeline'],
 					},
 					{
-						id: '8xazEqlgZ',
+						id: 'yxehrHZ6x',
 						label: i18n.ts.alwaysConfirmFollow,
 						keywords: ['follow', 'confirm', 'always'],
 					},
 					{
-						id: 'wZqrDQZar',
+						id: 'DdoFLaSG8',
 						label: i18n.ts.confirmWhenRevealingSensitiveMedia,
 						keywords: ['sensitive', 'nsfw', 'media', 'image', 'photo', 'picture', 'attachment', 'confirm'],
 					},
 					{
-						id: '5QTUzrpT3',
+						id: 'uIMCIK7kG',
 						label: i18n.ts.confirmOnReact,
 						keywords: ['reaction', 'confirm'],
 					},
 					{
-						id: 'nygexkaUk',
+						id: 'zvM13vl26',
+						label: i18n.ts.keepCw,
+						keywords: ['remember', 'keep', 'note', 'cw'],
+					},
+					{
+						id: 'm75VEWI3S',
 						label: i18n.ts.whenServerDisconnected,
 						keywords: ['server', 'disconnect', 'reconnect', 'reload', 'streaming'],
 					},
 					{
-						id: 'whKYKvaQB',
+						id: 'bLO9vCyKW',
 						label: i18n.ts.numberOfPageCache,
 						keywords: ['cache', 'page'],
 					},
 					{
-						id: 'lBbtAg0Hm',
+						id: 'iQ7Er89l5',
 						label: i18n.ts.dataSaver,
 						keywords: ['datasaver'],
 					},
@@ -422,20 +422,20 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['behavior'],
 			},
 			{
-				id: 'y2v7CV9zs',
+				id: 'C2WYcVM1d',
 				children: [
 					{
-						id: 'k1qTdyfzM',
+						id: 'Cu7ErCM7C',
 						label: i18n.ts.forceShowAds,
 						keywords: ['ad', 'show'],
 					},
 					{
-						id: 'e9As4Us48',
+						id: 'BBxwy4F6E',
 						label: i18n.ts.hemisphere,
 						keywords: [],
 					},
 					{
-						id: 'zvM13vl26',
+						id: '9YdUwDC8d',
 						label: i18n.ts.additionalEmojiDictionary,
 						keywords: ['emoji', 'dictionary', 'additional', 'extra'],
 					},
@@ -449,6 +449,13 @@ export const searchIndexes: SearchIndexItem[] = [
 		path: '/settings/preferences',
 		icon: 'ti ti-adjustments',
 	},
+	{
+		id: 'mwkwtw83Y',
+		label: i18n.ts.plugins,
+		keywords: ['plugin'],
+		path: '/settings/plugin',
+		icon: 'ti ti-plug',
+	},
 	{
 		id: 'F1uK9ssiY',
 		children: [
@@ -626,17 +633,17 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['keep', 'original', 'raw', 'upload', i18n.ts.keepOriginalUploadingDescription],
 			},
 			{
-				id: 'oqUiI5w0s',
+				id: 'D8HUTGWE1',
 				label: i18n.ts.keepOriginalFilename,
 				keywords: ['keep', 'original', 'filename', i18n.ts.keepOriginalFilenameDescription],
 			},
 			{
-				id: 'Aszkikq9n',
+				id: '6xAvsWSZi',
 				label: i18n.ts.alwaysMarkSensitive,
 				keywords: ['always', 'default', 'mark', 'nsfw', 'sensitive', 'media', 'file'],
 			},
 			{
-				id: 'iGlVjsfVj',
+				id: 'csNNPF1KX',
 				label: i18n.ts.enableAutoSensitive,
 				keywords: ['auto', 'nsfw', 'sensitive', 'media', 'file', i18n.ts.enableAutoSensitiveDescription],
 			},
@@ -662,80 +669,80 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['blur'],
 			},
 			{
-				id: 'vbZvyLDC1',
+				id: 'C05WQNSIJ',
 				label: i18n.ts.useBlurEffectForModal,
 				keywords: ['blur', 'modal'],
 			},
 			{
-				id: '6fLNMTwNt',
+				id: 'snVKNr7Bw',
 				label: i18n.ts.highlightSensitiveMedia,
 				keywords: ['highlight', 'sensitive', 'nsfw', 'image', 'photo', 'picture', 'media', 'thumbnail'],
 			},
 			{
-				id: 'hhvF8Z4pF',
+				id: 'DsS2CwjYE',
 				label: i18n.ts.squareAvatars,
 				keywords: ['avatar', 'icon', 'square'],
 			},
 			{
-				id: 'DsS2CwjYE',
+				id: 'xCcTDl651',
 				label: i18n.ts.showAvatarDecorations,
 				keywords: ['avatar', 'icon', 'decoration', 'show'],
 			},
 			{
-				id: 'pWZ0ypy2g',
+				id: '3dHw723VD',
 				label: i18n.ts.showGapBetweenNotesInTimeline,
 				keywords: ['note', 'timeline', 'gap'],
 			},
 			{
-				id: 'AfRMcC6IM',
-				label: i18n.ts.useSystemFont,
-				keywords: ['font', 'system', 'native'],
-			},
-			{
-				id: 'jD0qbxlzN',
+				id: 'AWi72xbrl',
 				label: i18n.ts.seasonalScreenEffect,
 				keywords: ['effect', 'show'],
 			},
 			{
-				id: 'EdYo3hOK',
+				id: 'Ces8FsJws',
 				label: i18n.ts.menuStyle,
 				keywords: ['menu', 'style', 'popup', 'drawer'],
 			},
 			{
-				id: '9mSlX0EkD',
+				id: 'wDr9xSXCv',
 				label: i18n.ts.emojiStyle,
 				keywords: ['emoji', 'style', 'native', 'system', 'fluent', 'twemoji'],
 			},
 			{
-				id: '44UmMwmUe',
+				id: 'vFB0pLzck',
 				label: i18n.ts.fontSize,
 				keywords: ['font', 'size'],
 			},
 			{
-				id: 'vFB0pLzck',
+				id: '23BhvYXPC',
+				label: i18n.ts.useSystemFont,
+				keywords: ['font', 'system', 'native'],
+			},
+			{
+				id: 'EeNLndAOa',
 				children: [
 					{
-						id: 'pc7IpPEU4',
+						id: 'rAAPoaodS',
 						label: i18n.ts.reactionsDisplaySize,
 						keywords: ['reaction', 'size', 'scale', 'display'],
 					},
 					{
-						id: 'siOW5aSwp',
+						id: 'qTLAvNWsc',
 						label: i18n.ts.limitWidthOfReaction,
 						keywords: ['reaction', 'size', 'scale', 'display', 'width', 'limit'],
 					},
 					{
-						id: 'dDUvhk13F',
+						id: '2lWgzAm13',
 						label: i18n.ts.mediaListWithOneImageAppearance,
 						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'list', 'size', 'height'],
 					},
 					{
-						id: 'CLxNL1Rp0',
+						id: 'EU7HbxOR5',
 						label: i18n.ts.instanceTicker,
 						keywords: ['ticker', 'information', 'label', 'instance', 'server', 'host', 'federation'],
 					},
 					{
-						id: 'dP2KWDYzD',
+						id: 'AEtM0FAp1',
 						label: i18n.ts.displayOfSensitiveMedia,
 						keywords: ['attachment', 'image', 'photo', 'picture', 'media', 'thumbnail', 'nsfw', 'sensitive', 'display', 'show', 'hide', 'visibility'],
 					},
@@ -744,15 +751,15 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['note', 'display'],
 			},
 			{
-				id: 'dVOzi22IW',
+				id: 'A1FMC2Zon',
 				children: [
 					{
-						id: 'aoF4ufUwn',
+						id: 'CB37G5ZDo',
 						label: i18n.ts.position,
 						keywords: ['position'],
 					},
 					{
-						id: 'sKK2XSS69',
+						id: 'gGS2i19hS',
 						label: i18n.ts.stackAxis,
 						keywords: ['stack', 'axis', 'direction'],
 					},
@@ -775,32 +782,32 @@ export const searchIndexes: SearchIndexItem[] = [
 				keywords: ['animation', 'motion', 'reduce'],
 			},
 			{
-				id: 'RhYwm8At',
+				id: 'cXr3tFdpa',
 				label: i18n.ts.disableShowingAnimatedImages,
 				keywords: ['disable', 'animation', 'image', 'photo', 'picture', 'media', 'thumbnail', 'gif'],
 			},
 			{
-				id: '5mZxz2cru',
+				id: 'Ok1UBwtP',
 				label: i18n.ts.enableAnimatedMfm,
 				keywords: ['mfm', 'enable', 'show', 'animated'],
 			},
 			{
-				id: 'bgjamYEis',
+				id: 'yPEpJigqY',
 				label: i18n.ts.enableHorizontalSwipe,
 				keywords: ['swipe', 'horizontal', 'tab'],
 			},
 			{
-				id: 'yPEpJigqY',
+				id: 'h7iZtdTU3',
 				label: i18n.ts.keepScreenOn,
 				keywords: ['keep', 'screen', 'display', 'on'],
 			},
 			{
-				id: 'oxwiGKMu0',
+				id: 'gP1BY3PDy',
 				label: i18n.ts.useNativeUIForVideoAudioPlayer,
 				keywords: ['native', 'system', 'video', 'audio', 'player', 'media'],
 			},
 			{
-				id: 'n90tffyiU',
+				id: 'jnMK3M6rs',
 				label: i18n.ts._contextMenu.title,
 				keywords: ['contextmenu', 'system', 'native'],
 			},
diff --git a/packages/frontend/src/utility/autospacing.ts b/packages/frontend/src/utility/autospacing.ts
new file mode 100644
index 0000000000..e95f2e21f5
--- /dev/null
+++ b/packages/frontend/src/utility/autospacing.ts
@@ -0,0 +1,86 @@
+import * as misskey from 'misskey-js';
+import { store } from '@/store.js';
+
+const NO_SPACEING_LIST = [
+	'A股',
+	'B股',
+	'H股',
+	'SIM卡',
+	'PC端',
+	'T恤',
+	'A站',
+	'B站',
+	'C站',
+	'N卡',
+	'A卡',
+	'UP主',
+	'X光',
+	'B超',
+	'Q弹',
+];
+
+const LIST_WINDOW = NO_SPACEING_LIST.reduce((a, b) => Math.max(a, b.length), 0) + 1;
+
+const hashtagMap = new Map<string, string>();
+let placeholderCounter = 0;
+
+function preserveHashtags(text: string): string {
+	placeholderCounter = 0;
+	hashtagMap.clear();
+
+	return text.replace(/#[^\s]+/g, (match) => {
+		const placeholder = `__HASHTAG_${placeholderCounter}__`;
+		hashtagMap.set(placeholder, match);
+		placeholderCounter++;
+		return placeholder;
+	});
+}
+
+function restoreHashtags(text: string): string {
+	let result = text;
+	for (const [placeholder, hashtag] of hashtagMap) {
+		result = result.replace(placeholder, hashtag);
+	}
+	return result;
+}
+
+export function autoSpacing(plainText: string) {
+	if (store.r.autoSpacingBehaviour.value == null) return plainText;
+
+	const textWithPlaceholders = preserveHashtags(plainText);
+
+	const rep = (matched: string, c1: string, c2: string, position: number) => {
+		if (store.r.autoSpacingBehaviour.value === 'all') return `${c1} ${c2}`;
+		const context = plainText
+			.slice(Math.max(0, position - LIST_WINDOW), position + LIST_WINDOW)
+			.toUpperCase();
+		if (NO_SPACEING_LIST.some((text) => context.includes(text))) {
+			return matched;
+		} else {
+			return `${c1} ${c2}`;
+		}
+	};
+
+	const spacedText = textWithPlaceholders
+		.replace(/([\u4e00-\u9fa5\u0800-\u4e00\uac00-\ud7ff])([a-zA-Z0-9])/g, rep)
+		.replace(/([a-zA-Z0-9,\.:])([\u4e00-\u9fa5\u0800-\u4e00\uac00-\ud7ff])/g, rep);
+
+	return restoreHashtags(spacedText);
+}
+
+export function spacingNote(note: misskey.entities.Note) {
+	const noteAsRecord = note as unknown as Record<string, string | null | undefined>;
+	if (!noteAsRecord.__autospacing_raw_text) {
+		noteAsRecord.__autospacing_raw_text = note.text;
+	}
+	if (!noteAsRecord.__autospacing_raw_cw) {
+		noteAsRecord.__autospacing_raw_cw = note.cw;
+	}
+	note.text = noteAsRecord.__autospacing_raw_text
+		? autoSpacing(noteAsRecord.__autospacing_raw_text)
+		: null;
+	note.cw = noteAsRecord.__autospacing_raw_cw
+		? autoSpacing(noteAsRecord.__autospacing_raw_cw)
+		: null;
+	return note;
+}
diff --git a/packages/frontend/src/scripts/cache.ts b/packages/frontend/src/utility/cache.ts
similarity index 100%
rename from packages/frontend/src/scripts/cache.ts
rename to packages/frontend/src/utility/cache.ts
diff --git a/packages/frontend/src/scripts/chart-legend.ts b/packages/frontend/src/utility/chart-legend.ts
similarity index 100%
rename from packages/frontend/src/scripts/chart-legend.ts
rename to packages/frontend/src/utility/chart-legend.ts
diff --git a/packages/frontend/src/scripts/chart-vline.ts b/packages/frontend/src/utility/chart-vline.ts
similarity index 100%
rename from packages/frontend/src/scripts/chart-vline.ts
rename to packages/frontend/src/utility/chart-vline.ts
diff --git a/packages/frontend/src/scripts/check-permissions.ts b/packages/frontend/src/utility/check-permissions.ts
similarity index 100%
rename from packages/frontend/src/scripts/check-permissions.ts
rename to packages/frontend/src/utility/check-permissions.ts
diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/utility/check-reaction-permissions.ts
similarity index 100%
rename from packages/frontend/src/scripts/check-reaction-permissions.ts
rename to packages/frontend/src/utility/check-reaction-permissions.ts
diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/utility/check-word-mute.ts
similarity index 100%
rename from packages/frontend/src/scripts/check-word-mute.ts
rename to packages/frontend/src/utility/check-word-mute.ts
diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/utility/clear-cache.ts
similarity index 92%
rename from packages/frontend/src/scripts/clear-cache.ts
rename to packages/frontend/src/utility/clear-cache.ts
index 71d1232710..b6ae254727 100644
--- a/packages/frontend/src/scripts/clear-cache.ts
+++ b/packages/frontend/src/utility/clear-cache.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 import * as os from '@/os.js';
 import { miLocalStorage } from '@/local-storage.js';
 import { fetchCustomEmojis } from '@/custom-emojis.js';
diff --git a/packages/frontend/src/scripts/clicker-game.ts b/packages/frontend/src/utility/clicker-game.ts
similarity index 96%
rename from packages/frontend/src/scripts/clicker-game.ts
rename to packages/frontend/src/utility/clicker-game.ts
index f9c4bc1829..0544be7757 100644
--- a/packages/frontend/src/scripts/clicker-game.ts
+++ b/packages/frontend/src/utility/clicker-game.ts
@@ -4,7 +4,7 @@
  */
 
 import { ref, computed } from 'vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 
 type SaveData = {
 	gameVersion: number;
diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/utility/clone.ts
similarity index 100%
rename from packages/frontend/src/scripts/clone.ts
rename to packages/frontend/src/utility/clone.ts
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/utility/code-highlighter.ts
similarity index 78%
rename from packages/frontend/src/scripts/code-highlighter.ts
rename to packages/frontend/src/utility/code-highlighter.ts
index 4d57dcd944..4f2aff9d4c 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/utility/code-highlighter.ts
@@ -10,18 +10,20 @@ import { bundledThemesInfo } from 'shiki/themes';
 import { bundledLanguagesInfo } from 'shiki/langs';
 import lightTheme from '@@/themes/_light.json5';
 import darkTheme from '@@/themes/_dark.json5';
+import defaultLightTheme from '@@/themes/l-light.json5';
+import defaultDarkTheme from '@@/themes/d-green-lime.json5';
 import { unique } from './array.js';
 import { deepClone } from './clone.js';
 import { deepMerge } from './merge.js';
 import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core';
-import { ColdDeviceStorage } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 let _highlighter: HighlighterCore | null = null;
 
 export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>;
 export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>;
 export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> {
-	const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme'));
+	const theme = deepClone(mode === 'light' ? prefer.s.lightTheme ?? defaultLightTheme : prefer.s.darkTheme ?? defaultDarkTheme);
 
 	if (theme.base) {
 		const base = [lightTheme, darkTheme].find(x => x.id === theme.base);
@@ -77,19 +79,19 @@ async function initHighlighter() {
 		],
 	});
 
-	ColdDeviceStorage.watch('lightTheme', async () => {
-		const newTheme = await getTheme('light');
-		if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
-			highlighter.loadTheme(newTheme);
-		}
-	});
-
-	ColdDeviceStorage.watch('darkTheme', async () => {
-		const newTheme = await getTheme('dark');
-		if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
-			highlighter.loadTheme(newTheme);
-		}
-	});
+	// TODO
+	//watch('lightTheme', async () => {
+	//	const newTheme = await getTheme('light');
+	//	if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
+	//		highlighter.loadTheme(newTheme);
+	//	}
+	//});
+	//watch('darkTheme', async () => {
+	//	const newTheme = await getTheme('dark');
+	//	if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) {
+	//		highlighter.loadTheme(newTheme);
+	//	}
+	//});
 
 	_highlighter = highlighter;
 
diff --git a/packages/frontend/src/scripts/collect-page-vars.ts b/packages/frontend/src/utility/collect-page-vars.ts
similarity index 100%
rename from packages/frontend/src/scripts/collect-page-vars.ts
rename to packages/frontend/src/utility/collect-page-vars.ts
diff --git a/packages/frontend/src/scripts/color.ts b/packages/frontend/src/utility/color.ts
similarity index 100%
rename from packages/frontend/src/scripts/color.ts
rename to packages/frontend/src/utility/color.ts
diff --git a/packages/frontend/src/scripts/confetti.ts b/packages/frontend/src/utility/confetti.ts
similarity index 100%
rename from packages/frontend/src/scripts/confetti.ts
rename to packages/frontend/src/utility/confetti.ts
diff --git a/packages/frontend/src/scripts/contains.ts b/packages/frontend/src/utility/contains.ts
similarity index 100%
rename from packages/frontend/src/scripts/contains.ts
rename to packages/frontend/src/utility/contains.ts
diff --git a/packages/frontend/src/scripts/copy-to-clipboard.ts b/packages/frontend/src/utility/copy-to-clipboard.ts
similarity index 100%
rename from packages/frontend/src/scripts/copy-to-clipboard.ts
rename to packages/frontend/src/utility/copy-to-clipboard.ts
diff --git a/packages/frontend/src/scripts/device-kind.ts b/packages/frontend/src/utility/device-kind.ts
similarity index 100%
rename from packages/frontend/src/scripts/device-kind.ts
rename to packages/frontend/src/utility/device-kind.ts
diff --git a/packages/frontend/src/scripts/emoji-picker.ts b/packages/frontend/src/utility/emoji-picker.ts
similarity index 94%
rename from packages/frontend/src/scripts/emoji-picker.ts
rename to packages/frontend/src/utility/emoji-picker.ts
index e704b5fd6f..e7275b86f2 100644
--- a/packages/frontend/src/scripts/emoji-picker.ts
+++ b/packages/frontend/src/utility/emoji-picker.ts
@@ -6,7 +6,7 @@
 import { defineAsyncComponent, ref } from 'vue';
 import type { Ref } from 'vue';
 import { popup } from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 /**
  * 絵文字ピッカーを表示する。
@@ -25,7 +25,7 @@ class EmojiPicker {
 	}
 
 	public async init() {
-		const emojisRef = defaultStore.reactiveState.pinnedEmojis;
+		const emojisRef = store.r.pinnedEmojis;
 		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
 			src: this.src,
 			pinnedEmojis: emojisRef,
diff --git a/packages/frontend/src/scripts/extract-mentions.ts b/packages/frontend/src/utility/extract-mentions.ts
similarity index 100%
rename from packages/frontend/src/scripts/extract-mentions.ts
rename to packages/frontend/src/utility/extract-mentions.ts
diff --git a/packages/frontend/src/scripts/extract-url-from-mfm.ts b/packages/frontend/src/utility/extract-url-from-mfm.ts
similarity index 94%
rename from packages/frontend/src/scripts/extract-url-from-mfm.ts
rename to packages/frontend/src/utility/extract-url-from-mfm.ts
index d5654ba850..570823d5b5 100644
--- a/packages/frontend/src/scripts/extract-url-from-mfm.ts
+++ b/packages/frontend/src/utility/extract-url-from-mfm.ts
@@ -4,7 +4,7 @@
  */
 
 import * as mfm from 'mfm-js';
-import { unique } from '@/scripts/array.js';
+import { unique } from '@/utility/array.js';
 
 // unique without hash
 // [ http://a/#1, http://a/#2, http://b/#3 ] => [ http://a/#1, http://b/#3 ]
diff --git a/packages/frontend/src/scripts/file-drop.ts b/packages/frontend/src/utility/file-drop.ts
similarity index 100%
rename from packages/frontend/src/scripts/file-drop.ts
rename to packages/frontend/src/utility/file-drop.ts
diff --git a/packages/frontend/src/scripts/focus-trap.ts b/packages/frontend/src/utility/focus-trap.ts
similarity index 98%
rename from packages/frontend/src/scripts/focus-trap.ts
rename to packages/frontend/src/utility/focus-trap.ts
index fb7caea830..fd17fa38a0 100644
--- a/packages/frontend/src/scripts/focus-trap.ts
+++ b/packages/frontend/src/utility/focus-trap.ts
@@ -2,7 +2,7 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
-import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
+import { getHTMLElementOrNull } from '@/utility/get-dom-node-or-null.js';
 
 const focusTrapElements = new Set<HTMLElement>();
 const ignoreElements = [
diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/utility/focus.ts
similarity index 97%
rename from packages/frontend/src/scripts/focus.ts
rename to packages/frontend/src/utility/focus.ts
index 81278b17ea..e3fd928d1d 100644
--- a/packages/frontend/src/scripts/focus.ts
+++ b/packages/frontend/src/utility/focus.ts
@@ -4,7 +4,7 @@
  */
 
 import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js';
-import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js';
+import { getElementOrNull, getNodeOrNull } from '@/utility/get-dom-node-or-null.js';
 
 type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement;
 
diff --git a/packages/frontend/src/scripts/font-settings.ts b/packages/frontend/src/utility/font-settings.ts
similarity index 100%
rename from packages/frontend/src/scripts/font-settings.ts
rename to packages/frontend/src/utility/font-settings.ts
diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/utility/form.ts
similarity index 100%
rename from packages/frontend/src/scripts/form.ts
rename to packages/frontend/src/utility/form.ts
diff --git a/packages/frontend/src/scripts/format-time-string.ts b/packages/frontend/src/utility/format-time-string.ts
similarity index 100%
rename from packages/frontend/src/scripts/format-time-string.ts
rename to packages/frontend/src/utility/format-time-string.ts
diff --git a/packages/frontend/src/scripts/fullscreen.ts b/packages/frontend/src/utility/fullscreen.ts
similarity index 100%
rename from packages/frontend/src/scripts/fullscreen.ts
rename to packages/frontend/src/utility/fullscreen.ts
diff --git a/packages/frontend/src/scripts/get-account-from-id.ts b/packages/frontend/src/utility/get-account-from-id.ts
similarity index 88%
rename from packages/frontend/src/scripts/get-account-from-id.ts
rename to packages/frontend/src/utility/get-account-from-id.ts
index 40afa10f2d..5d9662a747 100644
--- a/packages/frontend/src/scripts/get-account-from-id.ts
+++ b/packages/frontend/src/utility/get-account-from-id.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { get } from '@/scripts/idb-proxy.js';
+import { get } from '@/utility/idb-proxy.js';
 
 export async function getAccountFromId(id: string) {
 	const accounts = await get('accounts') as { token: string; id: string; }[];
diff --git a/packages/frontend/src/scripts/get-appear-note.ts b/packages/frontend/src/utility/get-appear-note.ts
similarity index 100%
rename from packages/frontend/src/scripts/get-appear-note.ts
rename to packages/frontend/src/utility/get-appear-note.ts
diff --git a/packages/frontend/src/scripts/get-bg-color.ts b/packages/frontend/src/utility/get-bg-color.ts
similarity index 100%
rename from packages/frontend/src/scripts/get-bg-color.ts
rename to packages/frontend/src/utility/get-bg-color.ts
diff --git a/packages/frontend/src/scripts/get-dom-node-or-null.ts b/packages/frontend/src/utility/get-dom-node-or-null.ts
similarity index 100%
rename from packages/frontend/src/scripts/get-dom-node-or-null.ts
rename to packages/frontend/src/utility/get-dom-node-or-null.ts
diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/utility/get-drive-file-menu.ts
similarity index 94%
rename from packages/frontend/src/scripts/get-drive-file-menu.ts
rename to packages/frontend/src/utility/get-drive-file-menu.ts
index c8ab9238d3..477043a342 100644
--- a/packages/frontend/src/scripts/get-drive-file-menu.ts
+++ b/packages/frontend/src/utility/get-drive-file-menu.ts
@@ -5,12 +5,12 @@
 
 import * as Misskey from 'misskey-js';
 import { defineAsyncComponent } from 'vue';
-import { i18n } from '@/i18n.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
 import type { MenuItem } from '@/types/menu.js';
-import { defaultStore } from '@/store.js';
+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 { prefer } from '@/preferences.js';
 
 function rename(file: Misskey.entities.DriveFile) {
 	os.inputText({
@@ -148,7 +148,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss
 		action: () => deleteFile(file),
 	});
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyFileId,
diff --git a/packages/frontend/src/scripts/get-embed-code.ts b/packages/frontend/src/utility/get-embed-code.ts
similarity index 97%
rename from packages/frontend/src/scripts/get-embed-code.ts
rename to packages/frontend/src/utility/get-embed-code.ts
index 158ab9c7f8..9021520da8 100644
--- a/packages/frontend/src/scripts/get-embed-code.ts
+++ b/packages/frontend/src/utility/get-embed-code.ts
@@ -7,7 +7,7 @@ import { v4 as uuid } from 'uuid';
 import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js';
 import { url } from '@@/js/config.js';
 import * as os from '@/os.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js';
 
 const MOBILE_THRESHOLD = 500;
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/utility/get-note-menu.ts
similarity index 95%
rename from packages/frontend/src/scripts/get-note-menu.ts
rename to packages/frontend/src/utility/get-note-menu.ts
index f63b7a2584..069f4c7e0b 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/utility/get-note-menu.ts
@@ -4,25 +4,27 @@
  */
 
 import { defineAsyncComponent } from 'vue';
-import type { Ref, ShallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 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 { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import { defaultStore, noteActions } from '@/store.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
+import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
+import { store } from '@/store.js';
 import { miLocalStorage } from '@/local-storage.js';
-import { getUserMenu } from '@/scripts/get-user-menu.js';
+import { getUserMenu } from '@/utility/get-user-menu.js';
 import { clipsCache, favoritedChannelsCache } from '@/cache.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
-import { isSupportShare } from '@/scripts/navigator.js';
-import { getAppearNote } from '@/scripts/get-appear-note.js';
-import { genEmbedCode } from '@/scripts/get-embed-code.js';
+import { isSupportShare } from '@/utility/navigator.js';
+import { getAppearNote } from '@/utility/get-appear-note.js';
+import { genEmbedCode } from '@/utility/get-embed-code.js';
+import { prefer } from '@/preferences.js';
+import { getPluginHandlers } from '@/plugin.js';
 
 export async function getNoteClipMenu(props: {
 	note: Misskey.entities.Note;
@@ -508,6 +510,7 @@ export function getNoteMenu(props: {
 		}
 	}
 
+	const noteActions = getPluginHandlers('note_action');
 	if (noteActions.length > 0) {
 		menuItems.push({ type: 'divider' });
 
@@ -520,7 +523,7 @@ export function getNoteMenu(props: {
 		})));
 	}
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyNoteId,
@@ -571,7 +574,7 @@ export function getRenoteMenu(props: {
 			icon: 'ti ti-repeat',
 			action: () => {
 				const el = props.renoteButton.value;
-				if (el && defaultStore.state.animation) {
+				if (el && prefer.s.animation) {
 					const rect = el.getBoundingClientRect();
 					const x = rect.left + (el.offsetWidth / 2);
 					const y = rect.top + (el.offsetHeight / 2);
@@ -609,7 +612,7 @@ export function getRenoteMenu(props: {
 			icon: 'ti ti-repeat',
 			action: () => {
 				const el = props.renoteButton.value;
-				if (el && defaultStore.state.animation) {
+				if (el && prefer.s.animation) {
 					const rect = el.getBoundingClientRect();
 					const x = rect.left + (el.offsetWidth / 2);
 					const y = rect.top + (el.offsetHeight / 2);
@@ -618,8 +621,8 @@ export function getRenoteMenu(props: {
 					});
 				}
 
-				const configuredVisibility = defaultStore.state.rememberNoteVisibility ? defaultStore.state.visibility : defaultStore.state.defaultNoteVisibility;
-				const localOnly = defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly;
+				const configuredVisibility = prefer.s.rememberNoteVisibility ? store.s.visibility : prefer.s.defaultNoteVisibility;
+				const localOnly = prefer.s.rememberNoteVisibility ? store.s.localOnly : prefer.s.defaultNoteLocalOnly;
 
 				let visibility = appearNote.visibility;
 				visibility = smallerVisibility(visibility, configuredVisibility);
@@ -660,7 +663,7 @@ export function getRenoteMenu(props: {
 					text: channel.name,
 					action: () => {
 						const el = props.renoteButton.value;
-						if (el && defaultStore.state.animation) {
+						if (el && prefer.s.animation) {
 							const rect = el.getBoundingClientRect();
 							const x = rect.left + (el.offsetWidth / 2);
 							const y = rect.top + (el.offsetHeight / 2);
diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/utility/get-note-summary.ts
similarity index 100%
rename from packages/frontend/src/scripts/get-note-summary.ts
rename to packages/frontend/src/utility/get-note-summary.ts
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/utility/get-user-menu.ts
similarity index 96%
rename from packages/frontend/src/scripts/get-user-menu.ts
rename to packages/frontend/src/utility/get-user-menu.ts
index 8f7c3ba3be..1b9b0eac95 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/utility/get-user-menu.ts
@@ -6,19 +6,20 @@
 import { toUnicode } from 'punycode.js';
 import { defineAsyncComponent, ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
-import { i18n } from '@/i18n.js';
-import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 import { host, url } from '@@/js/config.js';
-import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
-import { defaultStore, userActions } from '@/store.js';
-import { $i, iAmModerator } from '@/account.js';
-import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-permissions.js';
 import type { IRouter } from '@/nirax.js';
+import type { MenuItem } from '@/types/menu.js';
+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 { notesSearchAvailable, canSearchNonLocalNotes } from '@/utility/check-permissions.js';
 import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
 import { mainRouter } from '@/router/main.js';
-import { genEmbedCode } from '@/scripts/get-embed-code.js';
-import type { MenuItem } from '@/types/menu.js';
+import { genEmbedCode } from '@/utility/get-embed-code.js';
+import { prefer } from '@/preferences.js';
+import { getPluginHandlers } from '@/plugin.js';
 
 export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
 	const meId = $i ? $i.id : null;
@@ -251,7 +252,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 								listId: list.id,
 								userId: user.id,
 							}).then(() => {
-								list.userIds?.splice(list.userIds?.indexOf(user.id), 1);
+								list.userIds?.splice(list.userIds.indexOf(user.id), 1);
 							});
 						}
 					}));
@@ -398,7 +399,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 		});
 	}
 
-	if (defaultStore.state.devMode) {
+	if (prefer.s.devMode) {
 		menuItems.push({ type: 'divider' }, {
 			icon: 'ti ti-id',
 			text: i18n.ts.copyUserId,
@@ -418,6 +419,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
 		});
 	}
 
+	const userActions = getPluginHandlers('user_action');
 	if (userActions.length > 0) {
 		menuItems.push({ type: 'divider' }, ...userActions.map(action => ({
 			icon: 'ti ti-plug',
diff --git a/packages/frontend/src/scripts/get-user-name.ts b/packages/frontend/src/utility/get-user-name.ts
similarity index 100%
rename from packages/frontend/src/scripts/get-user-name.ts
rename to packages/frontend/src/utility/get-user-name.ts
diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/utility/hotkey.ts
similarity index 98%
rename from packages/frontend/src/scripts/hotkey.ts
rename to packages/frontend/src/utility/hotkey.ts
index 04fb235694..fe62139a74 100644
--- a/packages/frontend/src/scripts/hotkey.ts
+++ b/packages/frontend/src/utility/hotkey.ts
@@ -2,7 +2,7 @@
  * SPDX-FileCopyrightText: syuilo and misskey-project
  * SPDX-License-Identifier: AGPL-3.0-only
  */
-import { getHTMLElementOrNull } from "@/scripts/get-dom-node-or-null.js";
+import { getHTMLElementOrNull } from "@/utility/get-dom-node-or-null.js";
 
 //#region types
 export type Keymap = Record<string, CallbackFunction | CallbackObject>;
diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/utility/idb-proxy.ts
similarity index 100%
rename from packages/frontend/src/scripts/idb-proxy.ts
rename to packages/frontend/src/utility/idb-proxy.ts
diff --git a/packages/frontend/src/scripts/idle-render.ts b/packages/frontend/src/utility/idle-render.ts
similarity index 100%
rename from packages/frontend/src/scripts/idle-render.ts
rename to packages/frontend/src/utility/idle-render.ts
diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/utility/init-chart.ts
similarity index 87%
rename from packages/frontend/src/scripts/init-chart.ts
rename to packages/frontend/src/utility/init-chart.ts
index 41e1636aa7..9775b9fec4 100644
--- a/packages/frontend/src/scripts/init-chart.ts
+++ b/packages/frontend/src/utility/init-chart.ts
@@ -24,7 +24,7 @@ import {
 import gradient from 'chartjs-plugin-gradient';
 import zoomPlugin from 'chartjs-plugin-zoom';
 import { MatrixController, MatrixElement } from 'chartjs-chart-matrix';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import 'chartjs-adapter-date-fns';
 
 export function initChart() {
@@ -52,7 +52,7 @@ export function initChart() {
 	// フォントカラー
 	Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg');
 
-	Chart.defaults.borderColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
+	Chart.defaults.borderColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
 
 	Chart.defaults.animation = false;
 }
diff --git a/packages/frontend/src/scripts/initialize-sw.ts b/packages/frontend/src/utility/initialize-sw.ts
similarity index 100%
rename from packages/frontend/src/scripts/initialize-sw.ts
rename to packages/frontend/src/utility/initialize-sw.ts
diff --git a/packages/frontend/src/scripts/intl-const.ts b/packages/frontend/src/utility/intl-const.ts
similarity index 100%
rename from packages/frontend/src/scripts/intl-const.ts
rename to packages/frontend/src/utility/intl-const.ts
diff --git a/packages/frontend/src/utility/intl-string.ts b/packages/frontend/src/utility/intl-string.ts
new file mode 100644
index 0000000000..a5b5bbb592
--- /dev/null
+++ b/packages/frontend/src/utility/intl-string.ts
@@ -0,0 +1,97 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { versatileLang } from '@@/js/intl-const.js';
+import type { toHiragana as toHiraganaType } from 'wanakana';
+
+let toHiragana: typeof toHiraganaType = (str?: string) => str ?? '';
+let isWanakanaLoaded = false;
+
+/** 
+ * ローマ字変換のセットアップ(日本語以外の環境で読み込まないのでlazy-loading)
+ * 
+ * ここの比較系関数を使う際は事前に呼び出す必要がある
+ */
+export async function initIntlString(forceWanakana = false) {
+	if ((!versatileLang.includes('ja') && !forceWanakana) || isWanakanaLoaded) return;
+	const { toHiragana: _toHiragana } = await import('wanakana');
+	toHiragana = _toHiragana;
+	isWanakanaLoaded = true;
+}
+
+/**
+ * - 全角英数字を半角に
+ * - 半角カタカナを全角に
+ * - 濁点・半濁点がリガチャになっている(例: `か` + `゛` )ひらがな・カタカナを結合
+ * - 異体字を正規化
+ * - 小文字に揃える
+ * - 文字列のトリム
+ */
+export function normalizeString(str: string) {
+	const segmenter = new Intl.Segmenter(versatileLang, { granularity: 'grapheme' });
+	return [...segmenter.segment(str)].map(({ segment }) => segment.normalize('NFKC')).join('').toLowerCase().trim();
+}
+
+// https://qiita.com/non-caffeine/items/77360dda05c8ce510084
+const hyphens = [
+	0x002d, // hyphen-minus
+	0x02d7, // modifier letter minus sign
+	0x1173, // hangul jongseong eu
+	0x1680, // ogham space mark
+	0x1b78, // balinese musical symbol left-hand open pang
+	0x2010, // hyphen
+	0x2011, // non-breaking hyphen
+	0x2012, // figure dash
+	0x2013, // en dash
+	0x2014, // em dash
+	0x2015, // horizontal bar
+	0x2043, // hyphen bullet
+	0x207b, // superscript minus
+	0x2212, // minus sign
+	0x25ac, // black rectangle
+	0x2500, // box drawings light horizontal
+	0x2501, // box drawings heavy horizontal
+	0x2796, // heavy minus sign
+	0x30fc, // katakana-hiragana prolonged sound mark
+	0x3161, // hangul letter eu
+	0xfe58, // small em dash
+	0xfe63, // small hyphen-minus
+	0xff0d, // fullwidth hyphen-minus
+	0xff70, // halfwidth katakana-hiragana prolonged sound mark
+	0x10110, // aegean number ten
+	0x10191, // roman uncia sign
+];
+
+const hyphensCodePoints = hyphens.map(code => `\\u{${code.toString(16).padStart(4, '0')}}`);
+
+/** ハイフンを統一(ローマ字半角入力時に`ー`と`-`が判定できない問題の調整) */
+export function normalizeHyphens(str: string) {
+	return str.replace(new RegExp(`[${hyphensCodePoints.join('')}]`, 'ug'), '\u002d');
+}
+
+/**
+ * `normalizeString` に加えて、カタカナ・ローマ字をひらがなに揃え、ハイフンを統一
+ *
+ * (ローマ字じゃないものもローマ字として認識され変換されるので、文字列比較の際は `normalizeString` を併用する必要あり)
+ */
+export function normalizeStringWithHiragana(str: string) {
+	return normalizeHyphens(toHiragana(normalizeString(str), { convertLongVowelMark: false }));
+}
+
+/** aとbが同じかどうか */
+export function compareStringEquals(a: string, b: string) {
+	return (
+		normalizeString(a) === normalizeString(b) ||
+		normalizeStringWithHiragana(a) === normalizeStringWithHiragana(b)
+	);
+}
+
+/** baseにqueryが含まれているかどうか */
+export function compareStringIncludes(base: string, query: string) {
+	return (
+		normalizeString(base).includes(normalizeString(query)) ||
+		normalizeStringWithHiragana(base).includes(normalizeStringWithHiragana(query))
+	);
+}
diff --git a/packages/frontend/src/scripts/is-device-darkmode.ts b/packages/frontend/src/utility/is-device-darkmode.ts
similarity index 100%
rename from packages/frontend/src/scripts/is-device-darkmode.ts
rename to packages/frontend/src/utility/is-device-darkmode.ts
diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/utility/isFfVisibleForMe.ts
similarity index 100%
rename from packages/frontend/src/scripts/isFfVisibleForMe.ts
rename to packages/frontend/src/utility/isFfVisibleForMe.ts
diff --git a/packages/frontend/src/scripts/key-event.ts b/packages/frontend/src/utility/key-event.ts
similarity index 100%
rename from packages/frontend/src/scripts/key-event.ts
rename to packages/frontend/src/utility/key-event.ts
diff --git a/packages/frontend/src/scripts/langmap.ts b/packages/frontend/src/utility/langmap.ts
similarity index 100%
rename from packages/frontend/src/scripts/langmap.ts
rename to packages/frontend/src/utility/langmap.ts
diff --git a/packages/frontend/src/scripts/load-font.ts b/packages/frontend/src/utility/load-font.ts
similarity index 100%
rename from packages/frontend/src/scripts/load-font.ts
rename to packages/frontend/src/utility/load-font.ts
diff --git a/packages/frontend/src/scripts/login-id.ts b/packages/frontend/src/utility/login-id.ts
similarity index 100%
rename from packages/frontend/src/scripts/login-id.ts
rename to packages/frontend/src/utility/login-id.ts
diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/utility/lookup.ts
similarity index 97%
rename from packages/frontend/src/scripts/lookup.ts
rename to packages/frontend/src/utility/lookup.ts
index 02f589c7ca..d3a2d854a0 100644
--- a/packages/frontend/src/scripts/lookup.ts
+++ b/packages/frontend/src/utility/lookup.ts
@@ -4,7 +4,7 @@
  */
 
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { Router } from '@/nirax.js';
 import { mainRouter } from '@/router/main.js';
diff --git a/packages/frontend/src/scripts/media-has-audio.ts b/packages/frontend/src/utility/media-has-audio.ts
similarity index 100%
rename from packages/frontend/src/scripts/media-has-audio.ts
rename to packages/frontend/src/utility/media-has-audio.ts
diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/utility/media-proxy.ts
similarity index 100%
rename from packages/frontend/src/scripts/media-proxy.ts
rename to packages/frontend/src/utility/media-proxy.ts
diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/utility/merge.ts
similarity index 100%
rename from packages/frontend/src/scripts/merge.ts
rename to packages/frontend/src/utility/merge.ts
diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/utility/mfm-function-picker.ts
similarity index 100%
rename from packages/frontend/src/scripts/mfm-function-picker.ts
rename to packages/frontend/src/utility/mfm-function-picker.ts
diff --git a/packages/frontend/src/scripts/misskey-api.ts b/packages/frontend/src/utility/misskey-api.ts
similarity index 100%
rename from packages/frontend/src/scripts/misskey-api.ts
rename to packages/frontend/src/utility/misskey-api.ts
diff --git a/packages/frontend/src/scripts/navigator.ts b/packages/frontend/src/utility/navigator.ts
similarity index 100%
rename from packages/frontend/src/scripts/navigator.ts
rename to packages/frontend/src/utility/navigator.ts
diff --git a/packages/frontend/src/scripts/physics.ts b/packages/frontend/src/utility/physics.ts
similarity index 100%
rename from packages/frontend/src/scripts/physics.ts
rename to packages/frontend/src/utility/physics.ts
diff --git a/packages/frontend/src/scripts/player-url-transform.ts b/packages/frontend/src/utility/player-url-transform.ts
similarity index 100%
rename from packages/frontend/src/scripts/player-url-transform.ts
rename to packages/frontend/src/utility/player-url-transform.ts
diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/utility/please-login.ts
similarity index 100%
rename from packages/frontend/src/scripts/please-login.ts
rename to packages/frontend/src/utility/please-login.ts
diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/utility/popout.ts
similarity index 100%
rename from packages/frontend/src/scripts/popout.ts
rename to packages/frontend/src/utility/popout.ts
diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/utility/popup-position.ts
similarity index 100%
rename from packages/frontend/src/scripts/popup-position.ts
rename to packages/frontend/src/utility/popup-position.ts
diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/utility/post-message.ts
similarity index 100%
rename from packages/frontend/src/scripts/post-message.ts
rename to packages/frontend/src/utility/post-message.ts
diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/utility/reaction-picker.ts
similarity index 93%
rename from packages/frontend/src/scripts/reaction-picker.ts
rename to packages/frontend/src/utility/reaction-picker.ts
index c142b3ed2a..200fb0b686 100644
--- a/packages/frontend/src/scripts/reaction-picker.ts
+++ b/packages/frontend/src/utility/reaction-picker.ts
@@ -7,7 +7,7 @@ import * as Misskey from 'misskey-js';
 import { defineAsyncComponent, ref } from 'vue';
 import type { Ref } from 'vue';
 import { popup } from '@/os.js';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 
 class ReactionPicker {
 	private src: Ref<HTMLElement | null> = ref(null);
@@ -21,7 +21,7 @@ class ReactionPicker {
 	}
 
 	public async init() {
-		const reactionsRef = defaultStore.reactiveState.reactions;
+		const reactionsRef = store.r.reactions;
 		await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
 			src: this.src,
 			pinnedEmojis: reactionsRef,
diff --git a/packages/frontend/src/scripts/reload-ask.ts b/packages/frontend/src/utility/reload-ask.ts
similarity index 92%
rename from packages/frontend/src/scripts/reload-ask.ts
rename to packages/frontend/src/utility/reload-ask.ts
index 733d91b85a..057f57471a 100644
--- a/packages/frontend/src/scripts/reload-ask.ts
+++ b/packages/frontend/src/utility/reload-ask.ts
@@ -5,7 +5,7 @@
 
 import { i18n } from '@/i18n.js';
 import * as os from '@/os.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { unisonReload } from '@/utility/unison-reload.js';
 
 let isReloadConfirming = false;
 
diff --git a/packages/frontend/src/scripts/search-emoji.ts b/packages/frontend/src/utility/search-emoji.ts
similarity index 100%
rename from packages/frontend/src/scripts/search-emoji.ts
rename to packages/frontend/src/utility/search-emoji.ts
diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/utility/select-file.ts
similarity index 89%
rename from packages/frontend/src/scripts/select-file.ts
rename to packages/frontend/src/utility/select-file.ts
index c25b4d73bd..1bee4986f6 100644
--- a/packages/frontend/src/scripts/select-file.ts
+++ b/packages/frontend/src/utility/select-file.ts
@@ -6,11 +6,11 @@
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
-import { uploadFile } from '@/scripts/upload.js';
+import { uploadFile } from '@/utility/upload.js';
+import { prefer } from '@/preferences.js';
 
 export function chooseFileFromPc(
 	multiple: boolean,
@@ -20,8 +20,8 @@ export function chooseFileFromPc(
 		nameConverter?: (file: File) => string | undefined;
 	},
 ): Promise<Misskey.entities.DriveFile[]> {
-	const uploadFolder = options?.uploadFolder ?? defaultStore.state.uploadFolder;
-	const keepOriginal = options?.keepOriginal ?? defaultStore.state.keepOriginalUploading;
+	const uploadFolder = options?.uploadFolder ?? prefer.s.uploadFolder;
+	const keepOriginal = options?.keepOriginal ?? prefer.s.keepOriginalUploading;
 	const nameConverter = options?.nameConverter ?? (() => undefined);
 
 	return new Promise((res, rej) => {
@@ -82,7 +82,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
 
 			misskeyApi('drive/files/upload-from-url', {
 				url: url,
-				folderId: defaultStore.state.uploadFolder,
+				folderId: prefer.s.uploadFolder,
 				marker,
 			});
 
@@ -96,7 +96,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
 
 function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
 	return new Promise((res, rej) => {
-		const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
+		const keepOriginal = ref(prefer.s.keepOriginalUploading);
 
 		os.popupMenu([label ? {
 			text: label,
diff --git a/packages/frontend/src/scripts/show-moved-dialog.ts b/packages/frontend/src/utility/show-moved-dialog.ts
similarity index 100%
rename from packages/frontend/src/scripts/show-moved-dialog.ts
rename to packages/frontend/src/utility/show-moved-dialog.ts
diff --git a/packages/frontend/src/scripts/show-suspended-dialog.ts b/packages/frontend/src/utility/show-suspended-dialog.ts
similarity index 100%
rename from packages/frontend/src/scripts/show-suspended-dialog.ts
rename to packages/frontend/src/utility/show-suspended-dialog.ts
diff --git a/packages/frontend/src/scripts/shuffle.ts b/packages/frontend/src/utility/shuffle.ts
similarity index 100%
rename from packages/frontend/src/scripts/shuffle.ts
rename to packages/frontend/src/utility/shuffle.ts
diff --git a/packages/frontend/src/scripts/snowfall-effect.ts b/packages/frontend/src/utility/snowfall-effect.ts
similarity index 100%
rename from packages/frontend/src/scripts/snowfall-effect.ts
rename to packages/frontend/src/utility/snowfall-effect.ts
diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/utility/sound.ts
similarity index 91%
rename from packages/frontend/src/scripts/sound.ts
rename to packages/frontend/src/utility/sound.ts
index 2008afe045..436c2b75f0 100644
--- a/packages/frontend/src/scripts/sound.ts
+++ b/packages/frontend/src/utility/sound.ts
@@ -3,8 +3,9 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import type { SoundStore } from '@/store.js';
-import { defaultStore } from '@/store.js';
+import type { SoundStore } from '@/preferences/def.js';
+import { prefer } from '@/preferences.js';
+import { PREF_DEF } from '@/preferences/def.js';
 
 let ctx: AudioContext;
 const cache = new Map<string, AudioBuffer>();
@@ -127,11 +128,11 @@ export async function loadAudio(url: string, options?: { useCache?: boolean; })
  * @param type スプライトの種類を指定
  */
 export function playMisskeySfx(operationType: OperationType) {
-	const sound = defaultStore.state[`sound_${operationType}`];
+	const sound = prefer.s[`sound.on.${operationType}`];
 	playMisskeySfxFile(sound).then((succeed) => {
 		if (!succeed && sound.type === '_driveFile_') {
 			// ドライブファイルが存在しない場合はデフォルトのサウンドを再生する
-			const soundName = defaultStore.def[`sound_${operationType}`].default.type as Exclude<SoundType, '_driveFile_'>;
+			const soundName = PREF_DEF[`sound_${operationType}`].default.type as Exclude<SoundType, '_driveFile_'>;
 			if (_DEV_) console.log(`Failed to play sound: ${sound.fileUrl}, so play default sound: ${soundName}`);
 			playMisskeySfxFileInternal({
 				type: soundName,
@@ -166,7 +167,7 @@ async function playMisskeySfxFileInternal(soundStore: SoundStore): Promise<boole
 	if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) {
 		return false;
 	}
-	const masterVolume = defaultStore.state.sound_masterVolume;
+	const masterVolume = prefer.s['sound.masterVolume'];
 	if (isMute() || masterVolume === 0 || soundStore.volume === 0) {
 		return true; // ミュート時は成功として扱う
 	}
@@ -198,10 +199,10 @@ export function createSourceNode(buffer: AudioBuffer, opts: {
 	pan?: number;
 	playbackRate?: number;
 }): {
-	soundSource: AudioBufferSourceNode;
-	panNode: StereoPannerNode;
-	gainNode: GainNode;
-} {
+		soundSource: AudioBufferSourceNode;
+		panNode: StereoPannerNode;
+		gainNode: GainNode;
+	} {
 	const panNode = ctx.createStereoPanner();
 	panNode.pan.value = opts.pan ?? 0;
 
@@ -242,13 +243,13 @@ export async function getSoundDuration(file: string): Promise<number> {
  * ミュートすべきかどうかを判断する
  */
 export function isMute(): boolean {
-	if (defaultStore.state.sound_notUseSound) {
+	if (prefer.s['sound.notUseSound']) {
 		// サウンドを出力しない
 		return true;
 	}
 
 	// noinspection RedundantIfStatementJS
-	if (defaultStore.state.sound_useSoundOnlyWhenActive && document.visibilityState === 'hidden') {
+	if (prefer.s['sound.useSoundOnlyWhenActive'] && document.visibilityState === 'hidden') {
 		// ブラウザがアクティブな時のみサウンドを出力する
 		return true;
 	}
diff --git a/packages/frontend/src/scripts/sticky-sidebar.ts b/packages/frontend/src/utility/sticky-sidebar.ts
similarity index 100%
rename from packages/frontend/src/scripts/sticky-sidebar.ts
rename to packages/frontend/src/utility/sticky-sidebar.ts
diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/utility/stream-mock.ts
similarity index 100%
rename from packages/frontend/src/scripts/stream-mock.ts
rename to packages/frontend/src/utility/stream-mock.ts
diff --git a/packages/frontend/src/scripts/test-utils.ts b/packages/frontend/src/utility/test-utils.ts
similarity index 100%
rename from packages/frontend/src/scripts/test-utils.ts
rename to packages/frontend/src/utility/test-utils.ts
diff --git a/packages/frontend/src/scripts/theme-editor.ts b/packages/frontend/src/utility/theme-editor.ts
similarity index 96%
rename from packages/frontend/src/scripts/theme-editor.ts
rename to packages/frontend/src/utility/theme-editor.ts
index 0206e378bf..ea07e5f2ff 100644
--- a/packages/frontend/src/scripts/theme-editor.ts
+++ b/packages/frontend/src/utility/theme-editor.ts
@@ -5,8 +5,8 @@
 
 import { v4 as uuid } from 'uuid';
 
-import { themeProps } from './theme.js';
-import type { Theme } from './theme.js';
+import type { Theme } from '@/theme.js';
+import { themeProps } from '@/theme.js';
 
 export type Default = null;
 export type Color = string;
diff --git a/packages/frontend/src/scripts/time.ts b/packages/frontend/src/utility/time.ts
similarity index 100%
rename from packages/frontend/src/scripts/time.ts
rename to packages/frontend/src/utility/time.ts
diff --git a/packages/frontend/src/scripts/timezones.ts b/packages/frontend/src/utility/timezones.ts
similarity index 100%
rename from packages/frontend/src/scripts/timezones.ts
rename to packages/frontend/src/utility/timezones.ts
diff --git a/packages/frontend/src/scripts/touch.ts b/packages/frontend/src/utility/touch.ts
similarity index 93%
rename from packages/frontend/src/scripts/touch.ts
rename to packages/frontend/src/utility/touch.ts
index 13c9d648dc..adc2e4c093 100644
--- a/packages/frontend/src/scripts/touch.ts
+++ b/packages/frontend/src/utility/touch.ts
@@ -4,7 +4,7 @@
  */
 
 import { ref } from 'vue';
-import { deviceKind } from '@/scripts/device-kind.js';
+import { deviceKind } from '@/utility/device-kind.js';
 
 const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0;
 
diff --git a/packages/frontend/src/scripts/unison-reload.ts b/packages/frontend/src/utility/unison-reload.ts
similarity index 100%
rename from packages/frontend/src/scripts/unison-reload.ts
rename to packages/frontend/src/utility/unison-reload.ts
diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/utility/upload.ts
similarity index 96%
rename from packages/frontend/src/scripts/upload.ts
rename to packages/frontend/src/utility/upload.ts
index 713573a377..d105a318a7 100644
--- a/packages/frontend/src/scripts/upload.ts
+++ b/packages/frontend/src/utility/upload.ts
@@ -7,13 +7,13 @@ import { reactive, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { v4 as uuid } from 'uuid';
 import { readAndCompressImage } from '@misskey-dev/browser-image-resizer';
-import { getCompressionConfig } from './upload/compress-config.js';
-import { defaultStore } from '@/store.js';
 import { apiUrl } from '@@/js/config.js';
+import { getCompressionConfig } from './upload/compress-config.js';
 import { $i } from '@/account.js';
 import { alert } from '@/os.js';
 import { i18n } from '@/i18n.js';
 import { instance } from '@/instance.js';
+import { prefer } from '@/preferences.js';
 
 type Uploading = {
 	id: string;
@@ -34,7 +34,7 @@ export function uploadFile(
 	file: File,
 	folder?: string | Misskey.entities.DriveFolder,
 	name?: string,
-	keepOriginal: boolean = defaultStore.state.keepOriginalUploading,
+	keepOriginal: boolean = prefer.s.keepOriginalUploading,
 ): Promise<Misskey.entities.DriveFile> {
 	if ($i == null) throw new Error('Not logged in');
 
@@ -59,7 +59,7 @@ export function uploadFile(
 
 			const ctx = reactive<Uploading>({
 				id,
-				name: defaultStore.state.keepOriginalFilename ? filename : id + extension,
+				name: prefer.s.keepOriginalFilename ? filename : id + extension,
 				progressMax: undefined,
 				progressValue: undefined,
 				img: window.URL.createObjectURL(file),
diff --git a/packages/frontend/src/scripts/upload/compress-config.ts b/packages/frontend/src/utility/upload/compress-config.ts
similarity index 100%
rename from packages/frontend/src/scripts/upload/compress-config.ts
rename to packages/frontend/src/utility/upload/compress-config.ts
diff --git a/packages/frontend/src/scripts/upload/isWebpSupported.ts b/packages/frontend/src/utility/upload/isWebpSupported.ts
similarity index 100%
rename from packages/frontend/src/scripts/upload/isWebpSupported.ts
rename to packages/frontend/src/utility/upload/isWebpSupported.ts
diff --git a/packages/frontend/src/scripts/use-chart-tooltip.ts b/packages/frontend/src/utility/use-chart-tooltip.ts
similarity index 100%
rename from packages/frontend/src/scripts/use-chart-tooltip.ts
rename to packages/frontend/src/utility/use-chart-tooltip.ts
diff --git a/packages/frontend/src/scripts/use-form.ts b/packages/frontend/src/utility/use-form.ts
similarity index 100%
rename from packages/frontend/src/scripts/use-form.ts
rename to packages/frontend/src/utility/use-form.ts
diff --git a/packages/frontend/src/scripts/use-leave-guard.ts b/packages/frontend/src/utility/use-leave-guard.ts
similarity index 100%
rename from packages/frontend/src/scripts/use-leave-guard.ts
rename to packages/frontend/src/utility/use-leave-guard.ts
diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/utility/use-note-capture.ts
similarity index 100%
rename from packages/frontend/src/scripts/use-note-capture.ts
rename to packages/frontend/src/utility/use-note-capture.ts
diff --git a/packages/frontend/src/scripts/use-tooltip.ts b/packages/frontend/src/utility/use-tooltip.ts
similarity index 100%
rename from packages/frontend/src/scripts/use-tooltip.ts
rename to packages/frontend/src/utility/use-tooltip.ts
diff --git a/packages/frontend/src/widgets/WidgetActivity.vue b/packages/frontend/src/widgets/WidgetActivity.vue
index cf1110da2b..d911e71ab2 100644
--- a/packages/frontend/src/widgets/WidgetActivity.vue
+++ b/packages/frontend/src/widgets/WidgetActivity.vue
@@ -25,8 +25,8 @@ import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
 import XCalendar from './WidgetActivity.calendar.vue';
 import XChart from './WidgetActivity.chart.vue';
-import type { GetFormResultType } from '@/scripts/form.js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+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 { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/widgets/WidgetAichan.vue b/packages/frontend/src/widgets/WidgetAichan.vue
index 9b04c463ba..29e21ee6c3 100644
--- a/packages/frontend/src/widgets/WidgetAichan.vue
+++ b/packages/frontend/src/widgets/WidgetAichan.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onMounted, onUnmounted, shallowRef } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentProps, WidgetComponentEmits, WidgetComponentExpose } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 
 const name = 'ai';
 
diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index 80573d2dc4..b49041158f 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -23,10 +23,10 @@ import { ref } from 'vue';
 import { Interpreter, Parser, utils } from '@syuilo/aiscript';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import { $i } from '@/account.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/widgets/WidgetAiscriptApp.vue b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
index f0f81a4a89..fb9dea1847 100644
--- a/packages/frontend/src/widgets/WidgetAiscriptApp.vue
+++ b/packages/frontend/src/widgets/WidgetAiscriptApp.vue
@@ -18,14 +18,14 @@ import type { Ref } from 'vue';
 import { Interpreter, Parser } from '@syuilo/aiscript';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import { $i } from '@/account.js';
 import MkAsUi from '@/components/MkAsUi.vue';
 import MkContainer from '@/components/MkContainer.vue';
-import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
-import type { AsUiComponent, AsUiRoot } from '@/scripts/aiscript/ui.js';
+import { registerAsUiLib } from '@/aiscript/ui.js';
+import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
 
 const name = 'aiscriptApp';
 
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index 98e186f836..8c7507ef44 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -28,9 +28,9 @@ import * as Misskey from 'misskey-js';
 import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
 import { infoImageUrl } from '@/instance.js';
 import { $i } from '@/account.js';
diff --git a/packages/frontend/src/widgets/WidgetButton.vue b/packages/frontend/src/widgets/WidgetButton.vue
index 3e455bee3b..3f0f9eb9fd 100644
--- a/packages/frontend/src/widgets/WidgetButton.vue
+++ b/packages/frontend/src/widgets/WidgetButton.vue
@@ -15,9 +15,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { Interpreter, Parser } from '@syuilo/aiscript';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import { $i } from '@/account.js';
 import MkButton from '@/components/MkButton.vue';
 
diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue
index 94169d5e40..54f78469b2 100644
--- a/packages/frontend/src/widgets/WidgetCalendar.vue
+++ b/packages/frontend/src/widgets/WidgetCalendar.vue
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import { i18n } from '@/i18n.js';
 import { useInterval } from '@@/js/use-interval.js';
 
diff --git a/packages/frontend/src/widgets/WidgetClicker.vue b/packages/frontend/src/widgets/WidgetClicker.vue
index 2b3663d35b..87ffd3d732 100644
--- a/packages/frontend/src/widgets/WidgetClicker.vue
+++ b/packages/frontend/src/widgets/WidgetClicker.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkClickerGame from '@/components/MkClickerGame.vue';
 
diff --git a/packages/frontend/src/widgets/WidgetClock.vue b/packages/frontend/src/widgets/WidgetClock.vue
index 2d3f57a92f..826ecf6e02 100644
--- a/packages/frontend/src/widgets/WidgetClock.vue
+++ b/packages/frontend/src/widgets/WidgetClock.vue
@@ -32,11 +32,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkAnalogClock from '@/components/MkAnalogClock.vue';
 import MkDigitalClock from '@/components/MkDigitalClock.vue';
-import { timezones } from '@/scripts/timezones.js';
+import { timezones } from '@/utility/timezones.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'clock';
diff --git a/packages/frontend/src/widgets/WidgetDigitalClock.vue b/packages/frontend/src/widgets/WidgetDigitalClock.vue
index 6e0c9e6dfc..d79ec79d4f 100644
--- a/packages/frontend/src/widgets/WidgetDigitalClock.vue
+++ b/packages/frontend/src/widgets/WidgetDigitalClock.vue
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { computed } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
-import { timezones } from '@/scripts/timezones.js';
+import type { GetFormResultType } from '@/utility/form.js';
+import { timezones } from '@/utility/timezones.js';
 import MkDigitalClock from '@/components/MkDigitalClock.vue';
 
 const name = 'digitalClock';
diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue
index 89716575a9..ca6f27bd09 100644
--- a/packages/frontend/src/widgets/WidgetFederation.vue
+++ b/packages/frontend/src/widgets/WidgetFederation.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<div class="wbrkwalb">
 		<MkLoading v-if="fetching"/>
-		<TransitionGroup v-else tag="div" :name="defaultStore.state.animation ? 'chart' : ''" class="instances">
+		<TransitionGroup v-else tag="div" :name="prefer.s.animation ? 'chart' : ''" class="instances">
 			<div v-for="(instance, i) in instances" :key="instance.id" class="instance">
 				<img :src="getInstanceIcon(instance)" alt=""/>
 				<div class="body">
@@ -27,16 +27,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkMiniChart from '@/components/MkMiniChart.vue';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
-import { useInterval } from '@@/js/use-interval.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
-import { defaultStore } from '@/store.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
+import { prefer } from '@/preferences.js';
 
 const name = 'federation';
 
diff --git a/packages/frontend/src/widgets/WidgetInstanceCloud.vue b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
index aee9066731..c6724127fa 100644
--- a/packages/frontend/src/widgets/WidgetInstanceCloud.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceCloud.vue
@@ -22,13 +22,13 @@ import { shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkTagCloud from '@/components/MkTagCloud.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
-import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
+import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
 
 const name = 'instanceCloud';
 
diff --git a/packages/frontend/src/widgets/WidgetInstanceInfo.vue b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
index 69832332b1..8d721298d5 100644
--- a/packages/frontend/src/widgets/WidgetInstanceInfo.vue
+++ b/packages/frontend/src/widgets/WidgetInstanceInfo.vue
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import { host } from '@@/js/config.js';
 import { instance } from '@/instance.js';
 
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index 84ba05b5d3..485e532d51 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -54,12 +54,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onUnmounted, reactive, ref } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import { useStream } from '@/stream.js';
 import kmg from '@/filters/kmg.js';
-import * as sound from '@/scripts/sound.js';
-import { deepClone } from '@/scripts/clone.js';
-import { defaultStore } from '@/store.js';
+import * as sound from '@/utility/sound.js';
+import { deepClone } from '@/utility/clone.js';
+import { prefer } from '@/preferences.js';
 
 const name = 'jobQueue';
 
@@ -104,7 +104,7 @@ const prev = reactive({} as typeof current);
 const jammedAudioBuffer = ref<AudioBuffer | null>(null);
 const jammedSoundNodePlaying = ref<boolean>(false);
 
-if (defaultStore.state.sound_masterVolume) {
+if (prefer.s['sound.masterVolume']) {
 	sound.loadAudio('/client-assets/sounds/syuilo/queue-jammed.mp3').then(buf => {
 		if (!buf) throw new Error('[WidgetJobQueue] Failed to initialize AudioBuffer');
 		jammedAudioBuffer.value = buf;
diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue
index 65ab7a7075..3df5c5bfd7 100644
--- a/packages/frontend/src/widgets/WidgetMemo.vue
+++ b/packages/frontend/src/widgets/WidgetMemo.vue
@@ -19,9 +19,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, watch } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { defaultStore } from '@/store.js';
+import { store } from '@/store.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'memo';
@@ -48,12 +48,12 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
 	emit,
 );
 
-const text = ref<string | null>(defaultStore.state.memo);
+const text = ref<string | null>(store.s.memo);
 const changed = ref(false);
 let timeoutId;
 
 const saveMemo = () => {
-	defaultStore.set('memo', text.value);
+	store.set('memo', text.value);
 	changed.value = false;
 };
 
@@ -63,7 +63,7 @@ const onChange = () => {
 	timeoutId = window.setTimeout(saveMemo, 1000);
 };
 
-watch(() => defaultStore.reactiveState.memo, newText => {
+watch(() => store.r.memo, newText => {
 	text.value = newText.value;
 });
 
diff --git a/packages/frontend/src/widgets/WidgetNotifications.vue b/packages/frontend/src/widgets/WidgetNotifications.vue
index 8aaed2624d..c5e1324ef5 100644
--- a/packages/frontend/src/widgets/WidgetNotifications.vue
+++ b/packages/frontend/src/widgets/WidgetNotifications.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { defineAsyncComponent } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import XNotifications from '@/components/MkNotifications.vue';
 import * as os from '@/os.js';
diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
index 4a3cdb9ba3..f6bd4c0025 100644
--- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue
+++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
-import { misskeyApi, misskeyApiGet } from '@/scripts/misskey-api.js';
+import type { GetFormResultType } from '@/utility/form.js';
+import { misskeyApi, misskeyApiGet } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 import number from '@/filters/number.js';
diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue
index 6d13ba09cc..5d6e2ed48f 100644
--- a/packages/frontend/src/widgets/WidgetPhotos.vue
+++ b/packages/frontend/src/widgets/WidgetPhotos.vue
@@ -26,12 +26,12 @@ import { onUnmounted, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import { useStream } from '@/stream.js';
-import { getStaticImageUrl } from '@/scripts/media-proxy.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { getStaticImageUrl } from '@/utility/media-proxy.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 import { i18n } from '@/i18n.js';
 
 const name = 'photos';
@@ -70,7 +70,7 @@ const onDriveFileCreated = (file) => {
 };
 
 const thumbnail = (image: Misskey.entities.DriveFile): string => {
-	return defaultStore.state.disableShowingAnimatedImages
+	return prefer.s.disableShowingAnimatedImages
 		? getStaticImageUrl(image.url)
 		: image.thumbnailUrl ?? image.url;
 };
diff --git a/packages/frontend/src/widgets/WidgetPostForm.vue b/packages/frontend/src/widgets/WidgetPostForm.vue
index b0a62d1be2..3170eab305 100644
--- a/packages/frontend/src/widgets/WidgetPostForm.vue
+++ b/packages/frontend/src/widgets/WidgetPostForm.vue
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkPostForm from '@/components/MkPostForm.vue';
 
 const name = 'postForm';
diff --git a/packages/frontend/src/widgets/WidgetProfile.vue b/packages/frontend/src/widgets/WidgetProfile.vue
index 9f006945ab..c86d1c9653 100644
--- a/packages/frontend/src/widgets/WidgetProfile.vue
+++ b/packages/frontend/src/widgets/WidgetProfile.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import { $i } from '@/account.js';
 import { userPage } from '@/filters/user.js';
 
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 0d7ce55be3..82dfeacff1 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -27,7 +27,7 @@ import { ref, watch, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { url as base } from '@@/js/config.js';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index 5ecc1ab022..13b76533d7 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -32,9 +32,9 @@ import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
 import MarqueeText from '@/components/MkMarquee.vue';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
-import { shuffle } from '@/scripts/shuffle.js';
+import { shuffle } from '@/utility/shuffle.js';
 import { url as base } from '@@/js/config.js';
 import { useInterval } from '@@/js/use-interval.js';
 
diff --git a/packages/frontend/src/widgets/WidgetSlideshow.vue b/packages/frontend/src/widgets/WidgetSlideshow.vue
index 67d35d71db..2cbf3a8ef6 100644
--- a/packages/frontend/src/widgets/WidgetSlideshow.vue
+++ b/packages/frontend/src/widgets/WidgetSlideshow.vue
@@ -21,9 +21,9 @@ import { onMounted, ref, shallowRef } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/widgets/WidgetTimeline.vue b/packages/frontend/src/widgets/WidgetTimeline.vue
index 02db9454a8..47dec05303 100644
--- a/packages/frontend/src/widgets/WidgetTimeline.vue
+++ b/packages/frontend/src/widgets/WidgetTimeline.vue
@@ -34,9 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkTimeline from '@/components/MkTimeline.vue';
 import { i18n } from '@/i18n.js';
diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue
index 3354912c07..db09031c33 100644
--- a/packages/frontend/src/widgets/WidgetTrends.vue
+++ b/packages/frontend/src/widgets/WidgetTrends.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 	<div class="wbrkwala">
 		<MkLoading v-if="fetching"/>
-		<TransitionGroup v-else tag="div" :name="defaultStore.state.animation ? 'chart' : ''" class="tags">
+		<TransitionGroup v-else tag="div" :name="prefer.s.animation ? 'chart' : ''" class="tags">
 			<div v-for="stat in stats" :key="stat.tag">
 				<div class="tag">
 					<MkA class="a" :to="`/tags/${ encodeURIComponent(stat.tag) }`" :title="stat.tag">#{{ stat.tag }}</MkA>
@@ -26,15 +26,15 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
+import { useInterval } from '@@/js/use-interval.js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import MkMiniChart from '@/components/MkMiniChart.vue';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
-import { useInterval } from '@@/js/use-interval.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { i18n } from '@/i18n.js';
-import { defaultStore } from '@/store.js';
+import { prefer } from '@/preferences.js';
 
 const name = 'hashtags';
 
diff --git a/packages/frontend/src/widgets/WidgetUnixClock.vue b/packages/frontend/src/widgets/WidgetUnixClock.vue
index f85d27d6aa..f51ef12a2a 100644
--- a/packages/frontend/src/widgets/WidgetUnixClock.vue
+++ b/packages/frontend/src/widgets/WidgetUnixClock.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { onUnmounted, ref, watch } from 'vue';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 
 const name = 'unixClock';
 
diff --git a/packages/frontend/src/widgets/WidgetUserList.vue b/packages/frontend/src/widgets/WidgetUserList.vue
index 805e14c669..eb86732817 100644
--- a/packages/frontend/src/widgets/WidgetUserList.vue
+++ b/packages/frontend/src/widgets/WidgetUserList.vue
@@ -28,10 +28,10 @@ import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import type { GetFormResultType } from '@/scripts/form.js';
+import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import * as os from '@/os.js';
-import { misskeyApi } from '@/scripts/misskey-api.js';
+import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
 import { i18n } from '@/i18n.js';
 import MkButton from '@/components/MkButton.vue';
diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue
index 3264c8dc04..9026fefb20 100644
--- a/packages/frontend/src/widgets/server-metric/index.vue
+++ b/packages/frontend/src/widgets/server-metric/index.vue
@@ -30,8 +30,8 @@ import XCpu from './cpu.vue';
 import XMemory from './mem.vue';
 import XDisk from './disk.vue';
 import MkContainer from '@/components/MkContainer.vue';
-import type { GetFormResultType } from '@/scripts/form.js';
-import { misskeyApiGet } from '@/scripts/misskey-api.js';
+import type { GetFormResultType } from '@/utility/form.js';
+import { misskeyApiGet } from '@/utility/misskey-api.js';
 import { useStream } from '@/stream.js';
 import { i18n } from '@/i18n.js';
 
diff --git a/packages/frontend/src/widgets/widget.ts b/packages/frontend/src/widgets/widget.ts
index 98e1e44cd8..de4c369cbb 100644
--- a/packages/frontend/src/widgets/widget.ts
+++ b/packages/frontend/src/widgets/widget.ts
@@ -5,9 +5,9 @@
 
 import { reactive, watch } from 'vue';
 import { throttle } from 'throttle-debounce';
-import type { Form, GetFormResultType } from '@/scripts/form.js';
+import type { Form, GetFormResultType } from '@/utility/form.js';
 import * as os from '@/os.js';
-import { deepClone } from '@/scripts/clone.js';
+import { deepClone } from '@/utility/clone.js';
 
 export type Widget<P extends Record<string, unknown>> = {
 	id: string;
diff --git a/packages/frontend/test/aiscript/api.test.ts b/packages/frontend/test/aiscript/api.test.ts
index 2a15a74249..a569c0fa51 100644
--- a/packages/frontend/test/aiscript/api.test.ts
+++ b/packages/frontend/test/aiscript/api.test.ts
@@ -4,7 +4,7 @@
  */
 
 import { miLocalStorage } from '@/local-storage.js';
-import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
+import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
 import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
 import {
 	afterAll,
@@ -59,7 +59,7 @@ vi.mock('@/os.js', () => {
 
 const misskeyApiMock = vi.hoisted(() => vi.fn());
 
-vi.mock('@/scripts/misskey-api.js', () => {
+vi.mock('@/utility/misskey-api.js', () => {
 	return { misskeyApi: misskeyApiMock };
 });
 
diff --git a/packages/frontend/test/aiscript/common.test.ts b/packages/frontend/test/aiscript/common.test.ts
index acc48826ea..c0c978001b 100644
--- a/packages/frontend/test/aiscript/common.test.ts
+++ b/packages/frontend/test/aiscript/common.test.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { assertStringAndIsIn } from "@/scripts/aiscript/common.js";
+import { assertStringAndIsIn } from "@/aiscript/common.js";
 import { values } from "@syuilo/aiscript";
 import { describe, expect, test } from "vitest";
 
diff --git a/packages/frontend/test/aiscript/ui.test.ts b/packages/frontend/test/aiscript/ui.test.ts
index 5f77edbb49..44a50aaa62 100644
--- a/packages/frontend/test/aiscript/ui.test.ts
+++ b/packages/frontend/test/aiscript/ui.test.ts
@@ -3,7 +3,7 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { registerAsUiLib } from '@/scripts/aiscript/ui.js';
+import { registerAsUiLib } from '@/aiscript/ui.js';
 import { errors, Interpreter, Parser, values } from '@syuilo/aiscript';
 import { describe, expect, test } from 'vitest';
 import { type Ref, ref } from 'vue';
@@ -19,7 +19,7 @@ import type {
 	AsUiText,
 	AsUiTextarea,
 	AsUiTextInput,
-} from '@/scripts/aiscript/ui.js';
+} from '@/aiscript/ui.js';
 
 type ExeResult = {
 	root: AsUiRoot;
diff --git a/packages/frontend/test/autocomplete.test.ts b/packages/frontend/test/autocomplete.test.ts
index 394ac3a821..38be35813f 100644
--- a/packages/frontend/test/autocomplete.test.ts
+++ b/packages/frontend/test/autocomplete.test.ts
@@ -4,7 +4,7 @@
  */
 
 import { assert, describe, test } from 'vitest';
-import { searchEmoji } from '@/scripts/search-emoji.js';
+import { searchEmoji } from '@/utility/search-emoji.js';
 
 describe('emoji autocomplete', () => {
 	test('名前の完全一致は名前の前方一致より優先される', async () => {
diff --git a/packages/frontend/test/emoji.test.ts b/packages/frontend/test/emoji.test.ts
index cf686efd0d..ffdc858b75 100644
--- a/packages/frontend/test/emoji.test.ts
+++ b/packages/frontend/test/emoji.test.ts
@@ -5,7 +5,7 @@
 
 import { describe, test, assert, afterEach } from 'vitest';
 import { render, cleanup, type RenderResult } from '@testing-library/vue';
-import { defaultStoreState } from './init.js';
+import { preferState } from './init.js';
 import { getEmojiName } from '@@/js/emojilist.js';
 import { components } from '@/components/index.js';
 import { directives } from '@/directives/index.js';
@@ -21,12 +21,12 @@ describe('Emoji', () => {
 
 	afterEach(() => {
 		cleanup();
-		defaultStoreState.emojiStyle = '';
+		preferState.emojiStyle = '';
 	});
 
 	describe('MkEmoji', () => {
 		test('Should render selector-less heart with color in native mode', async () => {
-			defaultStoreState.emojiStyle = 'native';
+			preferState.emojiStyle = 'native';
 			const mkEmoji = await renderEmoji('\u2764'); // monochrome heart
 			assert.ok(mkEmoji.queryByText('\u2764\uFE0F')); // colored heart
 			assert.ok(!mkEmoji.queryByText('\u2764'));
diff --git a/packages/frontend/test/init.ts b/packages/frontend/test/init.ts
index 0cde571dcb..3b6b4d581b 100644
--- a/packages/frontend/test/init.ts
+++ b/packages/frontend/test/init.ts
@@ -17,7 +17,7 @@ updateI18n(locales['en-US']);
 // XXX: misskey-js panics if WebSocket is not defined
 vi.stubGlobal('WebSocket', class WebSocket extends EventTarget { static CLOSING = 2; });
 
-export const defaultStoreState: Record<string, unknown> = {
+export const preferState: Record<string, unknown> = {
 
 	// なんかtestがうまいこと動かないのでここに書く
 	dataSaver: {
@@ -29,11 +29,11 @@ export const defaultStoreState: Record<string, unknown> = {
 
 };
 
-// XXX: defaultStore somehow becomes undefined in vitest?
-vi.mock('@/store.js', () => {
+// XXX: store somehow becomes undefined in vitest?
+vi.mock('@/preferences.js', () => {
 	return {
-		defaultStore: {
-			state: defaultStoreState,
+		prefer: {
+			s: preferState,
 		},
 	};
 });
diff --git a/packages/frontend/test/intl-string.test.ts b/packages/frontend/test/intl-string.test.ts
new file mode 100644
index 0000000000..b52824db86
--- /dev/null
+++ b/packages/frontend/test/intl-string.test.ts
@@ -0,0 +1,142 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { assert, beforeEach, describe, test } from 'vitest';
+import {
+	normalizeString,
+	initIntlString,
+	normalizeStringWithHiragana,
+	compareStringEquals,
+	compareStringIncludes,
+} from '@/utility/intl-string.js';
+
+// 共通のテストを実行するヘルパー関数
+const runCommonTests = (normalizeFn: (str: string) => string) => {
+	test('全角英数字が半角の小文字になる', () => {
+		// ローマ字にならないようにする
+		const input = 'B123';
+		const expected = 'b123';
+		assert.strictEqual(normalizeFn(input), expected);
+	});
+	test('濁点・半濁点が正しく結合される', () => {
+		const input = 'か\u3099';
+		const expected = 'が';
+		assert.strictEqual(normalizeFn(input), expected);
+	});
+	test('小文字に揃う', () => {
+		// ローマ字にならないようにする
+		const input = 'tSt';
+		const expected = 'tst';
+		assert.strictEqual(normalizeFn(input), expected);
+	});
+	test('文字列の前後の空白が削除される', () => {
+		const input = '   tst   ';
+		const expected = 'tst';
+		assert.strictEqual(normalizeFn(input), expected);
+	});
+};
+
+describe('normalize string', () => {
+	runCommonTests(normalizeString);
+
+	test('異体字の正規化 (ligature)', () => {
+		const input = 'fi';
+		const expected = 'fi';
+		assert.strictEqual(normalizeString(input), expected);
+	});
+
+	test('半角カタカナは全角に変換される', () => {
+		const input = 'カタカナ';
+		const expected = 'カタカナ';
+		assert.strictEqual(normalizeString(input), expected);
+	});
+});
+
+// normalizeStringWithHiraganaのテスト
+describe('normalize string with hiragana', () => {
+	beforeEach(async () => {
+		await initIntlString(true);
+	});
+
+	// 共通テスト
+	describe('共通のnormalizeStringテスト', () => {
+		runCommonTests(normalizeStringWithHiragana);
+	});
+
+	test('半角カタカナがひらがなに変換される', () => {
+		const input = 'カタカナ';
+		const expected = 'かたかな';
+		assert.strictEqual(normalizeStringWithHiragana(input), expected);
+	});
+
+	// normalizeStringWithHiragana特有のテスト
+	test('カタカナがひらがなに変換される・伸ばし棒はハイフンに変換される', () => {
+		const input = 'カタカナひーらがーな';
+		const expected = 'かたかなひ-らが-な';
+		assert.strictEqual(normalizeStringWithHiragana(input), expected);
+	});
+
+	test('ローマ字がひらがなに変換される', () => {
+		const input = 'ro-majimohiragananinarimasu';
+		const expected = 'ろ-まじもひらがなになります';
+		assert.strictEqual(normalizeStringWithHiragana(input), expected);
+	});
+});
+
+describe('compareStringEquals', () => {
+	beforeEach(async () => {
+		await initIntlString(true);
+	});
+
+	test('完全一致ならtrue', () => {
+		assert.isTrue(compareStringEquals('テスト', 'テスト'));
+	});
+
+	test('大文字・小文字の違いを無視', () => {
+		assert.isTrue(compareStringEquals('TeSt', 'test'));
+	});
+
+	test('全角・半角の違いを無視', () => {
+		assert.isTrue(compareStringEquals('ABC', 'abc'));
+	});
+
+	test('カタカナとひらがなの違いを無視', () => {
+		assert.isTrue(compareStringEquals('カタカナ', 'かたかな'));
+	});
+
+	test('ローマ字をひらがなと比較可能', () => {
+		assert.isTrue(compareStringEquals('hiragana', 'ひらがな'));
+	});
+
+	test('異なる文字列はfalse', () => {
+		assert.isFalse(compareStringEquals('テスト', 'サンプル'));
+	});
+});
+
+describe('compareStringIncludes', () => {
+	test('部分一致ならtrue', () => {
+		assert.isTrue(compareStringIncludes('これはテストです', 'テスト'));
+	});
+
+	test('大文字・小文字の違いを無視', () => {
+		assert.isTrue(compareStringIncludes('This is a Test', 'test'));
+	});
+
+	test('全角・半角の違いを無視', () => {
+		assert.isTrue(compareStringIncludes('ABCDE', 'abc'));
+	});
+
+	test('カタカナとひらがなの違いを無視', () => {
+		assert.isTrue(compareStringIncludes('カタカナのテスト', 'かたかな'));
+	});
+
+	test('ローマ字をひらがなと比較可能', () => {
+		assert.isTrue(compareStringIncludes('これはhiraganaのテスト', 'ひらがな'));
+	});
+
+	test('異なる文字列はfalse', () => {
+		assert.isFalse(compareStringIncludes('これはテストです', 'サンプル'));
+	});
+});
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 1c094e272b..a28fc553f4 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -86,7 +86,7 @@ export function getConfig(): UserConfig {
 		plugins: [
 			pluginCreateSearchIndex({
 				targetFilePaths: ['src/pages/settings/*.vue'],
-				exportFilePath: './src/scripts/autogen/settings-search-index.ts',
+				exportFilePath: './src/utility/autogen/settings-search-index.ts',
 				verbose: process.env.FRONTEND_SEARCH_INDEX_VERBOSE === 'true',
 			}),
 			pluginVue(),
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index eb8e7c479b..0ca8b36732 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.1",
+	"version": "2025.3.2-alpha.5",
 	"description": "Misskey SDK for JavaScript",
 	"license": "MIT",
 	"main": "./built/index.js",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2c8f152756..23e42c7916 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -54,16 +54,16 @@ importers:
     devDependencies:
       '@misskey-dev/eslint-plugin':
         specifier: 2.1.0
-        version: 2.1.0(@eslint/compat@1.2.6(eslint@9.21.0))(@stylistic/eslint-plugin@3.1.0(eslint@9.21.0)(typescript@5.8.2))(@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2))(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0))(eslint@9.21.0)(globals@16.0.0)
+        version: 2.1.0(@eslint/compat@1.2.6(eslint@9.22.0))(@stylistic/eslint-plugin@3.1.0(eslint@9.22.0)(typescript@5.8.2))(@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2))(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0))(eslint@9.22.0)(globals@16.0.0)
       '@types/node':
-        specifier: 22.13.9
-        version: 22.13.9
+        specifier: 22.13.10
+        version: 22.13.10
       '@typescript-eslint/eslint-plugin':
         specifier: 8.26.0
-        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)
       '@typescript-eslint/parser':
         specifier: 8.26.0
-        version: 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       cross-env:
         specifier: 7.0.3
         version: 7.0.3
@@ -71,8 +71,8 @@ importers:
         specifier: 14.1.0
         version: 14.1.0
       eslint:
-        specifier: 9.21.0
-        version: 9.21.0
+        specifier: 9.22.0
+        version: 9.22.0
       globals:
         specifier: 16.0.0
         version: 16.0.0
@@ -569,10 +569,10 @@ importers:
         version: 8.5.14
       '@typescript-eslint/eslint-plugin':
         specifier: 8.24.0
-        version: 8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.21.0)(typescript@5.7.3))(eslint@9.21.0)(typescript@5.7.3)
+        version: 8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0)(typescript@5.7.3)
       '@typescript-eslint/parser':
         specifier: 8.24.0
-        version: 8.24.0(eslint@9.21.0)(typescript@5.7.3)
+        version: 8.24.0(eslint@9.22.0)(typescript@5.7.3)
       aws-sdk-client-mock:
         specifier: 4.1.0
         version: 4.1.0
@@ -581,7 +581,7 @@ importers:
         version: 7.0.3
       eslint-plugin-import:
         specifier: 2.31.0
-        version: 2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.21.0)(typescript@5.7.3))(eslint@9.21.0)
+        version: 2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0)
       execa:
         specifier: 8.0.1
         version: 8.0.1
@@ -877,6 +877,9 @@ importers:
       vuedraggable:
         specifier: next
         version: 4.1.0(vue@3.5.13(typescript@5.8.2))
+      wanakana:
+        specifier: 5.3.1
+        version: 5.3.1
     devDependencies:
       '@misskey-dev/summaly':
         specifier: 5.2.0
@@ -976,10 +979,10 @@ importers:
         version: 8.18.0
       '@typescript-eslint/eslint-plugin':
         specifier: 8.26.0
-        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)
       '@typescript-eslint/parser':
         specifier: 8.26.0
-        version: 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       '@vitest/coverage-v8':
         specifier: 3.0.8
         version: 3.0.8(vitest@3.0.8(@types/debug@4.1.12)(@types/node@22.13.9)(happy-dom@17.3.0)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.3(@types/node@22.13.9)(typescript@5.8.2))(sass@1.85.1)(terser@5.39.0)(tsx@4.19.3))
@@ -997,10 +1000,10 @@ importers:
         version: 14.1.0
       eslint-plugin-import:
         specifier: 2.31.0
-        version: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)
+        version: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)
       eslint-plugin-vue:
         specifier: 10.0.0
-        version: 10.0.0(eslint@9.21.0)(vue-eslint-parser@10.1.1(eslint@9.21.0))
+        version: 10.0.0(eslint@9.22.0)(vue-eslint-parser@10.1.1(eslint@9.22.0))
       fast-glob:
         specifier: 3.3.3
         version: 3.3.3
@@ -1057,7 +1060,7 @@ importers:
         version: 2.2.8
       vue-eslint-parser:
         specifier: 10.1.1
-        version: 10.1.1(eslint@9.21.0)
+        version: 10.1.1(eslint@9.22.0)
       vue-tsc:
         specifier: 2.2.8
         version: 2.2.8(typescript@5.8.2)
@@ -1169,10 +1172,10 @@ importers:
         version: 8.18.0
       '@typescript-eslint/eslint-plugin':
         specifier: 8.26.0
-        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)
       '@typescript-eslint/parser':
         specifier: 8.26.0
-        version: 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       '@vitest/coverage-v8':
         specifier: 3.0.8
         version: 3.0.8(vitest@3.0.8(@types/debug@4.1.12)(@types/node@22.13.9)(happy-dom@17.3.0)(jsdom@26.0.0(bufferutil@4.0.9)(utf-8-validate@6.0.5))(msw@2.7.3(@types/node@22.13.9)(typescript@5.8.2))(sass@1.85.1)(terser@5.39.0)(tsx@4.19.3))
@@ -1187,10 +1190,10 @@ importers:
         version: 7.0.3
       eslint-plugin-import:
         specifier: 2.31.0
-        version: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)
+        version: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)
       eslint-plugin-vue:
         specifier: 10.0.0
-        version: 10.0.0(eslint@9.21.0)(vue-eslint-parser@10.1.1(eslint@9.21.0))
+        version: 10.0.0(eslint@9.22.0)(vue-eslint-parser@10.1.1(eslint@9.22.0))
       fast-glob:
         specifier: 3.3.3
         version: 3.3.3
@@ -1223,7 +1226,7 @@ importers:
         version: 2.2.8
       vue-eslint-parser:
         specifier: 10.1.1
-        version: 10.1.1(eslint@9.21.0)
+        version: 10.1.1(eslint@9.22.0)
       vue-tsc:
         specifier: 2.2.8
         version: 2.2.8(typescript@5.8.2)
@@ -1242,16 +1245,16 @@ importers:
         version: 22.13.9
       '@typescript-eslint/eslint-plugin':
         specifier: 8.26.0
-        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)
       '@typescript-eslint/parser':
         specifier: 8.26.0
-        version: 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       esbuild:
         specifier: 0.25.0
         version: 0.25.0
       eslint-plugin-vue:
         specifier: 10.0.0
-        version: 10.0.0(eslint@9.21.0)(vue-eslint-parser@10.1.1(eslint@9.21.0))
+        version: 10.0.0(eslint@9.22.0)(vue-eslint-parser@10.1.1(eslint@9.22.0))
       nodemon:
         specifier: 3.1.9
         version: 3.1.9
@@ -1260,7 +1263,7 @@ importers:
         version: 5.8.2
       vue-eslint-parser:
         specifier: 10.1.1
-        version: 10.1.1(eslint@9.21.0)
+        version: 10.1.1(eslint@9.22.0)
 
   packages/misskey-bubble-game:
     dependencies:
@@ -1285,10 +1288,10 @@ importers:
         version: 3.0.8
       '@typescript-eslint/eslint-plugin':
         specifier: 8.26.0
-        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)
       '@typescript-eslint/parser':
         specifier: 8.26.0
-        version: 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       esbuild:
         specifier: 0.25.0
         version: 0.25.0
@@ -1331,10 +1334,10 @@ importers:
         version: 22.13.9
       '@typescript-eslint/eslint-plugin':
         specifier: 8.26.0
-        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)
       '@typescript-eslint/parser':
         specifier: 8.26.0
-        version: 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       esbuild:
         specifier: 0.25.0
         version: 0.25.0
@@ -1379,10 +1382,10 @@ importers:
         version: 22.13.9
       '@typescript-eslint/eslint-plugin':
         specifier: 8.26.0
-        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)
       '@typescript-eslint/parser':
         specifier: 8.26.0
-        version: 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       openapi-types:
         specifier: 12.1.3
         version: 12.1.3
@@ -1410,10 +1413,10 @@ importers:
         version: 22.13.9
       '@typescript-eslint/eslint-plugin':
         specifier: 8.26.0
-        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)
       '@typescript-eslint/parser':
         specifier: 8.26.0
-        version: 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       esbuild:
         specifier: 0.25.0
         version: 0.25.0
@@ -1444,13 +1447,13 @@ importers:
     devDependencies:
       '@typescript-eslint/parser':
         specifier: 8.26.0
-        version: 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+        version: 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       '@typescript/lib-webworker':
         specifier: npm:@types/serviceworker@0.0.74
         version: '@types/serviceworker@0.0.74'
       eslint-plugin-import:
         specifier: 2.31.0
-        version: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)
+        version: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)
       nodemon:
         specifier: 3.1.9
         version: 3.1.9
@@ -2108,6 +2111,10 @@ packages:
     resolution: {integrity: sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
+  '@eslint/config-helpers@0.1.0':
+    resolution: {integrity: sha512-kLrdPDJE1ckPo94kmPPf9Hfd0DU0Jw6oKYrhe+pwSC0iTUInmTa+w6fw8sGgcfkFJGNdWOUeOaDM4quW4a7OkA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
   '@eslint/core@0.12.0':
     resolution: {integrity: sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -2116,8 +2123,8 @@ packages:
     resolution: {integrity: sha512-yaVPAiNAalnCZedKLdR21GOGILMLKPyqSLWaAjQFvYA2i/ciDi8ArYVr69Anohb6cH2Ukhqti4aFnYyPm8wdwQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  '@eslint/js@9.21.0':
-    resolution: {integrity: sha512-BqStZ3HX8Yz6LvsF5ByXYrtigrV5AXADWLAGc7PH/1SxOb7/FIYYMszZZWiUou/GB9P2lXWk2SV4d+Z8h0nknw==}
+  '@eslint/js@9.22.0':
+    resolution: {integrity: sha512-vLFajx9o8d1/oL2ZkpMYbkLv8nDB6yaIwFNt7nI4+I80U/z03SxmfOMsLbvWr3p7C+Wnoh//aOu2pQW8cS0HCQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
   '@eslint/object-schema@2.1.6':
@@ -4277,6 +4284,9 @@ packages:
   '@types/node-fetch@2.6.12':
     resolution: {integrity: sha512-8nneRWKCg3rMtF69nLQJnOYUcbafYeFSjqkw3jCRLsqkWFlHaoQrr5mXmofFGOx3DKn7UfmBMyov8ySvLRVldA==}
 
+  '@types/node@22.13.10':
+    resolution: {integrity: sha512-I6LPUvlRH+O6VRUqYOcMudhaIdUVWfsjnZavnsraHvpBwaEyMN29ry+0UVJhImYL16xsscu0aske3yA+uPOWfw==}
+
   '@types/node@22.13.4':
     resolution: {integrity: sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==}
 
@@ -6090,6 +6100,10 @@ packages:
     resolution: {integrity: sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
+  eslint-scope@8.3.0:
+    resolution: {integrity: sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
   eslint-visitor-keys@3.4.3:
     resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
@@ -6098,8 +6112,8 @@ packages:
     resolution: {integrity: sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
 
-  eslint@9.21.0:
-    resolution: {integrity: sha512-KjeihdFqTPhOMXTt7StsDxriV4n66ueuF/jfPNC3j/lduHwr/ijDwJMsF+wyMJethgiKi5wniIE243vi07d3pg==}
+  eslint@9.22.0:
+    resolution: {integrity: sha512-9V/QURhsRN40xuHXWjV64yvrzMjcz7ZyNoF2jJFmy9j/SLk0u1OLSZgXi28MrXjymnjEGSR80WCdab3RGMDveQ==}
     engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
     hasBin: true
     peerDependencies:
@@ -10381,6 +10395,10 @@ packages:
   walker@1.0.8:
     resolution: {integrity: sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==}
 
+  wanakana@5.3.1:
+    resolution: {integrity: sha512-OSDqupzTlzl2LGyqTdhcXcl6ezMiFhcUwLBP8YKaBIbMYW1wAwDvupw2T9G9oVaKT9RmaSpyTXjxddFPUcFFIw==}
+    engines: {node: '>=12'}
+
   web-push@3.6.7:
     resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==}
     engines: {node: '>= 16'}
@@ -11567,16 +11585,16 @@ snapshots:
   '@esbuild/win32-x64@0.25.0':
     optional: true
 
-  '@eslint-community/eslint-utils@4.4.1(eslint@9.21.0)':
+  '@eslint-community/eslint-utils@4.4.1(eslint@9.22.0)':
     dependencies:
-      eslint: 9.21.0
+      eslint: 9.22.0
       eslint-visitor-keys: 3.4.3
 
   '@eslint-community/regexpp@4.12.1': {}
 
-  '@eslint/compat@1.2.6(eslint@9.21.0)':
+  '@eslint/compat@1.2.6(eslint@9.22.0)':
     optionalDependencies:
-      eslint: 9.21.0
+      eslint: 9.22.0
 
   '@eslint/config-array@0.19.2':
     dependencies:
@@ -11586,6 +11604,8 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@eslint/config-helpers@0.1.0': {}
+
   '@eslint/core@0.12.0':
     dependencies:
       '@types/json-schema': 7.0.15
@@ -11604,7 +11624,7 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@eslint/js@9.21.0': {}
+  '@eslint/js@9.22.0': {}
 
   '@eslint/object-schema@2.1.6': {}
 
@@ -11884,7 +11904,7 @@ snapshots:
   '@jest/console@29.7.0':
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       chalk: 4.1.2
       jest-message-util: 29.7.0
       jest-util: 29.7.0
@@ -11897,14 +11917,14 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       ci-info: 3.9.0
       exit: 0.1.2
       graceful-fs: 4.2.11
       jest-changed-files: 29.7.0
-      jest-config: 29.7.0(@types/node@22.13.9)
+      jest-config: 29.7.0(@types/node@22.13.10)
       jest-haste-map: 29.7.0
       jest-message-util: 29.7.0
       jest-regex-util: 29.6.3
@@ -11933,7 +11953,7 @@ snapshots:
     dependencies:
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       jest-mock: 29.7.0
 
   '@jest/expect-utils@29.7.0':
@@ -11951,7 +11971,7 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@sinonjs/fake-timers': 10.3.0
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       jest-message-util: 29.7.0
       jest-mock: 29.7.0
       jest-util: 29.7.0
@@ -11973,7 +11993,7 @@ snapshots:
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
       '@jridgewell/trace-mapping': 0.3.25
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       chalk: 4.1.2
       collect-v8-coverage: 1.0.2
       exit: 0.1.2
@@ -12043,7 +12063,7 @@ snapshots:
       '@jest/schemas': 29.6.3
       '@types/istanbul-lib-coverage': 2.0.6
       '@types/istanbul-reports': 3.0.4
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       '@types/yargs': 17.0.33
       chalk: 4.1.2
 
@@ -12153,14 +12173,14 @@ snapshots:
 
   '@misskey-dev/browser-image-resizer@2024.1.0': {}
 
-  '@misskey-dev/eslint-plugin@2.1.0(@eslint/compat@1.2.6(eslint@9.21.0))(@stylistic/eslint-plugin@3.1.0(eslint@9.21.0)(typescript@5.8.2))(@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2))(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0))(eslint@9.21.0)(globals@16.0.0)':
+  '@misskey-dev/eslint-plugin@2.1.0(@eslint/compat@1.2.6(eslint@9.22.0))(@stylistic/eslint-plugin@3.1.0(eslint@9.22.0)(typescript@5.8.2))(@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2))(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0))(eslint@9.22.0)(globals@16.0.0)':
     dependencies:
-      '@eslint/compat': 1.2.6(eslint@9.21.0)
-      '@stylistic/eslint-plugin': 3.1.0(eslint@9.21.0)(typescript@5.8.2)
-      '@typescript-eslint/eslint-plugin': 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)
-      '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2)
-      eslint: 9.21.0
-      eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)
+      '@eslint/compat': 1.2.6(eslint@9.22.0)
+      '@stylistic/eslint-plugin': 3.1.0(eslint@9.22.0)(typescript@5.8.2)
+      '@typescript-eslint/eslint-plugin': 8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)
+      '@typescript-eslint/parser': 8.26.0(eslint@9.22.0)(typescript@5.8.2)
+      eslint: 9.22.0
+      eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)
       globals: 16.0.0
 
   '@misskey-dev/sharp-read-bmp@1.2.0':
@@ -13751,10 +13771,10 @@ snapshots:
       vue: 3.5.13(typescript@5.8.2)
       vue-component-type-helpers: 2.2.8
 
-  '@stylistic/eslint-plugin@3.1.0(eslint@9.21.0)(typescript@5.8.2)':
+  '@stylistic/eslint-plugin@3.1.0(eslint@9.22.0)(typescript@5.8.2)':
     dependencies:
-      '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2)
-      eslint: 9.21.0
+      '@typescript-eslint/utils': 8.26.0(eslint@9.22.0)(typescript@5.8.2)
+      eslint: 9.22.0
       eslint-visitor-keys: 4.2.0
       espree: 10.3.0
       estraverse: 5.3.0
@@ -14004,7 +14024,7 @@ snapshots:
 
   '@types/accepts@1.3.7':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/archiver@6.0.3':
     dependencies:
@@ -14042,7 +14062,7 @@ snapshots:
   '@types/body-parser@1.19.5':
     dependencies:
       '@types/connect': 3.4.38
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/braces@3.0.5': {}
 
@@ -14056,11 +14076,11 @@ snapshots:
 
   '@types/connect@3.4.36':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/connect@3.4.38':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/content-disposition@0.5.8': {}
 
@@ -14085,7 +14105,7 @@ snapshots:
 
   '@types/express-serve-static-core@5.0.6':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       '@types/qs': 6.9.18
       '@types/range-parser': 1.2.7
       '@types/send': 0.17.4
@@ -14099,11 +14119,11 @@ snapshots:
 
   '@types/fluent-ffmpeg@2.1.27':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/graceful-fs@4.1.9':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/hammerjs@2.0.46': {}
 
@@ -14119,7 +14139,7 @@ snapshots:
 
   '@types/http-link-header@1.0.7':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/istanbul-lib-coverage@2.0.6': {}
 
@@ -14140,7 +14160,7 @@ snapshots:
 
   '@types/jsdom@21.1.7':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       '@types/tough-cookie': 4.0.5
       parse5: 7.2.1
 
@@ -14176,13 +14196,17 @@ snapshots:
 
   '@types/mysql@2.15.26':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/node-fetch@2.6.12':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       form-data: 4.0.2
 
+  '@types/node@22.13.10':
+    dependencies:
+      undici-types: 6.20.0
+
   '@types/node@22.13.4':
     dependencies:
       undici-types: 6.20.0
@@ -14193,7 +14217,7 @@ snapshots:
 
   '@types/nodemailer@6.4.17':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/normalize-package-data@2.4.4': {}
 
@@ -14204,11 +14228,11 @@ snapshots:
   '@types/oauth2orize@1.11.5':
     dependencies:
       '@types/express': 5.0.0
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/oauth@0.9.6':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/offscreencanvas@2019.3.0': {}
 
@@ -14220,13 +14244,13 @@ snapshots:
 
   '@types/pg@8.11.11':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       pg-protocol: 1.7.1
       pg-types: 4.0.2
 
   '@types/pg@8.6.1':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       pg-protocol: 1.7.1
       pg-types: 2.2.0
 
@@ -14236,7 +14260,7 @@ snapshots:
 
   '@types/qrcode@1.5.5':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/qs@6.9.18': {}
 
@@ -14252,7 +14276,7 @@ snapshots:
 
   '@types/readdir-glob@1.1.5':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/rename@1.0.7': {}
 
@@ -14271,7 +14295,7 @@ snapshots:
   '@types/send@0.17.4':
     dependencies:
       '@types/mime': 1.3.5
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/serve-static@1.15.7':
     dependencies:
@@ -14301,7 +14325,7 @@ snapshots:
 
   '@types/tedious@4.0.14':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/throttle-debounce@5.0.2': {}
 
@@ -14317,19 +14341,19 @@ snapshots:
 
   '@types/vary@1.1.3':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/web-push@3.6.4':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/ws@8.18.0':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/ws@8.5.14':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
 
   '@types/yargs-parser@21.0.3': {}
 
@@ -14339,18 +14363,18 @@ snapshots:
 
   '@types/yauzl@2.10.3':
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
     optional: true
 
-  '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.21.0)(typescript@5.7.3))(eslint@9.21.0)(typescript@5.7.3)':
+  '@typescript-eslint/eslint-plugin@8.24.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0)(typescript@5.7.3)':
     dependencies:
       '@eslint-community/regexpp': 4.12.1
-      '@typescript-eslint/parser': 8.24.0(eslint@9.21.0)(typescript@5.7.3)
+      '@typescript-eslint/parser': 8.24.0(eslint@9.22.0)(typescript@5.7.3)
       '@typescript-eslint/scope-manager': 8.24.0
-      '@typescript-eslint/type-utils': 8.24.0(eslint@9.21.0)(typescript@5.7.3)
-      '@typescript-eslint/utils': 8.24.0(eslint@9.21.0)(typescript@5.7.3)
+      '@typescript-eslint/type-utils': 8.24.0(eslint@9.22.0)(typescript@5.7.3)
+      '@typescript-eslint/utils': 8.24.0(eslint@9.22.0)(typescript@5.7.3)
       '@typescript-eslint/visitor-keys': 8.24.0
-      eslint: 9.21.0
+      eslint: 9.22.0
       graphemer: 1.4.0
       ignore: 5.3.2
       natural-compare: 1.4.0
@@ -14359,15 +14383,15 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0)(typescript@5.8.2)':
+  '@typescript-eslint/eslint-plugin@8.26.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0)(typescript@5.8.2)':
     dependencies:
       '@eslint-community/regexpp': 4.12.1
-      '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+      '@typescript-eslint/parser': 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       '@typescript-eslint/scope-manager': 8.26.0
-      '@typescript-eslint/type-utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2)
-      '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+      '@typescript-eslint/type-utils': 8.26.0(eslint@9.22.0)(typescript@5.8.2)
+      '@typescript-eslint/utils': 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       '@typescript-eslint/visitor-keys': 8.26.0
-      eslint: 9.21.0
+      eslint: 9.22.0
       graphemer: 1.4.0
       ignore: 5.3.2
       natural-compare: 1.4.0
@@ -14376,26 +14400,26 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@8.24.0(eslint@9.21.0)(typescript@5.7.3)':
+  '@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3)':
     dependencies:
       '@typescript-eslint/scope-manager': 8.24.0
       '@typescript-eslint/types': 8.24.0
       '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
       '@typescript-eslint/visitor-keys': 8.24.0
       debug: 4.4.0(supports-color@8.1.1)
-      eslint: 9.21.0
+      eslint: 9.22.0
       typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2)':
+  '@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2)':
     dependencies:
       '@typescript-eslint/scope-manager': 8.26.0
       '@typescript-eslint/types': 8.26.0
       '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2)
       '@typescript-eslint/visitor-keys': 8.26.0
       debug: 4.4.0(supports-color@8.1.1)
-      eslint: 9.21.0
+      eslint: 9.22.0
       typescript: 5.8.2
     transitivePeerDependencies:
       - supports-color
@@ -14410,23 +14434,23 @@ snapshots:
       '@typescript-eslint/types': 8.26.0
       '@typescript-eslint/visitor-keys': 8.26.0
 
-  '@typescript-eslint/type-utils@8.24.0(eslint@9.21.0)(typescript@5.7.3)':
+  '@typescript-eslint/type-utils@8.24.0(eslint@9.22.0)(typescript@5.7.3)':
     dependencies:
       '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
-      '@typescript-eslint/utils': 8.24.0(eslint@9.21.0)(typescript@5.7.3)
+      '@typescript-eslint/utils': 8.24.0(eslint@9.22.0)(typescript@5.7.3)
       debug: 4.4.0(supports-color@8.1.1)
-      eslint: 9.21.0
+      eslint: 9.22.0
       ts-api-utils: 2.0.1(typescript@5.7.3)
       typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/type-utils@8.26.0(eslint@9.21.0)(typescript@5.8.2)':
+  '@typescript-eslint/type-utils@8.26.0(eslint@9.22.0)(typescript@5.8.2)':
     dependencies:
       '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2)
-      '@typescript-eslint/utils': 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+      '@typescript-eslint/utils': 8.26.0(eslint@9.22.0)(typescript@5.8.2)
       debug: 4.4.0(supports-color@8.1.1)
-      eslint: 9.21.0
+      eslint: 9.22.0
       ts-api-utils: 2.0.1(typescript@5.8.2)
       typescript: 5.8.2
     transitivePeerDependencies:
@@ -14464,24 +14488,24 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@8.24.0(eslint@9.21.0)(typescript@5.7.3)':
+  '@typescript-eslint/utils@8.24.0(eslint@9.22.0)(typescript@5.7.3)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0)
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0)
       '@typescript-eslint/scope-manager': 8.24.0
       '@typescript-eslint/types': 8.24.0
       '@typescript-eslint/typescript-estree': 8.24.0(typescript@5.7.3)
-      eslint: 9.21.0
+      eslint: 9.22.0
       typescript: 5.7.3
     transitivePeerDependencies:
       - supports-color
 
-  '@typescript-eslint/utils@8.26.0(eslint@9.21.0)(typescript@5.8.2)':
+  '@typescript-eslint/utils@8.26.0(eslint@9.22.0)(typescript@5.8.2)':
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0)
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0)
       '@typescript-eslint/scope-manager': 8.26.0
       '@typescript-eslint/types': 8.26.0
       '@typescript-eslint/typescript-estree': 8.26.0(typescript@5.8.2)
-      eslint: 9.21.0
+      eslint: 9.22.0
       typescript: 5.8.2
     transitivePeerDependencies:
       - supports-color
@@ -16349,27 +16373,27 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.21.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0):
+  eslint-module-utils@2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0):
     dependencies:
       debug: 3.2.7(supports-color@8.1.1)
     optionalDependencies:
-      '@typescript-eslint/parser': 8.24.0(eslint@9.21.0)(typescript@5.7.3)
-      eslint: 9.21.0
+      '@typescript-eslint/parser': 8.24.0(eslint@9.22.0)(typescript@5.7.3)
+      eslint: 9.22.0
       eslint-import-resolver-node: 0.3.9
     transitivePeerDependencies:
       - supports-color
 
-  eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0):
+  eslint-module-utils@2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0):
     dependencies:
       debug: 3.2.7(supports-color@8.1.1)
     optionalDependencies:
-      '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2)
-      eslint: 9.21.0
+      '@typescript-eslint/parser': 8.26.0(eslint@9.22.0)(typescript@5.8.2)
+      eslint: 9.22.0
       eslint-import-resolver-node: 0.3.9
     transitivePeerDependencies:
       - supports-color
 
-  eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.21.0)(typescript@5.7.3))(eslint@9.21.0):
+  eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint@9.22.0):
     dependencies:
       '@rtsao/scc': 1.1.0
       array-includes: 3.1.8
@@ -16378,9 +16402,9 @@ snapshots:
       array.prototype.flatmap: 1.3.3
       debug: 3.2.7(supports-color@8.1.1)
       doctrine: 2.1.0
-      eslint: 9.21.0
+      eslint: 9.22.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.21.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0)
+      eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.24.0(eslint@9.22.0)(typescript@5.7.3))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0)
       hasown: 2.0.2
       is-core-module: 2.16.1
       is-glob: 4.0.3
@@ -16392,13 +16416,13 @@ snapshots:
       string.prototype.trimend: 1.0.9
       tsconfig-paths: 3.15.0
     optionalDependencies:
-      '@typescript-eslint/parser': 8.24.0(eslint@9.21.0)(typescript@5.7.3)
+      '@typescript-eslint/parser': 8.24.0(eslint@9.22.0)(typescript@5.7.3)
     transitivePeerDependencies:
       - eslint-import-resolver-typescript
       - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint@9.21.0):
+  eslint-plugin-import@2.31.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint@9.22.0):
     dependencies:
       '@rtsao/scc': 1.1.0
       array-includes: 3.1.8
@@ -16407,9 +16431,9 @@ snapshots:
       array.prototype.flatmap: 1.3.3
       debug: 3.2.7(supports-color@8.1.1)
       doctrine: 2.1.0
-      eslint: 9.21.0
+      eslint: 9.22.0
       eslint-import-resolver-node: 0.3.9
-      eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.21.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.21.0)
+      eslint-module-utils: 2.12.0(@typescript-eslint/parser@8.26.0(eslint@9.22.0)(typescript@5.8.2))(eslint-import-resolver-node@0.3.9)(eslint@9.22.0)
       hasown: 2.0.2
       is-core-module: 2.16.1
       is-glob: 4.0.3
@@ -16421,21 +16445,21 @@ snapshots:
       string.prototype.trimend: 1.0.9
       tsconfig-paths: 3.15.0
     optionalDependencies:
-      '@typescript-eslint/parser': 8.26.0(eslint@9.21.0)(typescript@5.8.2)
+      '@typescript-eslint/parser': 8.26.0(eslint@9.22.0)(typescript@5.8.2)
     transitivePeerDependencies:
       - eslint-import-resolver-typescript
       - eslint-import-resolver-webpack
       - supports-color
 
-  eslint-plugin-vue@10.0.0(eslint@9.21.0)(vue-eslint-parser@10.1.1(eslint@9.21.0)):
+  eslint-plugin-vue@10.0.0(eslint@9.22.0)(vue-eslint-parser@10.1.1(eslint@9.22.0)):
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0)
-      eslint: 9.21.0
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0)
+      eslint: 9.22.0
       natural-compare: 1.4.0
       nth-check: 2.1.1
       postcss-selector-parser: 6.1.2
       semver: 7.7.1
-      vue-eslint-parser: 10.1.1(eslint@9.21.0)
+      vue-eslint-parser: 10.1.1(eslint@9.22.0)
       xml-name-validator: 4.0.0
 
   eslint-rule-docs@1.1.235: {}
@@ -16445,18 +16469,24 @@ snapshots:
       esrecurse: 4.3.0
       estraverse: 5.3.0
 
+  eslint-scope@8.3.0:
+    dependencies:
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
   eslint-visitor-keys@3.4.3: {}
 
   eslint-visitor-keys@4.2.0: {}
 
-  eslint@9.21.0:
+  eslint@9.22.0:
     dependencies:
-      '@eslint-community/eslint-utils': 4.4.1(eslint@9.21.0)
+      '@eslint-community/eslint-utils': 4.4.1(eslint@9.22.0)
       '@eslint-community/regexpp': 4.12.1
       '@eslint/config-array': 0.19.2
+      '@eslint/config-helpers': 0.1.0
       '@eslint/core': 0.12.0
       '@eslint/eslintrc': 3.3.0
-      '@eslint/js': 9.21.0
+      '@eslint/js': 9.22.0
       '@eslint/plugin-kit': 0.2.7
       '@humanfs/node': 0.16.6
       '@humanwhocodes/module-importer': 1.0.1
@@ -16468,7 +16498,7 @@ snapshots:
       cross-spawn: 7.0.6
       debug: 4.4.0(supports-color@8.1.1)
       escape-string-regexp: 4.0.0
-      eslint-scope: 8.2.0
+      eslint-scope: 8.3.0
       eslint-visitor-keys: 4.2.0
       espree: 10.3.0
       esquery: 1.6.0
@@ -17655,7 +17685,7 @@ snapshots:
       '@jest/expect': 29.7.0
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       chalk: 4.1.2
       co: 4.6.0
       dedent: 1.5.3
@@ -17713,6 +17743,36 @@ snapshots:
       - supports-color
       - ts-node
 
+  jest-config@29.7.0(@types/node@22.13.10):
+    dependencies:
+      '@babel/core': 7.26.9
+      '@jest/test-sequencer': 29.7.0
+      '@jest/types': 29.6.3
+      babel-jest: 29.7.0(@babel/core@7.26.9)
+      chalk: 4.1.2
+      ci-info: 3.9.0
+      deepmerge: 4.3.1
+      glob: 7.2.3
+      graceful-fs: 4.2.11
+      jest-circus: 29.7.0
+      jest-environment-node: 29.7.0
+      jest-get-type: 29.6.3
+      jest-regex-util: 29.6.3
+      jest-resolve: 29.7.0
+      jest-runner: 29.7.0
+      jest-util: 29.7.0
+      jest-validate: 29.7.0
+      micromatch: 4.0.8
+      parse-json: 5.2.0
+      pretty-format: 29.7.0
+      slash: 3.0.0
+      strip-json-comments: 3.1.1
+    optionalDependencies:
+      '@types/node': 22.13.10
+    transitivePeerDependencies:
+      - babel-plugin-macros
+      - supports-color
+
   jest-config@29.7.0(@types/node@22.13.4):
     dependencies:
       '@babel/core': 7.26.9
@@ -17797,7 +17857,7 @@ snapshots:
       '@jest/environment': 29.7.0
       '@jest/fake-timers': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       jest-mock: 29.7.0
       jest-util: 29.7.0
 
@@ -17814,7 +17874,7 @@ snapshots:
     dependencies:
       '@jest/types': 29.6.3
       '@types/graceful-fs': 4.1.9
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       anymatch: 3.1.3
       fb-watchman: 2.0.2
       graceful-fs: 4.2.11
@@ -17853,7 +17913,7 @@ snapshots:
   jest-mock@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       jest-util: 29.7.0
 
   jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@@ -17888,7 +17948,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       chalk: 4.1.2
       emittery: 0.13.1
       graceful-fs: 4.2.11
@@ -17916,7 +17976,7 @@ snapshots:
       '@jest/test-result': 29.7.0
       '@jest/transform': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       chalk: 4.1.2
       cjs-module-lexer: 1.4.3
       collect-v8-coverage: 1.0.2
@@ -17962,7 +18022,7 @@ snapshots:
   jest-util@29.7.0:
     dependencies:
       '@jest/types': 29.6.3
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       chalk: 4.1.2
       ci-info: 3.9.0
       graceful-fs: 4.2.11
@@ -17981,7 +18041,7 @@ snapshots:
     dependencies:
       '@jest/test-result': 29.7.0
       '@jest/types': 29.6.3
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       ansi-escapes: 4.3.2
       chalk: 4.1.2
       emittery: 0.13.1
@@ -17995,7 +18055,7 @@ snapshots:
 
   jest-worker@29.7.0:
     dependencies:
-      '@types/node': 22.13.9
+      '@types/node': 22.13.10
       jest-util: 29.7.0
       merge-stream: 2.0.0
       supports-color: 8.1.1
@@ -21384,10 +21444,10 @@ snapshots:
       vue: 3.5.13(typescript@5.8.2)
       vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.13(typescript@5.8.2))
 
-  vue-eslint-parser@10.1.1(eslint@9.21.0):
+  vue-eslint-parser@10.1.1(eslint@9.22.0):
     dependencies:
       debug: 4.4.0(supports-color@8.1.1)
-      eslint: 9.21.0
+      eslint: 9.22.0
       eslint-scope: 8.2.0
       eslint-visitor-keys: 4.2.0
       espree: 10.3.0
@@ -21440,6 +21500,8 @@ snapshots:
     dependencies:
       makeerror: 1.0.12
 
+  wanakana@5.3.1: {}
+
   web-push@3.6.7:
     dependencies:
       asn1.js: 5.4.1