From f8e244f48d8382b9024a384f29605326ee4abef3 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Wed, 12 Mar 2025 14:34:10 +0900 Subject: [PATCH] =?UTF-8?q?enhance(frontend):=20=E3=82=A2=E3=82=AB?= =?UTF-8?q?=E3=82=A6=E3=83=B3=E3=83=88=E3=82=AA=E3=83=BC=E3=83=90=E3=83=BC?= =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=89=E8=A8=AD=E5=AE=9A=E3=81=A8=E3=83=87?= =?UTF-8?q?=E3=83=90=E3=82=A4=E3=82=B9=E9=96=93=E5=90=8C=E6=9C=9F=E3=81=AE?= =?UTF-8?q?=E4=BD=B5=E7=94=A8=E3=81=AB=E5=AF=BE=E5=BF=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/components/MkPreferenceContainer.vue | 20 +++++---- packages/frontend/src/preferences.ts | 42 +++++++++++++++---- packages/frontend/src/preferences/profile.ts | 41 ++++++++++-------- 3 files changed, 72 insertions(+), 31 deletions(-) diff --git a/packages/frontend/src/components/MkPreferenceContainer.vue b/packages/frontend/src/components/MkPreferenceContainer.vue index 40a9a4dca7..acdd2a8d3b 100644 --- a/packages/frontend/src/components/MkPreferenceContainer.vue +++ b/packages/frontend/src/components/MkPreferenceContainer.vue @@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<div :class="$style.root"> +<div :class="$style.root" @contextmenu.prevent.stop="showMenu($event, true)"> <div :class="$style.body"> <slot></slot> </div> @@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only <i v-if="isSyncEnabled" class="ti ti-cloud-cog" style="color: var(--MI_THEME-accent); opacity: 0.7;"></i> <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> + <button class="_button" style="color: var(--MI_THEME-fg)" @click="showMenu($event)"><i class="ti ti-dots"></i></button> </div> </div> </div> @@ -32,16 +32,22 @@ const props = withDefaults(defineProps<{ const isAccountOverrided = ref(prefer.isAccountOverrided(props.k)); const isSyncEnabled = ref(prefer.isSyncEnabled(props.k)); -function showMenu(ev: MouseEvent) { +function showMenu(ev: MouseEvent, contextmenu?: boolean) { const i = window.setInterval(() => { isAccountOverrided.value = prefer.isAccountOverrided(props.k); isSyncEnabled.value = prefer.isSyncEnabled(props.k); }, 100); - os.popupMenu(prefer.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, { - onClosing: () => { + if (contextmenu) { + os.contextMenu(prefer.getPerPrefMenu(props.k), ev).then(() => { window.clearInterval(i); - }, - }); + }); + } else { + os.popupMenu(prefer.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, { + onClosing: () => { + window.clearInterval(i); + }, + }); + } } </script> diff --git a/packages/frontend/src/preferences.ts b/packages/frontend/src/preferences.ts index ab234a926a..474abe22ab 100644 --- a/packages/frontend/src/preferences.ts +++ b/packages/frontend/src/preferences.ts @@ -7,7 +7,7 @@ import { v4 as uuid } from 'uuid'; import type { PreferencesProfile, StorageProvider } from '@/preferences/profile.js'; import { cloudBackup } from '@/preferences/utility.js'; import { miLocalStorage } from '@/local-storage.js'; -import { ProfileManager } from '@/preferences/profile.js'; +import { isSameCond, ProfileManager } from '@/preferences/profile.js'; import { store } from '@/store.js'; import { $i } from '@/account.js'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -28,22 +28,27 @@ function createProfileManager(storageProvider: StorageProvider) { return new ProfileManager(profile, storageProvider); } +const syncGroup = 'default'; + const storageProvider: StorageProvider = { save: (ctx) => { miLocalStorage.setItem('preferences', JSON.stringify(ctx.profile)); miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`); }, + cloudGet: async (ctx) => { // TODO: この取得方法だとアカウントが変わると保存場所も変わってしまうので改修する // 例えば複数アカウントある場合でも設定値を保存するための「プライマリアカウント」を設定できるようにするとか // TODO: keyのcondに応じた取得 try { - const value = await misskeyApi('i/registry/get', { + const cloudData = await misskeyApi('i/registry/get', { scope: ['client', 'preferences', 'sync'], - key: ctx.key, - }); + key: syncGroup + ':' + ctx.key, + }) as [any, any][]; + const target = cloudData.find(([cond]) => isSameCond(cond, ctx.cond)); + if (target == null) return null; return { - value, + value: target[1], }; } catch (err: any) { if (err.code === 'NO_SUCH_KEY') { @@ -53,11 +58,34 @@ const storageProvider: StorageProvider = { } } }, + cloudSet: async (ctx) => { + let cloudData: [any, any][] = []; + try { + cloudData = await misskeyApi('i/registry/get', { + scope: ['client', 'preferences', 'sync'], + key: syncGroup + ':' + ctx.key, + }) as [any, any][]; + } catch (err: any) { + if (err.code === 'NO_SUCH_KEY') { + cloudData = []; + } else { + throw err; + } + } + + const i = cloudData.findIndex(([cond]) => isSameCond(cond, ctx.cond)); + + if (i === -1) { + cloudData.push([ctx.cond, ctx.value]); + } else { + cloudData[i] = [ctx.cond, ctx.value]; + } + await misskeyApi('i/registry/set', { scope: ['client', 'preferences', 'sync'], - key: ctx.key, - value: ctx.value, + key: syncGroup + ':' + ctx.key, + value: cloudData, }); }, }; diff --git a/packages/frontend/src/preferences/profile.ts b/packages/frontend/src/preferences/profile.ts index fc8057540a..de1c674e5c 100644 --- a/packages/frontend/src/preferences/profile.ts +++ b/packages/frontend/src/preferences/profile.ts @@ -60,6 +60,12 @@ function makeCond(cond: Partial<{ return c; } +export function isSameCond(a: Cond, b: Cond): boolean { + // null と undefined (キー無し) は区別したくないので == で比較 + // eslint-disable-next-line eqeqeq + return a.server == b.server && a.account == b.account && a.device == b.device; +} + export type PreferencesProfile = { id: string; version: string; @@ -73,8 +79,8 @@ export type PreferencesProfile = { export type StorageProvider = { save: (ctx: { profile: PreferencesProfile; }) => void; - cloudGet: <K extends keyof PREF>(ctx: { key: K; }) => Promise<{ value: ValueOf<K>; } | null>; - cloudSet: <K extends keyof PREF>(ctx: { key: K; value: ValueOf<K>; }) => Promise<void>; + cloudGet: <K extends keyof PREF>(ctx: { key: K; cond: Cond; }) => Promise<{ value: ValueOf<K>; } | null>; + cloudSet: <K extends keyof PREF>(ctx: { key: K; cond: Cond; value: ValueOf<K>; }) => Promise<void>; }; export class ProfileManager { @@ -121,7 +127,7 @@ export class ProfileManager { this.rewriteRawState(key, value); - const record = this.getMatchedRecord(key); + const record = this.getMatchedRecordOf(key); if (parseCond(record[0]).account == null && PREF_DEF[key].accountDependent) { this.profile.preferences[key].push([makeCond({ account: `${host}/${$i!.id}`, @@ -130,14 +136,14 @@ export class ProfileManager { return; } + record[1] = value; + this.save(); + if (record[2].sync) { // awaitの必要なし // TODO: リクエストを間引く - this.storageProvider.cloudSet({ key, value }); + this.storageProvider.cloudSet({ key, cond: record[0], value: record[1] }); } - - record[1] = value; - this.save(); } /** @@ -180,7 +186,7 @@ export class ProfileManager { private genStates() { const states = {} as { [K in keyof PREF]: ValueOf<K> }; for (const key in PREF_DEF) { - const record = this.getMatchedRecord(key); + const record = this.getMatchedRecordOf(key); states[key] = record[1]; } @@ -192,9 +198,9 @@ export class ProfileManager { const promises: Promise<void>[] = []; for (const key in PREF_DEF) { - const record = this.getMatchedRecord(key); + const record = this.getMatchedRecordOf(key); if (record[2].sync) { - const getting = this.storageProvider.cloudGet({ key }); + const getting = this.storageProvider.cloudGet({ key, cond: record[0] }); promises.push(getting.then((res) => { if (res == null) return; const value = res.value; @@ -261,7 +267,7 @@ export class ProfileManager { this.storageProvider.save({ profile: this.profile }); } - public getMatchedRecord<K extends keyof PREF>(key: K): PrefRecord<K> { + public getMatchedRecordOf<K extends keyof PREF>(key: K): PrefRecord<K> { const records = this.profile.preferences[key]; if ($i == null) return records.find(([cond, v]) => parseCond(cond).account == null)!; @@ -302,19 +308,21 @@ export class ProfileManager { records.splice(index, 1); - this.rewriteRawState(key, this.getMatchedRecord(key)[1]); + this.rewriteRawState(key, this.getMatchedRecordOf(key)[1]); this.save(); } public isSyncEnabled<K extends keyof PREF>(key: K): boolean { - return this.getMatchedRecord(key)[2].sync ?? false; + return this.getMatchedRecordOf(key)[2].sync ?? false; } public async enableSync<K extends keyof PREF>(key: K): Promise<{ enabled: boolean; } | null> { if (this.isSyncEnabled(key)) return Promise.resolve(null); - const existing = await this.storageProvider.cloudGet({ key }); + const record = this.getMatchedRecordOf(key); + + const existing = await this.storageProvider.cloudGet({ key, cond: record[0] }); if (existing != null) { const { canceled, result } = await os.select({ title: i18n.ts.preferenceSyncConflictTitle, @@ -340,12 +348,11 @@ export class ProfileManager { } } - const record = this.getMatchedRecord(key); record[2].sync = true; this.save(); // awaitの必要性は無い - this.storageProvider.cloudSet({ key, value: this.s[key] }); + this.storageProvider.cloudSet({ key, cond: record[0], value: this.s[key] }); return { enabled: true }; } @@ -353,7 +360,7 @@ export class ProfileManager { public disableSync<K extends keyof PREF>(key: K) { if (!this.isSyncEnabled(key)) return; - const record = this.getMatchedRecord(key); + const record = this.getMatchedRecordOf(key); delete record[2].sync; this.save(); }