mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-07-25 23:44:57 +02:00
wip
This commit is contained in:
@@ -1597,7 +1597,9 @@ _preferencesBackup:
|
||||
youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効にするにはプロファイル名の設定が必要です。"
|
||||
autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になっていません。"
|
||||
backupFound: "設定のバックアップが見つかりました"
|
||||
forceBackup: "設定の強制バックアップ"
|
||||
forceBackup: "今すぐバックアップ"
|
||||
autoSync: "自動同期"
|
||||
forceSync: "今すぐ同期"
|
||||
|
||||
_accountSettings:
|
||||
requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
|
||||
|
||||
7
packages/frontend/docs/preferences.md
Normal file
7
packages/frontend/docs/preferences.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# Preferences system
|
||||
|
||||
## ユースケース
|
||||
|
||||
### (新しいデバイスなどで)既存のプロファイルを継承した新しいプロファイルを作りたい
|
||||
|
||||
継承したいプロファイルをバックアップから復元した後、プロファイルの名前を変える。
|
||||
@@ -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(() => []);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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[] {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -6336,9 +6336,17 @@ export interface Locale extends ILocale {
|
||||
*/
|
||||
"backupFound": string;
|
||||
/**
|
||||
* 設定の強制バックアップ
|
||||
* 今すぐバックアップ
|
||||
*/
|
||||
"forceBackup": string;
|
||||
/**
|
||||
* 自動同期
|
||||
*/
|
||||
"autoSync": string;
|
||||
/**
|
||||
* 今すぐ同期
|
||||
*/
|
||||
"forceSync": string;
|
||||
};
|
||||
"_accountSettings": {
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user