1
0
mirror of https://github.com/misskey-dev/misskey.git synced 2026-07-25 23:44:57 +02:00
This commit is contained in:
syuilo
2026-07-25 13:18:37 +09:00
parent c70fb20b9e
commit 8a3e33218e
8 changed files with 87 additions and 67 deletions

View File

@@ -1597,7 +1597,9 @@ _preferencesBackup:
youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効にするにはプロファイル名の設定が必要です。"
autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になっていません。"
backupFound: "設定のバックアップが見つかりました"
forceBackup: "設定の強制バックアップ"
forceBackup: "今すぐバックアップ"
autoSync: "自動同期"
forceSync: "今すぐ同期"
_accountSettings:
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"

View File

@@ -0,0 +1,7 @@
# Preferences system
## ユースケース
### (新しいデバイスなどで)既存のプロファイルを継承した新しいプロファイルを作りたい
継承したいプロファイルをバックアップから復元した後、プロファイルの名前を変える。

View File

@@ -144,7 +144,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton v-if="storagePersistenceSupported && !storagePersisted" @click="enableStoragePersistence">{{ i18n.ts._settings.settingsPersistence_title }}</MkButton>
<MkButton @click="forceCloudBackup">{{ i18n.ts._preferencesBackup.forceBackup }}</MkButton>
<SearchMarker :keywords="['profile', 'preferences']">
<MkFolder>
<template #icon><SearchIcon><i class="ti ti-cogs"></i></SearchIcon></template>
<template #label><SearchLabel>{{ i18n.ts.preferencesProfile }}</SearchLabel></template>
<div class="_buttons">
<MkButton @click="forceCloudBackup">{{ i18n.ts._preferencesBackup.forceBackup }}</MkButton>
<MkButton @click="forceCloudSync">{{ i18n.ts._preferencesBackup.forceSync }}</MkButton>
</div>
</MkFolder>
</SearchMarker>
</div>
</SearchMarker>
</template>
@@ -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(() => []);

View File

@@ -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

View File

@@ -102,14 +102,6 @@ type PreferencesDefinitionRecord<Default, T = Default extends (...args: any) =>
export type PreferencesDefinition = Record<string, PreferencesDefinitionRecord<any>>;
type PreferencesManagerEvents = {
'committed': <K extends keyof PREF>(ctx: {
key: K;
value: ValueOf<K>;
oldValue: ValueOf<K>;
}) => void;
};
export function definePreferences<T extends Record<string, unknown>>(x: {
[K in keyof T]: PreferencesDefinitionRecord<T[K]>
}): {
@@ -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<PreferencesManagerEvents> {
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<PreferencesManagerEvents> {
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<PreferencesManagerEvents> {
}
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<K extends keyof PREF>(key: K): MenuItem[] {

View File

@@ -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,

View File

@@ -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,

View File

@@ -6336,9 +6336,17 @@ export interface Locale extends ILocale {
*/
"backupFound": string;
/**
* 設定の強制バックアップ
* 今すぐバックアップ
*/
"forceBackup": string;
/**
* 自動同期
*/
"autoSync": string;
/**
* 今すぐ同期
*/
"forceSync": string;
};
"_accountSettings": {
/**