summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authortamaina <tamaina@hotmail.co.jp>2022-04-17 05:12:33 +0900
committertamaina <tamaina@hotmail.co.jp>2022-04-17 05:12:33 +0900
commit39e4f2c137fbdf9be30e986bceac7230841576ab (patch)
treebb3645188c8b5fe5eb6fff1555257c6cd77bee3f
parentclean up (diff)
downloadmisskey-39e4f2c137fbdf9be30e986bceac7230841576ab.tar.gz
misskey-39e4f2c137fbdf9be30e986bceac7230841576ab.tar.bz2
misskey-39e4f2c137fbdf9be30e986bceac7230841576ab.zip
feature: Client Preferences Registry on the account
-rw-r--r--locales/ja-JP.yml21
-rw-r--r--packages/client/src/pages/settings/index.vue6
-rw-r--r--packages/client/src/pages/settings/preferences-registry.vue278
-rw-r--r--packages/client/src/store.ts10
4 files changed, 315 insertions, 0 deletions
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 6326094dd8..5754ed2bfc 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -559,6 +559,8 @@ author: "作者"
leaveConfirm: "未保存の変更があります。破棄しますか?"
manage: "管理"
plugins: "プラグイン"
+preferencesRegistry: "クライアント設定のレジストリ"
+preferencesRegistryShort: "レジストリ"
deck: "デッキ"
undeck: "デッキ解除"
useBlurEffectForModal: "モーダルにぼかし効果を使用"
@@ -894,6 +896,25 @@ _plugin:
installWarn: "信頼できないプラグインはインストールしないでください。"
manage: "プラグインの管理"
+_preferencesRegistry:
+ list: "一覧"
+ saveNew: "新規保存"
+ apply: "適用"
+ delete: "削除"
+ save: "上書き保存"
+ rename: "名称変更"
+ saveNewDescription: "現在のデバイスの状態をサーバーに保存します。"
+ inputName: "レジストリ名を入力"
+ cannotSave: "保存できません"
+ nameAlreadyExists: "レジストリ名「{name}」は既に存在します。違うレジストリ名を指定してください。"
+ applyConfirm: "プロファイル「{name}」を現在のデバイスに適用しますか?現在のデバイス設定は失われます。"
+ saveConfirm: "{name}に上書き保存しますか?"
+ deleteConfirm: "{name}を削除しますか?"
+ renameConfirm: "「{old}」を「{new}」に変更しますか?"
+ noRegistries: "レジストリは登録されていません。「新規保存」で現在のクライアント設定をサーバーに保存できます。"
+ createdAt: "作成日時: {date} {time}"
+ updatedAt: "更新日時: {date} {time}"
+
_registry:
scope: "スコープ"
key: "キー"
diff --git a/packages/client/src/pages/settings/index.vue b/packages/client/src/pages/settings/index.vue
index 1838cb73e3..752054e9b3 100644
--- a/packages/client/src/pages/settings/index.vue
+++ b/packages/client/src/pages/settings/index.vue
@@ -132,6 +132,11 @@ const menuDef = computed(() => [{
text: i18n.ts.plugins,
to: '/settings/plugin',
active: props.initialPage === 'plugin',
+ }, {
+ icon: 'fas fa-floppy-disk',
+ text: i18n.ts.preferencesRegistryShort,
+ to: '/settings/preferences-registry',
+ active: props.initialPage === 'preferences-registry',
}],
}, {
title: i18n.ts.otherSettings,
@@ -225,6 +230,7 @@ const component = computed(() => {
case 'deck': return defineAsyncComponent(() => import('./deck.vue'));
case 'plugin': return defineAsyncComponent(() => import('./plugin.vue'));
case 'plugin/install': return defineAsyncComponent(() => import('./plugin.install.vue'));
+ case 'preferences-registry': return defineAsyncComponent(() => import('./preferences-registry.vue'));
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
case 'delete-account': return defineAsyncComponent(() => import('./delete-account.vue'));
diff --git a/packages/client/src/pages/settings/preferences-registry.vue b/packages/client/src/pages/settings/preferences-registry.vue
new file mode 100644
index 0000000000..d668150045
--- /dev/null
+++ b/packages/client/src/pages/settings/preferences-registry.vue
@@ -0,0 +1,278 @@
+<template>
+<div class="_formRoot">
+ <MkButton class="primary" @click="saveNew">{{ ts._preferencesRegistry.saveNew }}</MkButton>
+
+ <FormSection>
+ <template #label>{{ ts._preferencesRegistry.list }}</template>
+ <div
+ v-if="registries && Object.keys(registries).length > 0"
+ v-for="(registry, id) in registries"
+ :key="id"
+ class="_formBlock _panel"
+ :class="$style.registry"
+ @click="$event => menu($event, id)"
+ @contextmenu.prevent.stop="$event => menu($event, id)"
+ >
+ <div :class="$style.registryName">{{ registry.name }}</div>
+ <div :class="$style.registryTime">{{ t('_preferencesRegistry.createdAt', { date: (new Date(registry.createdAt)).toLocaleDateString(), time: (new Date(registry.createdAt)).toLocaleTimeString() }) }}</div>
+ <div :class="$style.registryTime" v-if="registry.updatedAt">{{ t('_preferencesRegistry.updatedAt', { date: (new Date(registry.createdAt)).toLocaleDateString(), time: (new Date(registry.createdAt)).toLocaleTimeString() }) }}</div>
+ </div>
+ <div v-else-if="registries">
+ <MkInfo>{{ ts._preferencesRegistry.noRegistries }}</MkInfo>
+ </div>
+ <MkLoading v-else />
+ </FormSection>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, onUnmounted, useCssModule } from 'vue';
+import FormSection from '@/components/form/section.vue';
+import MkButton from '@/components/ui/button.vue';
+import MkInfo from '@/components/ui/info.vue';
+import * as os from '@/os';
+import { v4 as uuid } from 'uuid';
+import { ColdDeviceStorage, defaultStore } from '@/store';
+import * as symbols from '@/symbols';
+import { unisonReload } from '@/scripts/unison-reload';
+import { stream } from '@/stream';
+import { $i } from '@/account';
+import { i18n } from '@/i18n';
+const { t, ts } = i18n;
+
+useCssModule();
+
+const scope = ['clientPreferencesProfiles'];
+
+const connection = $i && stream.useChannel('main');
+
+type Registry = {
+ name: string;
+ createdAt: string;
+ updatedAt: string | null;
+ defaultStore: Partial<typeof defaultStore.state>;
+ coldDeviceStorage: Partial<typeof ColdDeviceStorage.default>;
+ fontSize: string | null;
+ useSystemFont: 't' | null;
+};
+
+type Registries = {
+ [key: string]: Registry;
+};
+
+let registries = $ref<Registries | null>(null);
+
+os.api('i/registry/get-all', { scope })
+ .then(res => {
+ registries = res || {};
+ console.log(registries);
+ });
+
+function getDefaultStoreValues() {
+ return (Object.keys(defaultStore.state) as (keyof typeof defaultStore.state)[]).reduce((acc, key) => {
+ if (defaultStore.def[key].where !== 'account') acc[key] = defaultStore.state[key];
+ return acc;
+ }, {} as any);
+}
+
+async function saveNew() {
+ if (!registries) return;
+
+ const { canceled, result: name } = await os.inputText({
+ title: ts._preferencesRegistry.inputName,
+ text: ts._preferencesRegistry.saveNewDescription,
+ });
+
+ if (canceled) return;
+ if (Object.entries(registries).some(e => e[1].name === name)) {
+ return os.alert({
+ title: ts._preferencesRegistry.cannotSave,
+ text: t('_preferencesRegistry.nameAlreadyExists', { name }),
+ });
+ }
+
+ const id = uuid();
+ const registry: Registry = {
+ name,
+ createdAt: (new Date()).toISOString(),
+ updatedAt: null,
+ defaultStore: getDefaultStoreValues(),
+ coldDeviceStorage: ColdDeviceStorage.getAll(),
+ fontSize: localStorage.getItem('fontSize'),
+ useSystemFont: localStorage.getItem('useSystemFont') as 't' | null,
+ };
+ await os.api('i/registry/set', { scope, key: id, value: registry });
+ registries[id] = registry;
+}
+
+async function applyRegistry(id: string) {
+ if (!registries) return;
+
+ const registry = registries[id];
+
+ const { canceled: cancel1 } = await os.confirm({
+ type: 'warning',
+ title: ts._preferencesRegistry.apply,
+ text: t('_preferencesRegistry.applyConfirm', { name: registry.name }),
+ });
+ if (cancel1) return;
+
+ // defaultStore
+ for (const [key, value] of Object.entries(registry.defaultStore)) {
+ if (key in defaultStore.def && defaultStore.def[key].where !== 'account') {
+ console.log(key);
+ defaultStore.set(key as keyof Registry['defaultStore'], value);
+ }
+ }
+
+ // coldDeviceStorage
+ for (const [key, value] of Object.entries(registry.coldDeviceStorage)) {
+ ColdDeviceStorage.set(key as keyof Registry['coldDeviceStorage'], value);
+ }
+
+ // fontSize
+ if (registry.fontSize) {
+ localStorage.setItem('fontSize', registry.fontSize);
+ } else {
+ localStorage.removeItem('fontSize');
+ }
+
+ // useSystemFont
+ if (registry.useSystemFont) {
+ localStorage.setItem('useSystemFont', registry.useSystemFont);
+ } else {
+ localStorage.removeItem('useSystemFont');
+ }
+
+ const { canceled: cancel2 } = await os.confirm({
+ type: 'info',
+ text: ts.reloadToApplySetting,
+ });
+ if (cancel2) return;
+
+ unisonReload();
+}
+
+async function deleteRegistry(id: string) {
+ if (!registries) return;
+
+ const { canceled } = await os.confirm({
+ type: 'info',
+ title: ts._preferencesRegistry.delete,
+ text: t('_preferencesRegistry.deleteConfirm', { name: registries[id].name }),
+ });
+ if (canceled) return;
+
+ await os.api('i/registry/remove', { scope, key: id });
+ delete registries[id];
+}
+
+async function save(id: string) {
+ if (!registries) return;
+
+ const { name, createdAt } = registries[id];
+
+ const { canceled } = await os.confirm({
+ type: 'info',
+ title: ts._preferencesRegistry.save,
+ text: t('_preferencesRegistry.saveConfirm', { name }),
+ });
+ if (canceled) return;
+
+ const registry: Registry = {
+ name,
+ createdAt,
+ updatedAt: (new Date()).toISOString(),
+ defaultStore: getDefaultStoreValues(),
+ coldDeviceStorage: ColdDeviceStorage.getAll(),
+ };
+ await os.api('i/registry/set', { scope, key: id, value: registry });
+ registries[id] = registry;
+}
+
+async function rename(id: string) {
+ if (!registries) return;
+
+ const { canceled: cancel1, result: name } = await os.inputText({
+ title: ts._preferencesRegistry.inputName,
+ });
+ if (cancel1 || registries[id].name === name) return;
+
+ if (Object.entries(registries).some(e => e[1].name === name)) {
+ return os.alert({
+ title: ts._preferencesRegistry.cannotSave,
+ text: t('_preferencesRegistry.nameAlreadyExists', { name }),
+ });
+ }
+
+ const registry = Object.assign({}, { ...registries[id] });
+
+ const { canceled: cancel2 } = await os.confirm({
+ type: 'info',
+ title: ts._preferencesRegistry.rename,
+ text: t('_preferencesRegistry.renameConfirm', { old: registry.name, new: name }),
+ });
+ if (cancel2) return;
+
+ registry.name = name;
+ await os.api('i/registry/set', { scope, key: id, value: registry });
+}
+
+function menu(ev: MouseEvent, registryId: string) {
+ return os.popupMenu([{
+ text: ts._preferencesRegistry.apply,
+ icon: 'fas fa-circle-down',
+ action: () => applyRegistry(registryId),
+ }, {
+ text: ts._preferencesRegistry.rename,
+ icon: 'fas fa-i-cursor',
+ action: () => rename(registryId),
+ }, {
+ text: ts._preferencesRegistry.save,
+ icon: 'fas fa-floppy-disk',
+ action: () => save(registryId),
+ }, {
+ text: ts._preferencesRegistry.delete,
+ icon: 'fas fa-trash-can',
+ action: () => deleteRegistry(registryId),
+ }], ev.currentTarget ?? ev.target)
+}
+
+onMounted(() => {
+ // streamingのuser storage updateイベントを監視して更新
+ connection?.on('registryUpdated', ({ scope: recievedScope, key, value }) => {
+ if (!recievedScope || recievedScope.length !== scope.length || recievedScope[0] !== scope[0]) return;
+ if (!registries) return;
+
+ registries[key] = value;
+ });
+});
+
+onUnmounted(() => {
+ connection?.off('registryUpdated');
+});
+
+defineExpose({
+ [symbols.PAGE_INFO]: {
+ title: ts.preferencesRegistry,
+ icon: 'fas fa-floppy-disk',
+ bg: 'var(--bg)',
+ }
+})
+</script>
+
+<style lang="scss" module>
+.registry {
+ padding: 20px;
+ cursor: pointer;
+
+ &Name {
+ font-weight: 700;
+ }
+
+ &Time {
+ font-size: .85em;
+ opacity: .7;
+ }
+}
+</style>
diff --git a/packages/client/src/store.ts b/packages/client/src/store.ts
index b9800ec607..296eaa2068 100644
--- a/packages/client/src/store.ts
+++ b/packages/client/src/store.ts
@@ -286,6 +286,16 @@ export class ColdDeviceStorage {
}
}
+ public static getAll(): Partial<typeof this.default> {
+ return (Object.keys(this.default) as (keyof typeof this.default)[]).reduce((acc, key) => {
+ const value = localStorage.getItem(PREFIX + key);
+ if (value != null) {
+ acc[key] = JSON.parse(value);
+ }
+ return acc;
+ }, {} as any);
+ }
+
public static set<T extends keyof typeof ColdDeviceStorage.default>(key: T, value: typeof ColdDeviceStorage.default[T]): void {
localStorage.setItem(PREFIX + key, JSON.stringify(value));