1
0
Fork 0
mirror of https://github.com/paricafe/misskey.git synced 2025-03-23 11:39:25 -05:00

enhance(frontend): 設定値の同期を実装(実験的)

This commit is contained in:
syuilo 2025-03-12 11:39:05 +09:00
parent ddbc83b2e4
commit b03bcf26cd
9 changed files with 343 additions and 187 deletions

View file

@ -6,6 +6,7 @@
### Client
- Feat: 設定の管理が強化されました
- 自動でバックアップされるように
- 任意の設定項目をデバイス間で同期できるように(実験的)
- Enhance: プラグインの管理が強化されました
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように

24
locales/index.d.ts vendored
View file

@ -5310,6 +5310,30 @@ export interface Locale extends ILocale {
*
*/
"restore": string;
/**
*
*/
"syncBetweenDevices": string;
/**
*
*/
"preferenceSyncConflictTitle": string;
/**
*
*/
"preferenceSyncConflictText": string;
/**
*
*/
"preferenceSyncConflictChoiceServer": string;
/**
*
*/
"preferenceSyncConflictChoiceDevice": string;
/**
*
*/
"preferenceSyncConflictChoiceCancel": string;
"_settings": {
/**
* 使

View file

@ -1323,6 +1323,12 @@ untitled: "無題"
noName: "名前はありません"
skip: "スキップ"
restore: "復元"
syncBetweenDevices: "デバイス間で同期"
preferenceSyncConflictTitle: "サーバーに設定値が存在します"
preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?"
preferenceSyncConflictChoiceServer: "サーバーの設定値"
preferenceSyncConflictChoiceDevice: "デバイスの設定値"
preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
_settings:
driveBanner: "ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。"

View file

@ -9,6 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<slot></slot>
</div>
<div :class="$style.menu">
<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>
@ -21,20 +22,21 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import type { PREF_DEF } from '@/preferences/def.js';
import * as os from '@/os.js';
import { profileManager } from '@/preferences.js';
import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
k: keyof typeof PREF_DEF;
}>(), {
});
const isAccountOverrided = ref(profileManager.isAccountOverrided(props.k));
const isAccountOverrided = ref(prefer.isAccountOverrided(props.k));
const isSyncEnabled = ref(prefer.isSyncEnabled(props.k));
function showMenu(ev: MouseEvent) {
const i = window.setInterval(() => {
isAccountOverrided.value = profileManager.isAccountOverrided(props.k);
isAccountOverrided.value = prefer.isAccountOverrided(props.k);
}, 100);
os.popupMenu(profileManager.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, {
os.popupMenu(prefer.getPerPrefMenu(props.k), ev.currentTarget ?? ev.target, {
onClosing: () => {
window.clearInterval(i);
},

View file

@ -4,16 +4,17 @@
*/
import { v4 as uuid } from 'uuid';
import type { PreferencesProfile } from '@/preferences/profile.js';
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 { store } from '@/store.js';
import { $i } from '@/account.js';
import { misskeyApi } from '@/utility/misskey-api.js';
const TAB_ID = uuid();
function createProfileManager() {
function createProfileManager(storageProvider: StorageProvider) {
let profile: PreferencesProfile;
const savedProfileRaw = miLocalStorage.getItem('preferences');
@ -24,15 +25,44 @@ function createProfileManager() {
profile = ProfileManager.normalizeProfile(JSON.parse(savedProfileRaw));
}
return new ProfileManager(profile);
return new ProfileManager(profile, storageProvider);
}
export const profileManager = createProfileManager();
profileManager.addListener('updated', ({ profile: p }) => {
miLocalStorage.setItem('preferences', JSON.stringify(p));
miLocalStorage.setItem('latestPreferencesUpdate', `${TAB_ID}/${Date.now()}`);
});
export const prefer = profileManager.store;
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', {
scope: ['client', 'preferences', 'sync'],
key: ctx.key,
});
return {
value,
};
} catch (err: any) {
if (err.code === 'NO_SUCH_KEY') {
return null;
} else {
throw err;
}
}
},
cloudSet: async (ctx) => {
await misskeyApi('i/registry/set', {
scope: ['client', 'preferences', 'sync'],
key: ctx.key,
value: ctx.value,
});
},
};
export const prefer = createProfileManager(storageProvider);
let latestSyncedAt = Date.now();
@ -46,7 +76,7 @@ function syncBetweenTabs() {
if (latestTab === TAB_ID) return;
if (latestAt <= latestSyncedAt) return;
profileManager.rewriteProfile(ProfileManager.normalizeProfile(JSON.parse(miLocalStorage.getItem('preferences')!)));
prefer.rewriteProfile(ProfileManager.normalizeProfile(JSON.parse(miLocalStorage.getItem('preferences')!)));
latestSyncedAt = Date.now();
@ -67,7 +97,7 @@ window.setInterval(() => {
if ($i == null) return;
if (!store.s.enablePreferencesAutoCloudBackup) return;
if (document.visibilityState !== 'visible') return; // 同期されていない古い値がバックアップされるのを防ぐ
if (profileManager.profile.modifiedAt <= latestBackupAt) return;
if (prefer.profile.modifiedAt <= latestBackupAt) return;
cloudBackup().then(() => {
latestBackupAt = Date.now();
@ -75,7 +105,6 @@ window.setInterval(() => {
}, 1000 * 60 * 3);
if (_DEV_) {
(window as any).profileManager = profileManager;
(window as any).prefer = prefer;
(window as any).cloudBackup = cloudBackup;
}

View file

@ -327,4 +327,5 @@ export const PREF_DEF = {
} satisfies Record<string, {
default: any;
accountDependent?: boolean;
serverDependent?: boolean;
}>;

View file

@ -3,16 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
import { ref, watch } from 'vue';
import { computed, onUnmounted, 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 { Ref, WritableComputedRef } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import { $i } from '@/account.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
// NOTE: 明示的な設定値のひとつとして null もあり得るため、設定が存在しないかどうかを判定する目的で null で比較したり ?? を使ってはいけない
@ -24,11 +24,41 @@ type PREF = typeof PREF_DEF;
type ValueOf<K extends keyof PREF> = PREF[K]['default'];
type Account = string; // <host>/<userId>
type Cond = {
type Cond = Partial<{
server: string | null; // 将来のため
account: Account | null;
device: string | null; // 将来のため
};
}>;
type ValueMeta = Partial<{
sync: boolean;
}>;
type PrefRecord<K extends keyof PREF> = [cond: Cond, value: ValueOf<K>, meta: ValueMeta];
function parseCond(cond: Cond): {
server: string | null;
account: Account | null;
device: string | null;
} {
return {
server: cond.server ?? null,
account: cond.account ?? null,
device: cond.device ?? null,
};
}
function makeCond(cond: Partial<{
server: string | null;
account: Account | null;
device: string | null;
}>): Cond {
const c = {} as Cond;
if (cond.server != null) c.server = cond.server;
if (cond.account != null) c.account = cond.account;
if (cond.device != null) c.device = cond.device;
return c;
}
export type PreferencesProfile = {
id: string;
@ -37,53 +67,119 @@ export type PreferencesProfile = {
modifiedAt: number;
name: string;
preferences: {
[K in keyof PREF]: [Cond, ValueOf<K>][];
[K in keyof PREF]: PrefRecord<K>[];
};
syncByAccount: [Account, keyof PREF][],
};
// TODO: 任意のプロパティをデバイス間で同期できるようにする?
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>;
};
export class ProfileManager extends EventEmitter<{
updated: (ctx: {
profile: PreferencesProfile
}) => void;
}> {
export class ProfileManager {
private storageProvider: StorageProvider;
public profile: PreferencesProfile;
public store: Store<{
[K in keyof PREF]: ValueOf<K>;
}>;
constructor(profile: PreferencesProfile) {
super();
/**
* static / state (static )
*/
public s = {} as {
[K in keyof PREF]: ValueOf<K>;
};
/**
* reactive
*/
public r = {} as {
[K in keyof PREF]: Ref<ValueOf<K>>;
};
constructor(profile: PreferencesProfile, storageProvider: StorageProvider) {
this.profile = profile;
this.storageProvider = storageProvider;
const states = this.genStates();
this.store = new Store(states);
this.store.addListener('updated', ({ key, value }) => {
console.log('prefer:set', key, value);
for (const key in states) {
this.s[key] = states[key];
this.r[key] = ref(this.s[key]);
}
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;
}
this.fetchCloudValues();
record[1] = value;
// TODO: 定期的にクラウドの値をフェッチ
}
private rewriteRawState<K extends keyof PREF>(key: K, value: ValueOf<K>) {
const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
this.r[key].value = this.s[key] = v;
}
public commit<K extends keyof PREF>(key: K, value: ValueOf<K>) {
console.log('prefer:commit', key, value);
this.rewriteRawState(key, value);
const record = this.getMatchedRecord(key);
if (parseCond(record[0]).account == null && PREF_DEF[key].accountDependent) {
this.profile.preferences[key].push([makeCond({
account: `${host}/${$i!.id}`,
}), value, {}]);
this.save();
return;
}
if (record[2].sync) {
// awaitの必要なし
// TODO: リクエストを間引く
this.storageProvider.cloudSet({ key, value });
}
record[1] = value;
this.save();
}
/**
* computed refを作ります
* vue上で設定コントロールのmodelとして使う用
*/
public model<K extends keyof PREF, V extends ValueOf<K> = ValueOf<K>>(
key: K,
getter?: (v: ValueOf<K>) => V,
setter?: (v: V) => ValueOf<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.commit(key, val);
valueRef.value = val;
},
});
}
private genStates() {
const states = {} as { [K in keyof PREF]: ValueOf<K> };
let key: keyof PREF;
for (key in PREF_DEF) {
for (const key in PREF_DEF) {
const record = this.getMatchedRecord(key);
states[key] = record[1];
}
@ -91,15 +187,37 @@ export class ProfileManager extends EventEmitter<{
return states;
}
private fetchCloudValues() {
// TODO: 値の取得を1つのリクエストで済ませたい(バックエンド側でAPIの新設が必要)
const promises: Promise<void>[] = [];
for (const key in PREF_DEF) {
const record = this.getMatchedRecord(key);
if (record[2].sync) {
const getting = this.storageProvider.cloudGet({ key });
promises.push(getting.then((res) => {
if (res == null) return;
const value = res.value;
if (value !== this.s[key]) {
this.rewriteRawState(key, value);
record[1] = value;
console.log('cloud fetched', key, value);
}
}));
}
}
Promise.all(promises).then(() => {
console.log('cloud fetched all');
this.save();
console.log(this.s.showFixedPostForm, this.r.showFixedPostForm.value);
});
}
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]];
for (const key in PREF_DEF) {
data[key] = [[makeCond({}), PREF_DEF[key].default, {}]];
}
return {
id: uuid(),
@ -108,29 +226,31 @@ export class ProfileManager extends EventEmitter<{
modifiedAt: Date.now(),
name: '',
preferences: data,
syncByAccount: [],
};
}
public static normalizeProfile(profile: any): PreferencesProfile {
public static normalizeProfile(profileLike: any): PreferencesProfile {
const data = {} as PreferencesProfile['preferences'];
let key: keyof PREF;
for (key in PREF_DEF) {
const records = profile.preferences[key];
for (const key in PREF_DEF) {
const records = profileLike.preferences[key];
if (records == null || records.length === 0) {
data[key] = [[{
server: null,
account: null,
device: null,
}, PREF_DEF[key].default]];
data[key] = [[makeCond({}), PREF_DEF[key].default, {}]];
continue;
} else {
data[key] = records;
// alpha段階ではmetaが無かったのでマイグレート
// TODO: そのうち消す
for (const record of data[key] as any[][]) {
if (record.length === 2) {
record.push({});
}
}
}
}
return {
...profile,
...profileLike,
preferences: data,
};
}
@ -138,24 +258,24 @@ export class ProfileManager extends EventEmitter<{
public save() {
this.profile.modifiedAt = Date.now();
this.profile.version = version;
this.emit('updated', { profile: this.profile });
this.storageProvider.save({ profile: this.profile });
}
public getMatchedRecord<K extends keyof PREF>(key: K): [Cond, ValueOf<K>] {
public getMatchedRecord<K extends keyof PREF>(key: K): PrefRecord<K> {
const records = this.profile.preferences[key];
if ($i == null) return records.find(([cond, v]) => cond.account == null)!;
if ($i == null) return records.find(([cond, v]) => parseCond(cond).account == null)!;
const accountOverrideRecord = records.find(([cond, v]) => cond.account === `${host}/${$i!.id}`);
const accountOverrideRecord = records.find(([cond, v]) => parseCond(cond).account === `${host}/${$i!.id}`);
if (accountOverrideRecord) return accountOverrideRecord;
const record = records.find(([cond, v]) => cond.account == null);
const record = records.find(([cond, v]) => parseCond(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;
return this.profile.preferences[key].some(([cond, v]) => parseCond(cond).account === `${host}/${$i!.id}`) ?? false;
}
public setAccountOverride<K extends keyof PREF>(key: K) {
@ -164,11 +284,9 @@ export class ProfileManager extends EventEmitter<{
if (this.isAccountOverrided(key)) return;
const records = this.profile.preferences[key];
records.push([{
server: null,
records.push([makeCond({
account: `${host}/${$i!.id}`,
device: null,
}, this.store.s[key]]);
}), this.s[key], {}]);
this.save();
}
@ -179,16 +297,67 @@ export class ProfileManager extends EventEmitter<{
const records = this.profile.preferences[key];
const index = records.findIndex(([cond, v]) => cond.account === `${host}/${$i!.id}`);
const index = records.findIndex(([cond, v]) => parseCond(cond).account === `${host}/${$i!.id}`);
if (index === -1) return;
records.splice(index, 1);
this.store.rewrite(key, this.getMatchedRecord(key)[1]);
this.rewriteRawState(key, this.getMatchedRecord(key)[1]);
this.save();
}
public isSyncEnabled<K extends keyof PREF>(key: K): boolean {
return this.getMatchedRecord(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 });
if (existing != null) {
const { canceled, result } = await os.select({
title: i18n.ts.preferenceSyncConflictTitle,
text: i18n.ts.preferenceSyncConflictText,
items: [{
text: i18n.ts.preferenceSyncConflictChoiceServer,
value: 'remote',
}, {
text: i18n.ts.preferenceSyncConflictChoiceDevice,
value: 'local',
}, {
text: i18n.ts.preferenceSyncConflictChoiceCancel,
value: null,
}],
default: 'remote',
});
if (canceled || result == null) return { enabled: false };
if (result === 'remote') {
this.commit(key, existing.value);
} else if (result === 'local') {
// nop
}
}
const record = this.getMatchedRecord(key);
record[2].sync = true;
this.save();
// awaitの必要性は無い
this.storageProvider.cloudSet({ key, value: this.s[key] });
return { enabled: true };
}
public disableSync<K extends keyof PREF>(key: K) {
if (!this.isSyncEnabled(key)) return;
const record = this.getMatchedRecord(key);
delete record[2].sync;
this.save();
}
public renameProfile(name: string) {
this.profile.name = name;
this.save();
@ -198,13 +367,14 @@ export class ProfileManager extends EventEmitter<{
this.profile = profile;
const states = this.genStates();
for (const key in states) {
this.store.rewrite(key, states[key]);
this.rewriteRawState(key, states[key]);
}
this.fetchCloudValues();
}
public getPerPrefMenu<K extends keyof PREF>(key: K): MenuItem[] {
const overrideByAccount = ref(this.isAccountOverrided(key));
watch(overrideByAccount, () => {
if (overrideByAccount.value) {
this.setAccountOverride(key);
@ -213,6 +383,18 @@ export class ProfileManager extends EventEmitter<{
}
});
const sync = ref(this.isSyncEnabled(key));
watch(sync, () => {
if (sync.value) {
this.enableSync(key).then((res) => {
if (res == null) return;
if (!res.enabled) sync.value = false;
});
} else {
this.disableSync(key);
}
});
return [{
icon: 'ti ti-copy',
text: i18n.ts.copyPreferenceId,
@ -224,7 +406,7 @@ export class ProfileManager extends EventEmitter<{
text: i18n.ts.resetToDefaultValue,
danger: true,
action: () => {
this.store.commit(key, PREF_DEF[key].default);
this.commit(key, PREF_DEF[key].default);
},
}, {
type: 'divider',
@ -233,6 +415,11 @@ export class ProfileManager extends EventEmitter<{
icon: 'ti ti-user-cog',
text: i18n.ts.overrideByAccount,
ref: overrideByAccount,
}, {
type: 'switch',
icon: 'ti ti-cloud-cog',
text: i18n.ts.syncBetweenDevices,
ref: sync,
}];
}
}

View file

@ -1,94 +0,0 @@
/*
* 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 / state (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 commit<K extends keyof Data>(key: K, value: Data[K]) {
const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
this.r[key].value = this.s[key] = v;
this.emit('updated', { key, value: v });
}
public rewrite<K extends keyof Data>(key: K, value: Data[K]) {
const v = JSON.parse(JSON.stringify(value)); // deep copy 兼 vueのプロキシ解除
this.r[key].value = this.s[key] = v;
}
/**
* 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.commit(key, val);
valueRef.value = val;
},
});
}
}

View file

@ -9,7 +9,7 @@ import type { MenuItem } from '@/types/menu.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
import { prefer, profileManager } from '@/preferences.js';
import { prefer } from '@/preferences.js';
import * as os from '@/os.js';
import { store } from '@/store.js';
import { $i } from '@/account.js';
@ -17,7 +17,7 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import { unisonReload } from '@/utility/unison-reload.js';
function canAutoBackup() {
return profileManager.profile.name != null && profileManager.profile.name.trim() !== '';
return prefer.profile.name != null && prefer.profile.name.trim() !== '';
}
export function getPreferencesProfileMenu(): MenuItem[] {
@ -42,7 +42,7 @@ export function getPreferencesProfileMenu(): MenuItem[] {
const menu: MenuItem[] = [{
type: 'label',
text: profileManager.profile.name || `(${i18n.ts.noName})`,
text: prefer.profile.name || `(${i18n.ts.noName})`,
}, {
text: i18n.ts.rename,
icon: 'ti ti-pencil',
@ -83,7 +83,7 @@ export function getPreferencesProfileMenu(): MenuItem[] {
text: 'Copy profile as text',
icon: 'ti ti-clipboard',
action: () => {
copyToClipboard(JSON.stringify(profileManager.profile, null, '\t'));
copyToClipboard(JSON.stringify(prefer.profile, null, '\t'));
},
});
}
@ -95,16 +95,16 @@ 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,
placeholder: prefer.profile.name || null,
default: prefer.profile.name || null,
});
if (canceled || name == null || name.trim() === '') return;
profileManager.renameProfile(name);
prefer.renameProfile(name);
}
function exportCurrentProfile() {
const p = profileManager.profile;
const p = prefer.profile;
const txtBlob = new Blob([JSON.stringify(p)], { type: 'text/plain' });
const dummya = document.createElement('a');
dummya.href = URL.createObjectURL(txtBlob);
@ -140,8 +140,8 @@ export async function cloudBackup() {
await misskeyApi('i/registry/set', {
scope: ['client', 'preferences', 'backups'],
key: profileManager.profile.name,
value: profileManager.profile,
key: prefer.profile.name,
value: prefer.profile,
});
}