Refine preferences (#15597)

* wip

* wip

* wip

* test

* wip rollup pluginでsearchIndexの情報生成

* wip

* SPDX

* wip: markerIdを自動付与

* rollupでビルド時・devモード時に毎回uuidを生成するように

* 開発サーバーでだけ必要な挙動は開発サーバーのみで

* 条件が逆

* wip: childrenの生成

* update comment

* update comment

* rename auto generated file

* hashをパスと行数から決定

* Update privacy.vue

* Update privacy.vue

* wip

* Update general.vue

* Update general.vue

* wip

* wip

* Update SearchMarker.vue

* wip

* Update profile.vue

* Update mute-block.vue

* Update mute-block.vue

* Update general.vue

* Update general.vue

* childrenがduplicate key errorを吐く問題をいったん解決

* マーカーの形を成形

* loggerを置きかえ

* とりあえず省略記法に対応

* Refactor and Format codes

* wip

* Update settings-search-index.ts

* wip

* wip

* とりあえず不確定要因の仮置きidを削除

* hashの生成を正規化(絶対パスになっていたのを緩和)

* pathの入力を省略可能に

* adminでもパス生成できるように

* Update settings-search-index.ts

* Update privacy.vue

* wip

* build searchIndex

* wip

* build

* Update general.vue

* build

* Update sounds.vue

* build

* build

* Update sounds.vue

* 🎨

* 🎨

* Update privacy.vue

* Update privacy.vue

* Update security.vue

* create-search-indexを多少改善

* build

* Update 2fa.vue

* wip

* 必ずtransformCodeCacheを利用するように, キャッシュの明確な受け渡しを定義

* キャッシュはdevServerでなくても更新

* Revert "wip"

This reverts commit 41bffd3a13f55618bf939dc1c9acb2a77ead4054.

* inlining

* wip

* Update theme.vue

* 🎨

* wip normalize

* Update theme.vue

* キャッシュのパス変換

* build

* wip

* wip

* Update SearchMarker.vue

* i18n.ts['key'] の形式が取り出せない問題のFix

* build

* 仮でpath入れ

* 必ず絶対パスが使われるように

* wip

* 🎨

* storybookビルド時はcreateSearchIndexをしない

* inliningの構造化

* format code

* Update index.vue

* wip

* wip

* 🎨

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* clean up

* wip

* wip

* wip

* Update rollup-plugin-unwind-css-module-class-name.test.ts

* Update navbar.vue

* clean up

* wip

* wip

* wip

* wip

* wip

* Update preferences-backups.vue

* Update common.ts

* Update preferences.ts

* wip

* wip

* wip

* wip

* Update MkPreferenceContainer.vue

* Update MkPreferenceContainer.vue

* Update MkPreferenceContainer.vue

* enhance: 検索で上下矢印を使用することで検索結果を移動できるように

* Update main-boot.ts

* refactor

* wip

* Update sounds.vue

* fix(frontend): PageWindowでSearchMarkerが動作するように

* enhance(frontend): SearchMarkerの点滅を一定時間で止める

* wip

* lint fix

* fix: 子要素監視が抜けていたのを修正

* アニメーションの回数はCSSで制御するように

* refactor

* enhance(frontend): 検索インデックス作成時のログを削減

* revert

* fix

* fix

* Update preferences.ts

* Update preferences.ts

* wip

* Update preferences.ts

* wip

* 🎨

* wip

* Update MkPreferenceContainer.vue

* wip

* Update preferences.ts

* wip

* Update preferences.ts

* Update preferences.ts

* wip

* wip

* Update preferences.ts

* wip

* wip

* Update preferences.ts

* Update CHANGELOG.md

* Update preferences.ts

* Update deck-store.ts

* deckStoreをdefaultStoreに統合

* wip

* defaultStore -> store

* Update profile.ts

* wip

* refactor

* wip: plugin

* plugin

* plugin

* plugin

* Update plugin.ts

* wip

* Update plugin.vue

* Update preferences.ts

* Update main-boot.ts

* wip

* fix test

* Update plugin.vue

* Update plugin.vue

* Update utility.ts

* wip

* wip

* Update utility.ts

* wip

* wip

* clean up

* Update utility.ts

---------

Co-authored-by: tai-cha <dev@taichan.site>
Co-authored-by: taichan <40626578+tai-cha@users.noreply.github.com>
Co-authored-by: kakkokari-gtyih <67428053+kakkokari-gtyih@users.noreply.github.com>
This commit is contained in:
syuilo 2025-03-09 12:34:08 +09:00 committed by GitHub
parent 05cdc095c0
commit d30ddd4c2e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
181 changed files with 3437 additions and 2463 deletions

View file

@ -0,0 +1,308 @@
/*
* 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 '@/scripts/theme.js';
import type { SoundType } from '@/scripts/sound.js';
import type { Plugin } from '@/plugin.js';
import { DEFAULT_DEVICE_KIND } from '@/scripts/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,
},
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;
}>;

View file

@ -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 '@/scripts/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.set(key, PREF_DEF[key].default);
},
}, {
type: 'divider',
}, {
type: 'switch',
icon: 'ti ti-user-cog',
text: i18n.ts.overrideByAccount,
ref: overrideByAccount,
}];
}
}

View file

@ -0,0 +1,92 @@
/*
* 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 (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 set<K extends keyof Data>(key: K, value: Data[K]) {
this.r[key].value = this.s[key] = value;
this.emit('updated', { key, value });
}
public rewrite<K extends keyof Data>(key: K, value: Data[K]) {
this.r[key].value = this.s[key] = value;
}
/**
* 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.set(key, val);
valueRef.value = val;
},
});
}
}

View file

@ -0,0 +1,222 @@
/*
* 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 '@/scripts/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 '@/scripts/misskey-api.js';
import { unisonReload } from '@/scripts/unison-reload.js';
export function getPreferencesProfileMenu(): MenuItem[] {
const autoBackupEnabled = ref(store.state.enablePreferencesAutoCloudBackup);
watch(autoBackupEnabled, () => {
if (autoBackupEnabled.value) {
if (profileManager.profile.name == null || profileManager.profile.name.trim() === '') {
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 (profileManager.profile.name == null || profileManager.profile.name.trim() === '') {
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 (profileManager.profile.name == null || profileManager.profile.name.trim() === '') {
await renameProfile();
}
if (profileManager.profile.name == null || profileManager.profile.name.trim() === '') {
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;
}