From 8a3e33218e93b91fb953017b9a3d28c72dae8d21 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Sat, 25 Jul 2026 13:18:37 +0900 Subject: [PATCH] wip --- locales/ja-JP.yml | 4 +- packages/frontend/docs/preferences.md | 7 ++++ .../frontend/src/pages/settings/other.vue | 19 ++++++++- packages/frontend/src/preferences.ts | 39 ++----------------- packages/frontend/src/preferences/manager.ts | 37 ++++++------------ packages/frontend/src/preferences/utility.ts | 34 +++++++++++++++- packages/frontend/src/store.ts | 4 ++ packages/i18n/src/autogen/locale.ts | 10 ++++- 8 files changed, 87 insertions(+), 67 deletions(-) create mode 100644 packages/frontend/docs/preferences.md diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 2f6c912044..2fdab6862c 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1597,7 +1597,9 @@ _preferencesBackup: youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効にするにはプロファイル名の設定が必要です。" autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になっていません。" backupFound: "設定のバックアップが見つかりました" - forceBackup: "設定の強制バックアップ" + forceBackup: "今すぐバックアップ" + autoSync: "自動同期" + forceSync: "今すぐ同期" _accountSettings: requireSigninToViewContents: "コンテンツの表示にログインを必須にする" diff --git a/packages/frontend/docs/preferences.md b/packages/frontend/docs/preferences.md new file mode 100644 index 0000000000..f36fd1ceeb --- /dev/null +++ b/packages/frontend/docs/preferences.md @@ -0,0 +1,7 @@ +# Preferences system + +## ユースケース + +### (新しいデバイスなどで)既存のプロファイルを継承した新しいプロファイルを作りたい + +継承したいプロファイルをバックアップから復元した後、プロファイルの名前を変える。 diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue index 6ab645ed1c..f91bc48742 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -144,7 +144,17 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts._settings.settingsPersistence_title }} - {{ i18n.ts._preferencesBackup.forceBackup }} + + + + + +
+ {{ i18n.ts._preferencesBackup.forceBackup }} + {{ i18n.ts._preferencesBackup.forceSync }} +
+
+
@@ -170,7 +180,7 @@ import MkRolePreview from '@/components/MkRolePreview.vue'; import { signout } from '@/signout.js'; import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js'; import { suggestReload } from '@/utility/reload-suggest.js'; -import { cloudBackup } from '@/preferences/utility.js'; +import { cloudBackup, cloudSync } from '@/preferences/utility.js'; const $i = ensureSignin(); @@ -232,6 +242,11 @@ async function forceCloudBackup() { os.success(); } +async function forceCloudSync() { + await cloudSync(); + os.success(); +} + const headerActions = computed(() => []); const headerTabs = computed(() => []); diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts index 91866f1fbd..932e9fcdbb 100644 --- a/packages/frontend/src/preferences.ts +++ b/packages/frontend/src/preferences.ts @@ -3,7 +3,6 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { BroadcastChannel } from 'broadcast-channel'; import { createVisibilityAwareInterval } from '@@/js/interval.js'; import type { StorageProvider } from '@/preferences/manager.js'; import { cloudBackup } from '@/preferences/utility.js'; @@ -12,7 +11,6 @@ import { isSameScope, PreferencesManager } from '@/preferences/manager.js'; import { store } from '@/store.js'; import { $i } from '@/i.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -import { TAB_ID } from '@/tab-id.js'; // クラウド同期用グループ名 const syncGroup = 'default'; @@ -102,41 +100,10 @@ const io: StorageProvider = { export const prefer = new PreferencesManager(io, $i); //#region タブ間同期 -let latestPreferencesUpdate: { - tabId: string; - timestamp: number; -} | null = null; - -const preferencesChannel = new BroadcastChannel<{ - type: 'preferencesUpdate'; - tabId: string; - timestamp: number; -}>('preferences'); - -prefer.on('committed', () => { - latestPreferencesUpdate = { - tabId: TAB_ID, - timestamp: Date.now(), - }; - preferencesChannel.postMessage({ - type: 'preferencesUpdate', - tabId: TAB_ID, - timestamp: latestPreferencesUpdate.timestamp, - }); -}); - -preferencesChannel.addEventListener('message', (msg) => { - if (msg.type === 'preferencesUpdate') { - if (msg.tabId === TAB_ID) return; - if (latestPreferencesUpdate != null) { - if (msg.timestamp <= latestPreferencesUpdate.timestamp) return; - } +window.addEventListener('storage', (ev) => { + if (ev.key === 'preferences') { prefer.reloadProfile(); - if (_DEV_) console.log('prefer:received update from other tab'); - latestPreferencesUpdate = { - tabId: msg.tabId, - timestamp: msg.timestamp, - }; + if (_DEV_) console.log('prefer: received update from other tab'); } }); //#endregion diff --git a/packages/frontend/src/preferences/manager.ts b/packages/frontend/src/preferences/manager.ts index 05df4c70d9..363343d767 100644 --- a/packages/frontend/src/preferences/manager.ts +++ b/packages/frontend/src/preferences/manager.ts @@ -102,14 +102,6 @@ type PreferencesDefinitionRecord export type PreferencesDefinition = Record>; -type PreferencesManagerEvents = { - 'committed': (ctx: { - key: K; - value: ValueOf; - oldValue: ValueOf; - }) => void; -}; - export function definePreferences>(x: { [K in keyof T]: PreferencesDefinitionRecord }): { @@ -188,6 +180,10 @@ function normalizePreferences(preferences: PossiblyNonNormalizedPreferencesProfi return data as PreferencesProfile['preferences']; } +// eslint-disable-next-line @typescript-eslint/no-empty-object-type +type PreferencesManagerEvents = { +}; + // TODO: PreferencesManagerForGuest のような非ログイン専用のクラスを分離すればthis.currentAccountのnullチェックやaccountがnullであるスコープのレコード挿入などが不要になり綺麗になるかもしれない // と思ったけど操作アカウントが存在しない場合も考慮する現在の設計の方が汎用的かつ堅牢かもしれない // NOTE: accountDependentな設定は初期状態であってもアカウントごとのスコープでレコードを作成しておかないと、サーバー同期する際に正しく動作しなくなる @@ -262,21 +258,12 @@ export class PreferencesManager extends EventEmitter { const record = this.getMatchedRecordOf(key); - const _save = () => { - this.save(); - this.emit('committed', { - key, - value: v, - oldValue: this.s[key], - }); - }; - if (parseScope(record[0]).account == null && isAccountDependentKey(key) && currentAccount != null) { this.profile.preferences[key].push([makeScope({ server: host, account: currentAccount.id, }), v, {}]); - _save(); + this.save(); return; } @@ -284,12 +271,12 @@ export class PreferencesManager extends EventEmitter { this.profile.preferences[key].push([makeScope({ server: host, }), v, {}]); - _save(); + this.save(); return; } record[1] = v; - _save(); + this.save(); if (record[2].sync) { // awaitの必要なし @@ -554,20 +541,18 @@ export class PreferencesManager extends EventEmitter { } public reloadProfile() { - const newProfile = this.io.load(); - if (newProfile == null) return; + const freshProfile = this.io.load(); + if (freshProfile == null) return; this.profile = { - ...newProfile, - preferences: normalizePreferences(newProfile.preferences, this.currentAccount), + ...freshProfile, + preferences: normalizePreferences(freshProfile.preferences, this.currentAccount), }; const states = this.genStates(); for (const _key in states) { const key = _key as keyof PREF; this.rewriteRawState(key, states[key]); } - - this.fetchCloudValues(); } public getPerPrefMenu(key: K): MenuItem[] { diff --git a/packages/frontend/src/preferences/utility.ts b/packages/frontend/src/preferences/utility.ts index 7507a071f4..e712d46534 100644 --- a/packages/frontend/src/preferences/utility.ts +++ b/packages/frontend/src/preferences/utility.ts @@ -42,6 +42,16 @@ export function getPreferencesProfileMenu(): MenuItem[] { } }); + const autoSyncEnabled = ref(store.s.enablePreferencesAutoCloudSync); + + watch(autoSyncEnabled, () => { + if (autoSyncEnabled.value) { + store.set('enablePreferencesAutoCloudSync', true); + } else { + store.set('enablePreferencesAutoCloudSync', false); + } + }); + const menu: MenuItem[] = [{ type: 'label', text: prefer.profile.name || `(${i18n.ts.noName})`, @@ -56,6 +66,11 @@ export function getPreferencesProfileMenu(): MenuItem[] { icon: 'ti ti-cloud-up', text: i18n.ts._preferencesBackup.autoBackup, ref: autoBackupEnabled, + }, { + type: 'switch', + icon: 'ti ti-cloud-down', + text: i18n.ts._preferencesBackup.autoSync, + ref: autoSyncEnabled, }, { text: i18n.ts.export, icon: 'ti ti-download', @@ -139,6 +154,24 @@ function importProfile() { input.click(); } +export async function cloudSync() { + if ($i == null) return; + + const cloudProfile = await misskeyApi('i/registry/get', { + scope: ['client', 'preferences', 'backups'], + key: prefer.profile.name, + }) as PreferencesProfile | null; + + if (cloudProfile == null || cloudProfile.modifiedAt < prefer.profile.modifiedAt) { + await cloudBackup(); + return; + } + + miLocalStorage.setItem('preferences', JSON.stringify(cloudProfile)); + + prefer.reloadProfile(); +} + export async function cloudBackup() { if ($i == null) return; if (!canAutoBackup()) { @@ -186,7 +219,6 @@ export async function restoreFromCloudBackup() { const select = await os.select({ title: i18n.ts._preferencesBackup.selectBackupToRestore, - text: 'ℹ️ ' + i18n.ts._preferencesProfile.shareSameProfileBetweenDevicesIsNotRecommended + ' ' + i18n.ts._preferencesProfile.useSyncBetweenDevicesOptionIfYouWantToSyncSetting, items: backups.map(backup => ({ label: backup.name, value: backup.name, diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 7017725e30..84ed36abcd 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -110,6 +110,10 @@ export const store = markRaw(new Pizzax('base', { where: 'device', default: false, }, + enablePreferencesAutoCloudSync: { + where: 'device', + default: false, + }, showPreferencesAutoCloudBackupSuggestion: { where: 'device', default: true, diff --git a/packages/i18n/src/autogen/locale.ts b/packages/i18n/src/autogen/locale.ts index 5103f3845f..33fdfdd1ff 100644 --- a/packages/i18n/src/autogen/locale.ts +++ b/packages/i18n/src/autogen/locale.ts @@ -6336,9 +6336,17 @@ export interface Locale extends ILocale { */ "backupFound": string; /** - * 設定の強制バックアップ + * 今すぐバックアップ */ "forceBackup": string; + /** + * 自動同期 + */ + "autoSync": string; + /** + * 今すぐ同期 + */ + "forceSync": string; }; "_accountSettings": { /**