Registry (#7073)
* wip * wip * wip * wip * wip * Update registry.value.vue * wip * wip * wip * wip * typo
This commit is contained in:
@ -7,7 +7,6 @@ import { waiting } from '@/os';
|
||||
type Account = {
|
||||
id: string;
|
||||
token: string;
|
||||
clientData: Record<string, any>;
|
||||
};
|
||||
|
||||
const data = localStorage.getItem('account');
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -347,14 +347,6 @@ if ($i) {
|
||||
updateAccount({ hasUnreadAnnouncement: false });
|
||||
});
|
||||
|
||||
main.on('clientSettingUpdated', x => {
|
||||
updateAccount({
|
||||
clientData: {
|
||||
[x.key]: x.value
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// トークンが再生成されたとき
|
||||
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
||||
main.on('myTokenRegenerated', () => {
|
||||
|
@ -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>
|
||||
|
@ -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();
|
||||
|
@ -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
|
||||
}
|
||||
},
|
||||
|
||||
|
115
src/client/pages/settings/registry.keys.vue
Normal file
115
src/client/pages/settings/registry.keys.vue
Normal file
@ -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>
|
149
src/client/pages/settings/registry.value.vue
Normal file
149
src/client/pages/settings/registry.value.vue
Normal file
@ -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>
|
91
src/client/pages/settings/registry.vue
Normal file
91
src/client/pages/settings/registry.vue
Normal file
@ -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>
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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();
|
||||
}
|
||||
|
Reference in New Issue
Block a user