summaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2021-01-11 20:38:34 +0900
committerGitHub <noreply@github.com>2021-01-11 20:38:34 +0900
commit6c975275f82c79eed2c7757d55283c95d23ca5b8 (patch)
tree2871e4c3a1a67295ea5c3e19b9136ae79a17088c /src/client
parentfix context menu (diff)
downloadmisskey-6c975275f82c79eed2c7757d55283c95d23ca5b8.tar.gz
misskey-6c975275f82c79eed2c7757d55283c95d23ca5b8.tar.bz2
misskey-6c975275f82c79eed2c7757d55283c95d23ca5b8.zip
Registry (#7073)
* wip * wip * wip * wip * wip * Update registry.value.vue * wip * wip * wip * wip * typo
Diffstat (limited to 'src/client')
-rw-r--r--src/client/account.ts1
-rw-r--r--src/client/components/post-form.vue2
-rw-r--r--src/client/components/ui/info.vue2
-rw-r--r--src/client/init.ts8
-rw-r--r--src/client/pages/settings/deck.vue21
-rw-r--r--src/client/pages/settings/index.vue30
-rw-r--r--src/client/pages/settings/other.vue8
-rw-r--r--src/client/pages/settings/registry.keys.vue115
-rw-r--r--src/client/pages/settings/registry.value.vue149
-rw-r--r--src/client/pages/settings/registry.vue91
-rw-r--r--src/client/pizzax.ts56
-rw-r--r--src/client/router.ts1
-rw-r--r--src/client/ui/deck.vue3
-rw-r--r--src/client/ui/deck/deck-store.ts85
14 files changed, 517 insertions, 55 deletions
diff --git a/src/client/account.ts b/src/client/account.ts
index fdf49ee213..e6ee8613d2 100644
--- a/src/client/account.ts
+++ b/src/client/account.ts
@@ -7,7 +7,6 @@ import { waiting } from '@/os';
type Account = {
id: string;
token: string;
- clientData: Record<string, any>;
};
const data = localStorage.getItem('account');
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index 19773b3b6c..bf300eebd8 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -262,7 +262,7 @@ export default defineComponent({
}
// keep cw when reply
- if (this.$store.keepCw && this.reply && this.reply.cw) {
+ if (this.$store.state.keepCw && this.reply && this.reply.cw) {
this.useCw = true;
this.cw = this.reply.cw;
}
diff --git a/src/client/components/ui/info.vue b/src/client/components/ui/info.vue
index 3bdb69b3d1..5c71b14a0a 100644
--- a/src/client/components/ui/info.vue
+++ b/src/client/components/ui/info.vue
@@ -34,7 +34,7 @@ export default defineComponent({
font-size: 90%;
background: var(--infoBg);
color: var(--infoFg);
- border-radius: 5px;
+ border-radius: var(--radius);
&.warn {
background: var(--infoWarnBg);
diff --git a/src/client/init.ts b/src/client/init.ts
index f39f50eea6..f09097fe31 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -347,14 +347,6 @@ if ($i) {
updateAccount({ hasUnreadAnnouncement: false });
});
- main.on('clientSettingUpdated', x => {
- updateAccount({
- clientData: {
- [x.key]: x.value
- }
- });
- });
-
// トークンが再生成されたとき
// このままではMisskeyが利用できないので強制的にサインアウトさせる
main.on('myTokenRegenerated', () => {
diff --git a/src/client/pages/settings/deck.vue b/src/client/pages/settings/deck.vue
index 0d9f1ab0aa..30d36d4a06 100644
--- a/src/client/pages/settings/deck.vue
+++ b/src/client/pages/settings/deck.vue
@@ -24,6 +24,8 @@
<span>{{ $ts._deck.columnMargin }}</span>
<template #suffix>px</template>
</FormInput>
+
+ <FormLink @click="setProfile">{{ $ts._deck.profile }}<template #suffix>{{ profile }}</template></FormLink>
</FormBase>
</template>
@@ -31,7 +33,7 @@
import { defineComponent } from 'vue';
import { faImage, faCog, faColumns } from '@fortawesome/free-solid-svg-icons';
import FormSwitch from '@/components/form/switch.vue';
-import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/form/link.vue';
import FormRadios from '@/components/form/radios.vue';
import FormInput from '@/components/form/input.vue';
import FormBase from '@/components/form/base.vue';
@@ -42,7 +44,7 @@ import * as os from '@/os';
export default defineComponent({
components: {
FormSwitch,
- FormSelect,
+ FormLink,
FormInput,
FormRadios,
FormBase,
@@ -67,6 +69,7 @@ export default defineComponent({
columnAlign: deckStore.makeGetterSetter('columnAlign'),
columnMargin: deckStore.makeGetterSetter('columnMargin'),
columnHeaderHeight: deckStore.makeGetterSetter('columnHeaderHeight'),
+ profile: deckStore.makeGetterSetter('profile'),
},
watch: {
@@ -85,5 +88,19 @@ export default defineComponent({
mounted() {
this.$emit('info', this.INFO);
},
+
+ methods: {
+ async setProfile() {
+ const { canceled, result: name } = await os.dialog({
+ title: this.$ts._deck.profile,
+ input: {
+ allowEmpty: false
+ }
+ });
+ if (canceled) return;
+ this.profile = name;
+ location.reload();
+ }
+ }
});
</script>
diff --git a/src/client/pages/settings/index.vue b/src/client/pages/settings/index.vue
index aa9fe27164..0f95a76f11 100644
--- a/src/client/pages/settings/index.vue
+++ b/src/client/pages/settings/index.vue
@@ -35,13 +35,13 @@
</FormGroup>
</FormBase>
<div class="main">
- <component :is="component" @info="onInfo"/>
+ <component :is="component" :key="page" @info="onInfo" v-bind="pageProps"/>
</div>
</div>
</template>
<script lang="ts">
-import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, ref, watch } from 'vue';
+import { computed, defineAsyncComponent, defineComponent, nextTick, onMounted, reactive, ref, watch } from 'vue';
import { faCog, faPalette, faPlug, faUser, faListUl, faLock, faCommentSlash, faMusic, faCogs, faEllipsisH, faBan, faShareAlt, faLockOpen, faKey, faBoxes } from '@fortawesome/free-solid-svg-icons';
import { faLaugh, faBell, faEnvelope } from '@fortawesome/free-regular-svg-icons';
import { i18n } from '@/i18n';
@@ -78,7 +78,9 @@ export default defineComponent({
const onInfo = (viewInfo) => {
INFO.value = viewInfo;
};
+ const pageProps = ref({});
const component = computed(() => {
+ if (props.page == null) return null;
switch (props.page) {
case 'profile': return defineAsyncComponent(() => import('./profile.vue'));
case 'privacy': return defineAsyncComponent(() => import('./privacy.vue'));
@@ -104,16 +106,35 @@ export default defineComponent({
case 'plugins': return defineAsyncComponent(() => import('./plugins.vue'));
case 'import-export': return defineAsyncComponent(() => import('./import-export.vue'));
case 'account-info': return defineAsyncComponent(() => import('./account-info.vue'));
+ case 'registry': return defineAsyncComponent(() => import('./registry.vue'));
case 'experimental-features': return defineAsyncComponent(() => import('./experimental-features.vue'));
- default: return null;
+ }
+ if (props.page.startsWith('registry/keys/system/')) {
+ return defineAsyncComponent(() => import('./registry.keys.vue'));
+ }
+ if (props.page.startsWith('registry/value/system/')) {
+ return defineAsyncComponent(() => import('./registry.value.vue'));
}
});
watch(component, () => {
+ pageProps.value = {};
+
+ if (props.page) {
+ if (props.page.startsWith('registry/keys/system/')) {
+ pageProps.value.scope = props.page.replace('registry/keys/system/', '').split('/');
+ }
+ if (props.page.startsWith('registry/value/system/')) {
+ const path = props.page.replace('registry/value/system/', '').split('/');
+ pageProps.value.xKey = path.pop();
+ pageProps.value.scope = path;
+ }
+ }
+
nextTick(() => {
scroll(el.value, 0);
});
- });
+ }, { immediate: true });
onMounted(() => {
narrow.value = el.value.offsetWidth < 1025;
@@ -125,6 +146,7 @@ export default defineComponent({
view,
el,
onInfo,
+ pageProps,
component,
logout: () => {
signout();
diff --git a/src/client/pages/settings/other.vue b/src/client/pages/settings/other.vue
index 67edaf3faa..bc42b747d5 100644
--- a/src/client/pages/settings/other.vue
+++ b/src/client/pages/settings/other.vue
@@ -15,16 +15,17 @@
DEBUG MODE
</FormSwitch>
<template v-if="debug">
- <FormLink to="/settings/regedit">RegEdit</FormLink>
<FormButton @click="taskmanager">Task Manager</FormButton>
</template>
</FormGroup>
+
+ <FormLink to="/settings/registry"><template #icon><Fa :icon="faCogs"/></template>{{ $ts.registry }}</FormLink>
</FormBase>
</template>
<script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue';
-import { faEllipsisH } from '@fortawesome/free-solid-svg-icons';
+import { faEllipsisH, faCogs } from '@fortawesome/free-solid-svg-icons';
import FormSwitch from '@/components/form/switch.vue';
import FormSelect from '@/components/form/select.vue';
import FormLink from '@/components/form/link.vue';
@@ -53,7 +54,8 @@ export default defineComponent({
title: this.$ts.other,
icon: faEllipsisH
},
- debug
+ debug,
+ faCogs
}
},
diff --git a/src/client/pages/settings/registry.keys.vue b/src/client/pages/settings/registry.keys.vue
new file mode 100644
index 0000000000..c7a90fb461
--- /dev/null
+++ b/src/client/pages/settings/registry.keys.vue
@@ -0,0 +1,115 @@
+<template>
+<FormBase>
+ <FormGroup>
+ <FormKeyValueView>
+ <template #key>{{ $ts._registry.domain }}</template>
+ <template #value>{{ $ts.system }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $ts._registry.scope }}</template>
+ <template #value>{{ scope.join('/') }}</template>
+ </FormKeyValueView>
+ </FormGroup>
+
+ <FormGroup v-if="keys">
+ <template #label>{{ $ts._registry.keys }}</template>
+ <FormLink v-for="key in keys" :to="`/settings/registry/value/system/${scope.join('/')}/${key[0]}`" class="_monospace">{{ key[0] }}<template #suffix>{{ key[1].toUpperCase() }}</template></FormLink>
+ </FormGroup>
+
+ <FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import { faCogs } from '@fortawesome/free-solid-svg-icons';
+import * as JSON5 from 'json5';
+import MkInfo from '@/components/ui/info.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/form/link.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormButton from '@/components/form/button.vue';
+import FormKeyValueView from '@/components/form/key-value-view.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ MkInfo,
+ FormBase,
+ FormSelect,
+ FormSwitch,
+ FormButton,
+ FormLink,
+ FormGroup,
+ FormKeyValueView,
+ },
+
+ props: {
+ scope: {
+ required: true
+ }
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ title: this.$ts.registry,
+ icon: faCogs
+ },
+ keys: null,
+ }
+ },
+
+ watch: {
+ scope() {
+ this.fetch();
+ }
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ this.fetch();
+ },
+
+ methods: {
+ fetch() {
+ os.api('i/registry/keys-with-type', {
+ scope: this.scope
+ }).then(keys => {
+ this.keys = Object.entries(keys).sort((a, b) => a[0].localeCompare(b[0]));
+ });
+ },
+
+ async createKey() {
+ const { canceled, result } = await os.form(this.$ts._registry.createKey, {
+ key: {
+ type: 'string',
+ label: this.$ts._registry.key,
+ },
+ value: {
+ type: 'string',
+ multiline: true,
+ label: this.$ts.value,
+ },
+ scope: {
+ type: 'string',
+ label: this.$ts._registry.scope,
+ default: this.scope.join('/')
+ }
+ });
+ if (canceled) return;
+ os.apiWithDialog('i/registry/set', {
+ scope: result.scope.split('/'),
+ key: result.key,
+ value: JSON5.parse(result.value),
+ }).then(() => {
+ this.fetch();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/settings/registry.value.vue b/src/client/pages/settings/registry.value.vue
new file mode 100644
index 0000000000..943ededd21
--- /dev/null
+++ b/src/client/pages/settings/registry.value.vue
@@ -0,0 +1,149 @@
+<template>
+<FormBase>
+ <MkInfo warn>{{ $ts.editTheseSettingsMayBreakAccount }}</MkInfo>
+
+ <template v-if="value">
+ <FormGroup>
+ <FormKeyValueView>
+ <template #key>{{ $ts._registry.domain }}</template>
+ <template #value>{{ $ts.system }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $ts._registry.scope }}</template>
+ <template #value>{{ scope.join('/') }}</template>
+ </FormKeyValueView>
+ <FormKeyValueView>
+ <template #key>{{ $ts._registry.key }}</template>
+ <template #value>{{ xKey }}</template>
+ </FormKeyValueView>
+ </FormGroup>
+
+ <FormGroup>
+ <FormTextarea tall v-model:value="valueForEditor" class="_monospace" style="tab-size: 2;">
+ <span>{{ $ts.value }} (JSON)</span>
+ </FormTextarea>
+ <FormButton @click="save" primary><Fa :icon="faSave"/> {{ $ts.save }}</FormButton>
+ </FormGroup>
+
+ <FormKeyValueView>
+ <template #key>{{ $ts.updatedAt }}</template>
+ <template #value><MkTime :time="value.updatedAt" mode="detail"/></template>
+ </FormKeyValueView>
+
+ <FormButton danger @click="del"><Fa :icon="faTrash"/> {{ $ts.delete }}</FormButton>
+ </template>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import { faCogs, faSave, faTrash } from '@fortawesome/free-solid-svg-icons';
+import * as JSON5 from 'json5';
+import MkInfo from '@/components/ui/info.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormTextarea from '@/components/form/textarea.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormButton from '@/components/form/button.vue';
+import FormKeyValueView from '@/components/form/key-value-view.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ MkInfo,
+ FormBase,
+ FormSelect,
+ FormSwitch,
+ FormButton,
+ FormTextarea,
+ FormGroup,
+ FormKeyValueView,
+ },
+
+ props: {
+ scope: {
+ required: true
+ },
+ xKey: {
+ required: true
+ },
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ title: this.$ts.registry,
+ icon: faCogs
+ },
+ value: null,
+ valueForEditor: null,
+ faSave, faTrash,
+ }
+ },
+
+ watch: {
+ key() {
+ this.fetch();
+ },
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ this.fetch();
+ },
+
+ methods: {
+ fetch() {
+ os.api('i/registry/get-detail', {
+ scope: this.scope,
+ key: this.xKey
+ }).then(value => {
+ this.value = value;
+ this.valueForEditor = JSON5.stringify(this.value.value, null, '\t');
+ });
+ },
+
+ save() {
+ try {
+ JSON5.parse(this.valueForEditor);
+ } catch (e) {
+ os.dialog({
+ type: 'error',
+ text: this.$ts.invalidValue
+ });
+ return;
+ }
+
+ os.dialog({
+ type: 'warning',
+ text: this.$ts.saveConfirm,
+ showCancelButton: true
+ }).then(({ canceled }) => {
+ if (canceled) return;
+ os.apiWithDialog('i/registry/set', {
+ scope: this.scope,
+ key: this.xKey,
+ value: JSON5.parse(this.valueForEditor)
+ });
+ });
+ },
+
+ del() {
+ os.dialog({
+ type: 'warning',
+ text: this.$ts.deleteConfirm,
+ showCancelButton: true
+ }).then(({ canceled }) => {
+ if (canceled) return;
+ os.apiWithDialog('i/registry/remove', {
+ scope: this.scope,
+ key: this.xKey
+ });
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pages/settings/registry.vue b/src/client/pages/settings/registry.vue
new file mode 100644
index 0000000000..a43c98e730
--- /dev/null
+++ b/src/client/pages/settings/registry.vue
@@ -0,0 +1,91 @@
+<template>
+<FormBase>
+ <FormGroup v-if="scopes">
+ <template #label>{{ $ts.system }}</template>
+ <FormLink v-for="scope in scopes" :to="`/settings/registry/keys/system/${scope.join('/')}`" class="_monospace">{{ scope.join('/') }}</FormLink>
+ </FormGroup>
+ <FormButton @click="createKey" primary>{{ $ts._registry.createKey }}</FormButton>
+</FormBase>
+</template>
+
+<script lang="ts">
+import { defineAsyncComponent, defineComponent } from 'vue';
+import { faCogs } from '@fortawesome/free-solid-svg-icons';
+import * as JSON5 from 'json5';
+import MkInfo from '@/components/ui/info.vue';
+import FormSwitch from '@/components/form/switch.vue';
+import FormSelect from '@/components/form/select.vue';
+import FormLink from '@/components/form/link.vue';
+import FormBase from '@/components/form/base.vue';
+import FormGroup from '@/components/form/group.vue';
+import FormButton from '@/components/form/button.vue';
+import FormKeyValueView from '@/components/form/key-value-view.vue';
+import * as os from '@/os';
+
+export default defineComponent({
+ components: {
+ MkInfo,
+ FormBase,
+ FormSelect,
+ FormSwitch,
+ FormButton,
+ FormLink,
+ FormGroup,
+ FormKeyValueView,
+ },
+
+ emits: ['info'],
+
+ data() {
+ return {
+ INFO: {
+ title: this.$ts.registry,
+ icon: faCogs
+ },
+ scopes: null,
+ }
+ },
+
+ created() {
+ this.fetch();
+ },
+
+ mounted() {
+ this.$emit('info', this.INFO);
+ },
+
+ methods: {
+ fetch() {
+ os.api('i/registry/scopes').then(scopes => {
+ this.scopes = scopes.slice().sort((a, b) => a.join('/').localeCompare(b.join('/')));
+ });
+ },
+
+ async createKey() {
+ const { canceled, result } = await os.form(this.$ts._registry.createKey, {
+ key: {
+ type: 'string',
+ label: this.$ts._registry.key,
+ },
+ value: {
+ type: 'string',
+ multiline: true,
+ label: this.$ts.value,
+ },
+ scope: {
+ type: 'string',
+ label: this.$ts._registry.scope,
+ }
+ });
+ if (canceled) return;
+ os.apiWithDialog('i/registry/set', {
+ scope: result.scope.split('/'),
+ key: result.key,
+ value: JSON5.parse(result.value),
+ }).then(() => {
+ this.fetch();
+ });
+ }
+ }
+});
+</script>
diff --git a/src/client/pizzax.ts b/src/client/pizzax.ts
index fdaf2bebb6..794738edd4 100644
--- a/src/client/pizzax.ts
+++ b/src/client/pizzax.ts
@@ -11,6 +11,7 @@ type ArrayElement<A> = A extends readonly (infer T)[] ? T : never;
export class Storage<T extends StateDef> {
public readonly key: string;
+ public readonly keyForLocalStorage: string;
public readonly def: T;
@@ -19,20 +20,22 @@ export class Storage<T extends StateDef> {
public readonly reactiveState: { [K in keyof T]: Ref<T[K]['default']> };
constructor(key: string, def: T) {
- this.key = 'pizzax::' + key;
+ this.key = key;
+ this.keyForLocalStorage = 'pizzax::' + key;
this.def = def;
// TODO: indexedDBにする
- const deviceState = JSON.parse(localStorage.getItem(this.key) || '{}');
- const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.key + '::' + $i.id) || '{}') : {};
+ const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
+ const deviceAccountState = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}') : {};
+ const registryCache = $i ? JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}') : {};
const state = {};
const reactiveState = {};
for (const [k, v] of Object.entries(def)) {
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
state[k] = deviceState[k];
- } else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call($i.clientData, k)) {
- state[k] = $i.clientData[k];
+ } else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
+ state[k] = registryCache[k];
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
state[k] = deviceAccountState[k];
} else {
@@ -47,16 +50,24 @@ export class Storage<T extends StateDef> {
this.reactiveState = reactiveState as any;
if ($i) {
- watch($i, () => {
- if (_DEV_) console.log('$i updated');
-
- for (const [k, v] of Object.entries(def)) {
- if (v.where === 'account' && Object.prototype.hasOwnProperty.call($i!.clientData, k)) {
- state[k] = $i!.clientData[k];
- reactiveState[k].value = $i!.clientData[k];
+ // なぜかsetTimeoutしないとapi関数内でエラーになる(おそらく循環参照してることに原因がありそう)
+ setTimeout(() => {
+ api('i/registry/get-all', { scope: ['client', this.key] }).then(kvs => {
+ for (const [k, v] of Object.entries(def)) {
+ if (v.where === 'account') {
+ if (Object.prototype.hasOwnProperty.call(kvs, k)) {
+ state[k] = kvs[k];
+ reactiveState[k].value = kvs[k];
+ } else {
+ state[k] = v.default;
+ reactiveState[k].value = v.default;
+ }
+ }
}
- }
- });
+ });
+ }, 1);
+
+ // TODO: streamingのuser storage updateイベントを監視して更新
}
}
@@ -68,21 +79,26 @@ export class Storage<T extends StateDef> {
switch (this.def[key].where) {
case 'device': {
- const deviceState = JSON.parse(localStorage.getItem(this.key) || '{}');
+ const deviceState = JSON.parse(localStorage.getItem(this.keyForLocalStorage) || '{}');
deviceState[key] = value;
- localStorage.setItem(this.key, JSON.stringify(deviceState));
+ localStorage.setItem(this.keyForLocalStorage, JSON.stringify(deviceState));
break;
}
case 'deviceAccount': {
if ($i == null) break;
- const deviceAccountState = JSON.parse(localStorage.getItem(this.key + '::' + $i.id) || '{}');
+ const deviceAccountState = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::' + $i.id) || '{}');
deviceAccountState[key] = value;
- localStorage.setItem(this.key + '::' + $i.id, JSON.stringify(deviceAccountState));
+ localStorage.setItem(this.keyForLocalStorage + '::' + $i.id, JSON.stringify(deviceAccountState));
break;
}
case 'account': {
- api('i/update-client-setting', {
- name: key,
+ if ($i == null) break;
+ const cache = JSON.parse(localStorage.getItem(this.keyForLocalStorage + '::cache::' + $i.id) || '{}');
+ cache[key] = value;
+ localStorage.setItem(this.keyForLocalStorage + '::cache::' + $i.id, JSON.stringify(cache));
+ api('i/registry/set', {
+ scope: ['client', this.key],
+ key: key,
value: value
});
break;
diff --git a/src/client/router.ts b/src/client/router.ts
index 5753a47024..6f79426b23 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -81,7 +81,6 @@ export const router = createRouter({
{ path: '/miauth/:session', component: page('miauth') },
{ path: '/authorize-follow', component: page('follow') },
{ path: '/share', component: page('share') },
- { path: '/test', component: page('test') },
{ path: '/:catchAll(.*)', component: page('not-found') }
],
// なんかHacky
diff --git a/src/client/ui/deck.vue b/src/client/ui/deck.vue
index 099a6f60c6..a074629ddd 100644
--- a/src/client/ui/deck.vue
+++ b/src/client/ui/deck.vue
@@ -41,7 +41,7 @@ import { getScrollContainer } from '@/scripts/scroll';
import * as os from '@/os';
import { sidebarDef } from '@/sidebar';
import XCommon from './_common_/common.vue';
-import { deckStore, addColumn } from './deck/deck-store';
+import { deckStore, addColumn, loadDeck } from './deck/deck-store';
export default defineComponent({
components: {
@@ -88,6 +88,7 @@ export default defineComponent({
document.documentElement.style.overflowY = 'hidden';
document.documentElement.style.scrollBehavior = 'auto';
window.addEventListener('wheel', this.onWheel);
+ loadDeck();
},
mounted() {
diff --git a/src/client/ui/deck/deck-store.ts b/src/client/ui/deck/deck-store.ts
index 3d2e1873d3..93ea0a3228 100644
--- a/src/client/ui/deck/deck-store.ts
+++ b/src/client/ui/deck/deck-store.ts
@@ -1,5 +1,7 @@
+import { throttle } from 'throttle-debounce';
import { i18n } from '@/i18n';
-import { markRaw } from 'vue';
+import { api } from '@/os';
+import { markRaw, watch } from 'vue';
import { Storage } from '../../pizzax';
type ColumnWidget = {
@@ -21,23 +23,17 @@ function copy<T>(x: T): T {
}
export const deckStore = markRaw(new Storage('deck', {
+ profile: {
+ where: 'deviceAccount',
+ default: 'default'
+ },
columns: {
where: 'deviceAccount',
- default: [{
- id: 'a',
- type: 'main',
- name: i18n.locale._deck._columns.main,
- width: 350,
- }, {
- id: 'b',
- type: 'notifications',
- name: i18n.locale._deck._columns.notifications,
- width: 330,
- }] as Column[]
+ default: [] as Column[]
},
layout: {
where: 'deviceAccount',
- default: [['a'], ['b']] as Column['id'][][]
+ default: [] as Column['id'][][]
},
columnAlign: {
where: 'deviceAccount',
@@ -61,10 +57,60 @@ export const deckStore = markRaw(new Storage('deck', {
},
}));
+export const loadDeck = async () => {
+ let deck;
+
+ try {
+ deck = await api('i/registry/get', {
+ scope: ['client', 'deck', 'profiles'],
+ key: deckStore.state.profile,
+ });
+ } catch (e) {
+ if (e.code === 'NO_SUCH_KEY') {
+ // 後方互換性のため
+ if (deckStore.state.profile === 'default') {
+ saveDeck();
+ return;
+ }
+
+ deckStore.set('columns', [{
+ id: 'a',
+ type: 'main',
+ name: i18n.locale._deck._columns.main,
+ width: 350,
+ }, {
+ id: 'b',
+ type: 'notifications',
+ name: i18n.locale._deck._columns.notifications,
+ width: 330,
+ }]);
+ deckStore.set('layout', [['a'], ['b']]);
+ return;
+ }
+ throw e;
+ }
+
+ deckStore.set('columns', deck.columns);
+ deckStore.set('layout', deck.layout);
+};
+
+// TODO: deckがloadされていない状態でsaveすると意図せず上書きが発生するので対策する
+export const saveDeck = throttle(1000, () => {
+ api('i/registry/set', {
+ scope: ['client', 'deck', 'profiles'],
+ key: deckStore.state.profile,
+ value: {
+ columns: deckStore.reactiveState.columns.value,
+ layout: deckStore.reactiveState.layout.value,
+ }
+ });
+});
+
export function addColumn(column: Column) {
if (column.name == undefined) column.name = null;
deckStore.push('columns', column);
deckStore.push('layout', [column.id]);
+ saveDeck();
}
export function removeColumn(id: Column['id']) {
@@ -72,6 +118,7 @@ export function removeColumn(id: Column['id']) {
deckStore.set('layout', deckStore.state.layout
.map(ids => ids.filter(_id => _id !== id))
.filter(ids => ids.length > 0));
+ saveDeck();
}
export function swapColumn(a: Column['id'], b: Column['id']) {
@@ -83,6 +130,7 @@ export function swapColumn(a: Column['id'], b: Column['id']) {
layout[aX][aY] = b;
layout[bX][bY] = a;
deckStore.set('layout', layout);
+ saveDeck();
}
export function swapLeftColumn(id: Column['id']) {
@@ -98,6 +146,7 @@ export function swapLeftColumn(id: Column['id']) {
return true;
}
});
+ saveDeck();
}
export function swapRightColumn(id: Column['id']) {
@@ -113,6 +162,7 @@ export function swapRightColumn(id: Column['id']) {
return true;
}
});
+ saveDeck();
}
export function swapUpColumn(id: Column['id']) {
@@ -132,6 +182,7 @@ export function swapUpColumn(id: Column['id']) {
return true;
}
});
+ saveDeck();
}
export function swapDownColumn(id: Column['id']) {
@@ -151,6 +202,7 @@ export function swapDownColumn(id: Column['id']) {
return true;
}
});
+ saveDeck();
}
export function stackLeftColumn(id: Column['id']) {
@@ -160,6 +212,7 @@ export function stackLeftColumn(id: Column['id']) {
layout[i - 1].push(id);
layout = layout.filter(ids => ids.length > 0);
deckStore.set('layout', layout);
+ saveDeck();
}
export function popRightColumn(id: Column['id']) {
@@ -169,6 +222,7 @@ export function popRightColumn(id: Column['id']) {
layout.splice(i + 1, 0, [id]);
layout = layout.filter(ids => ids.length > 0);
deckStore.set('layout', layout);
+ saveDeck();
}
export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
@@ -180,6 +234,7 @@ export function addColumnWidget(id: Column['id'], widget: ColumnWidget) {
column.widgets.unshift(widget);
columns[columnIndex] = column;
deckStore.set('columns', columns);
+ saveDeck();
}
export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
@@ -190,6 +245,7 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
column.widgets = column.widgets.filter(w => w.id != widget.id);
columns[columnIndex] = column;
deckStore.set('columns', columns);
+ saveDeck();
}
export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
@@ -200,6 +256,7 @@ export function setColumnWidgets(id: Column['id'], widgets: ColumnWidget[]) {
column.widgets = widgets;
columns[columnIndex] = column;
deckStore.set('columns', columns);
+ saveDeck();
}
export function updateColumnWidget(id: Column['id'], widgetId: string, data: any) {
@@ -213,6 +270,7 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, data: any
} : w);
columns[columnIndex] = column;
deckStore.set('columns', columns);
+ saveDeck();
}
export function updateColumn(id: Column['id'], column: Partial<Column>) {
@@ -225,4 +283,5 @@ export function updateColumn(id: Column['id'], column: Partial<Column>) {
}
columns[columnIndex] = currentColumn;
deckStore.set('columns', columns);
+ saveDeck();
}