Merge b6ca1a5e90 into 794cb9ffe2
This commit is contained in:
commit
2ad1ba9fd4
19 changed files with 450 additions and 33 deletions
|
|
@ -14,7 +14,7 @@ import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
|
|||
import { updateI18n, i18n } from '@/i18n.js';
|
||||
import { $i, refreshAccount, login } from '@/account.js';
|
||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
|
||||
import { fetchInstance, instance } from '@/instance.js';
|
||||
import { initInstance, instance } from '@/instance.js';
|
||||
import { deviceKind } from '@/scripts/device-kind.js';
|
||||
import { reloadChannel } from '@/scripts/unison-reload.js';
|
||||
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
|
||||
|
|
@ -115,15 +115,12 @@ export async function common(createVue: () => App<Element>) {
|
|||
const html = document.documentElement;
|
||||
html.setAttribute('lang', lang);
|
||||
//#endregion
|
||||
|
||||
|
||||
await initInstance();
|
||||
await defaultStore.ready;
|
||||
await deckStore.ready;
|
||||
|
||||
const fetchInstanceMetaPromise = fetchInstance();
|
||||
|
||||
fetchInstanceMetaPromise.then(() => {
|
||||
miLocalStorage.setItem('v', instance.version);
|
||||
});
|
||||
miLocalStorage.setItem('v', instance.version);
|
||||
|
||||
//#region loginId
|
||||
const params = new URLSearchParams(location.search);
|
||||
|
|
@ -177,13 +174,11 @@ export async function common(createVue: () => App<Element>) {
|
|||
});
|
||||
//#endregion
|
||||
|
||||
fetchInstanceMetaPromise.then(() => {
|
||||
if (defaultStore.state.themeInitial) {
|
||||
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
|
||||
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
|
||||
defaultStore.set('themeInitial', false);
|
||||
}
|
||||
});
|
||||
if (defaultStore.state.themeInitial) {
|
||||
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
|
||||
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
|
||||
defaultStore.set('themeInitial', false);
|
||||
}
|
||||
|
||||
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
|
||||
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ const providedMetaEl = document.getElementById('misskey_meta');
|
|||
|
||||
let cachedMeta = miLocalStorage.getItem('instance') ? JSON.parse(miLocalStorage.getItem('instance')!) : null;
|
||||
let cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
|
||||
const providedMeta = providedMetaEl && providedMetaEl.textContent ? JSON.parse(providedMetaEl.textContent) : null;
|
||||
const providedMeta: Misskey.entities.MetaDetailed | null = providedMetaEl && providedMetaEl.textContent ? JSON.parse(providedMetaEl.textContent) : null;
|
||||
const providedAt = providedMetaEl && providedMetaEl.dataset.generatedAt ? parseInt(providedMetaEl.dataset.generatedAt) : 0;
|
||||
if (providedAt > cachedAt) {
|
||||
miLocalStorage.setItem('instance', JSON.stringify(providedMeta));
|
||||
|
|
@ -38,6 +38,19 @@ export const notFoundImageUrl = computed(() => instance.notFoundImageUrl ?? DEFA
|
|||
|
||||
export const isEnabledUrlPreview = computed(() => instance.enableUrlPreview ?? true);
|
||||
|
||||
/** instanceの中身が入っていることを保証する */
|
||||
export async function initInstance() {
|
||||
if (instance == null || Object.keys(instance).length === 0) {
|
||||
if (providedMeta != null) {
|
||||
for (const [k, v] of Object.entries(providedMeta)) {
|
||||
instance[k] = v;
|
||||
}
|
||||
} else {
|
||||
await fetchInstance(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function fetchInstance(force = false): Promise<Misskey.entities.MetaDetailed> {
|
||||
if (!force) {
|
||||
const cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
|
||||
|
|
@ -60,3 +73,9 @@ export async function fetchInstance(force = false): Promise<Misskey.entities.Met
|
|||
|
||||
return instance;
|
||||
}
|
||||
|
||||
/** キャッシュだけ飛ばす(リロード後からは新しい設定を読み込む) */
|
||||
export function pruneInstanceCache() {
|
||||
miLocalStorage.removeItem('instance');
|
||||
miLocalStorage.removeItem('instanceCachedAt');
|
||||
}
|
||||
|
|
|
|||
271
packages/frontend/src/pages/admin/client-setting-overrides.vue
Normal file
271
packages/frontend/src/pages/admin/client-setting-overrides.vue
Normal file
|
|
@ -0,0 +1,271 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :tabs="headerTabs"/></template>
|
||||
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="32">
|
||||
<div class="_gaps">
|
||||
<MkInfo warn :class="$style.warn">{{ i18n.ts.clientSettingOverridesWarn }}</MkInfo>
|
||||
<div v-if="fetching">
|
||||
<MkLoading/>
|
||||
</div>
|
||||
<div v-else class="_gaps_s">
|
||||
<MkInput v-model="query" type="search">
|
||||
<template #prefix><i class="ti ti-search"></i></template>
|
||||
</MkInput>
|
||||
|
||||
<MkFolder
|
||||
v-for="def, key in clientSettingOverrides"
|
||||
:key="key"
|
||||
v-show="query === '' || key.toLowerCase().includes(query.toLowerCase())"
|
||||
>
|
||||
<template #label>{{ key }}</template>
|
||||
<template #suffix>
|
||||
<span v-if="def.enableOverride && def.overrideValue != null && def.overrideValue !== def.defaultValue" class="_warn">{{ i18n.ts.modified }}</span>
|
||||
</template>
|
||||
<div class="_gaps">
|
||||
<MkKeyValue>
|
||||
<template #key>{{ i18n.ts.default }}</template>
|
||||
<template #value>
|
||||
<MkCode v-bind="getMkCodeProps(def)"></MkCode>
|
||||
</template>
|
||||
</MkKeyValue>
|
||||
<MkSwitch v-model="def.enableOverride">{{ i18n.ts.enableOverride }}</MkSwitch>
|
||||
<MkInput v-if="def.formType === 'text'" v-model="def.overrideValue" :disabled="!def.enableOverride" type="text">
|
||||
<template #label>{{ i18n.ts.overrideValue }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-else-if="def.formType === 'number'" v-model="def.overrideValue" :disabled="!def.enableOverride" type="number">
|
||||
<template #label>{{ i18n.ts.overrideValue }}</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-else-if="def.formType === 'boolean'" v-model="def.overrideValue" :disabled="!def.enableOverride">
|
||||
<template #label>{{ i18n.ts.overrideValue }}</template>
|
||||
<template #caption>{{ i18n.ts.onToTrue }}</template>
|
||||
</MkSwitch>
|
||||
<MkTextarea v-else-if="def.formType === 'codeEditor'" v-model="def.overrideValue" :disabled="!def.enableOverride" pre code>
|
||||
<template #label>{{ i18n.ts.overrideValue }}</template>
|
||||
</MkTextarea>
|
||||
</div>
|
||||
</MkFolder>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
<template #footer>
|
||||
<div :class="$style.footer">
|
||||
<div :class="$style.footerInner">
|
||||
<div class="_buttons">
|
||||
<MkButton primary @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
<MkButton danger @click="reset"><i class="ti ti-trash"></i> {{ i18n.ts.reset }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
|
||||
import { pruneInstanceCache } from '@/instance.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
||||
import MkCode from '@/components/MkCode.vue';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { reloadAsk } from '@/scripts/reload-ask.js';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkTextarea from '@/components/MkTextarea.vue';
|
||||
import JSON5 from 'json5';
|
||||
|
||||
const query = ref('');
|
||||
|
||||
const notConfigurableDefaultStoreSettings = [
|
||||
'accountSetupWizard',
|
||||
'timelineTutorials',
|
||||
'abusesTutorial',
|
||||
'memo',
|
||||
'mutedAds',
|
||||
'statusbars',
|
||||
'widgets',
|
||||
'pinnedUserLists',
|
||||
'recentlyUsedEmojis',
|
||||
'recentlyUsedUsers',
|
||||
'forceShowAds',
|
||||
'additionalUnicodeEmojiIndexes',
|
||||
'themeInitial',
|
||||
|
||||
// 光過敏性対策のためあえて鯖管に設定させない
|
||||
'animation',
|
||||
'animatedMfm',
|
||||
'disableShowingAnimatedImages'
|
||||
] satisfies (keyof typeof defaultStore.def)[];
|
||||
|
||||
const notConfigurableColdDeviceStorageSettings = [
|
||||
'darkTheme',
|
||||
'lightTheme',
|
||||
'plugins',
|
||||
] satisfies (keyof typeof ColdDeviceStorage.default)[];
|
||||
|
||||
type ClientSettingOverridesUIDefObj = {
|
||||
formType: 'text' | 'number' | 'boolean' | 'codeEditor';
|
||||
enableOverride: boolean;
|
||||
defaultValue: any;
|
||||
overrideValue?: any;
|
||||
}
|
||||
|
||||
const fetching = ref(true);
|
||||
const clientSettingOverrides = ref<Record<string, ClientSettingOverridesUIDefObj>>();
|
||||
|
||||
function getMkCodeProps(def: ClientSettingOverridesUIDefObj) {
|
||||
if (typeof def.defaultValue === 'string') {
|
||||
return {
|
||||
code: def.defaultValue,
|
||||
forceShow: true,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
code: JSON.stringify(def.defaultValue, null, 4),
|
||||
lang: 'json',
|
||||
forceShow: true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function typeSafeObjectEntries<T extends Record<string, any>>(obj: T) {
|
||||
return Object.entries(obj) as [keyof T, T[keyof T]][];
|
||||
}
|
||||
|
||||
function getClientSettingOverridesUIDefObj(def: unknown): ClientSettingOverridesUIDefObj {
|
||||
const _def = typeof def === 'object' ? JSON.stringify(def, null, '\t') : def;
|
||||
return {
|
||||
formType: (() => {
|
||||
if (typeof def === 'boolean') {
|
||||
return 'boolean';
|
||||
} else if (typeof def === 'number') {
|
||||
return 'number';
|
||||
} else if (typeof def === 'object') {
|
||||
return 'codeEditor';
|
||||
} else {
|
||||
return 'text';
|
||||
}
|
||||
})() satisfies ClientSettingOverridesUIDefObj['formType'] as ClientSettingOverridesUIDefObj['formType'],
|
||||
enableOverride: false,
|
||||
defaultValue: def,
|
||||
overrideValue: _def,
|
||||
};
|
||||
}
|
||||
|
||||
async function fetch() {
|
||||
fetching.value = true;
|
||||
const overrideDefs = Object.fromEntries([
|
||||
...typeSafeObjectEntries(defaultStore.def)
|
||||
.filter(([key, _]) => !(notConfigurableDefaultStoreSettings as string[]).includes(key))
|
||||
.map(([key, def]) => [`defaultStore::${key}`, getClientSettingOverridesUIDefObj(def.default)]),
|
||||
...typeSafeObjectEntries(ColdDeviceStorage.default)
|
||||
.filter(([key, _]) => !(notConfigurableColdDeviceStorageSettings as string[]).includes(key))
|
||||
.map(([key, def]) => [`ColdDeviceStorage::${key}`, getClientSettingOverridesUIDefObj(def)]),
|
||||
]);
|
||||
const res = await misskeyApi('admin/meta');
|
||||
if (res.defaultClientSettingOverrides != null) {
|
||||
try {
|
||||
const parsed = JSON.parse(res.defaultClientSettingOverrides);
|
||||
for (const key in parsed) {
|
||||
if (key in overrideDefs) {
|
||||
overrideDefs[key].enableOverride = true;
|
||||
overrideDefs[key].overrideValue = parsed[key];
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
clientSettingOverrides.value = overrideDefs;
|
||||
fetching.value = false;
|
||||
}
|
||||
|
||||
async function save() {
|
||||
if (clientSettingOverrides.value == null) return;
|
||||
|
||||
const overrides = Object.fromEntries(
|
||||
typeSafeObjectEntries(clientSettingOverrides.value)
|
||||
.filter(([key, def]) => (
|
||||
def.enableOverride &&
|
||||
def.overrideValue !== def.defaultValue && (
|
||||
(typeof def.defaultValue === 'string' && typeof def.overrideValue === 'string' && def.overrideValue !== def.defaultValue) ||
|
||||
(typeof def.defaultValue === 'object' && typeof def.overrideValue === 'string' && JSON.stringify(def.overrideValue) !== JSON.stringify(def.defaultValue)) ||
|
||||
(typeof def.defaultValue !== 'string' && typeof def.overrideValue === 'string' && def.overrideValue !== JSON.stringify(def.defaultValue))
|
||||
)
|
||||
))
|
||||
.map(([key, def]) => [key, typeof def.overrideValue === 'string' && typeof def.defaultValue !== 'string' ? JSON5.parse(def.overrideValue) : def.overrideValue])
|
||||
);
|
||||
|
||||
let defaultClientSettingOverrides: string | null = JSON.stringify(overrides);
|
||||
|
||||
if (Object.keys(overrides).length === 0) {
|
||||
defaultClientSettingOverrides = null;
|
||||
}
|
||||
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
defaultClientSettingOverrides,
|
||||
});
|
||||
|
||||
await fetch();
|
||||
pruneInstanceCache();
|
||||
await reloadAsk({ reason: i18n.ts.reloadToApplySetting });
|
||||
}
|
||||
|
||||
async function reset() {
|
||||
const { canceled } = await os.confirm({
|
||||
type: 'warning',
|
||||
text: i18n.ts.resetAreYouSure,
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
defaultClientSettingOverrides: null,
|
||||
});
|
||||
|
||||
await fetch();
|
||||
}
|
||||
|
||||
fetch();
|
||||
|
||||
const headerActions = computed(() => []);
|
||||
|
||||
const headerTabs = computed(() => []);
|
||||
|
||||
definePageMetadata(() => ({
|
||||
title: i18n.ts.clientSettingOverrides,
|
||||
icon: 'ti ti-checkbox',
|
||||
}));
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.warn {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.footer {
|
||||
backdrop-filter: var(--MI-blur, blur(15px));
|
||||
background: var(--MI_THEME-acrylicBg);
|
||||
border-top: solid .5px var(--MI_THEME-divider);
|
||||
}
|
||||
|
||||
.footerInner {
|
||||
max-width: 700px;
|
||||
margin: 0 auto;
|
||||
padding: 16px;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -249,6 +249,8 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<MkButton primary @click="chooseProxyAccount">{{ i18n.ts.selectAccount }}</MkButton>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
<FormLink to="/admin/client-setting-overrides">{{ i18n.ts.clientSettingOverrides }} <span class="_beta">{{ i18n.ts.beta }}</span></FormLink>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
|
|
@ -274,6 +276,7 @@ import MkKeyValue from '@/components/MkKeyValue.vue';
|
|||
import { useForm } from '@/scripts/use-form.js';
|
||||
import MkFormFooter from '@/components/MkFormFooter.vue';
|
||||
import MkRadios from '@/components/MkRadios.vue';
|
||||
import FormLink from '@/components/form/link.vue';
|
||||
|
||||
const meta = await misskeyApi('admin/meta');
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { get, set } from '@/scripts/idb-proxy.js';
|
|||
import { defaultStore } from '@/store.js';
|
||||
import { useStream } from '@/stream.js';
|
||||
import { deepClone } from '@/scripts/clone.js';
|
||||
import { deepMerge } from '@/scripts/merge.js';
|
||||
import { deepMerge, type DeepPartial } from '@/scripts/merge.js';
|
||||
|
||||
type StateDef = Record<string, {
|
||||
where: 'account' | 'device' | 'deviceAccount';
|
||||
|
|
@ -44,6 +44,7 @@ export class Storage<T extends StateDef> {
|
|||
public readonly def: T;
|
||||
|
||||
// TODO: これが実装されたらreadonlyにしたい: https://github.com/microsoft/TypeScript/issues/37487
|
||||
private readonly defaultState: State<T>;
|
||||
public readonly state: State<T>;
|
||||
public readonly reactiveState: ReactiveState<T>;
|
||||
|
||||
|
|
@ -60,7 +61,7 @@ export class Storage<T extends StateDef> {
|
|||
return promise;
|
||||
}
|
||||
|
||||
constructor(key: string, def: T) {
|
||||
constructor(key: string, def: T, defaultOverrides?: DeepPartial<State<T>>) {
|
||||
this.key = key;
|
||||
this.deviceStateKeyName = `pizzax::${key}`;
|
||||
this.deviceAccountStateKeyName = $i ? `pizzax::${key}::${$i.id}` : '';
|
||||
|
|
@ -69,25 +70,43 @@ export class Storage<T extends StateDef> {
|
|||
|
||||
this.pizzaxChannel = new BroadcastChannel(`pizzax::${key}`);
|
||||
|
||||
this.defaultState = {} as State<T>;
|
||||
this.state = {} as State<T>;
|
||||
this.reactiveState = {} as ReactiveState<T>;
|
||||
|
||||
for (const [k, v] of Object.entries(def) as [keyof T, T[keyof T]['default']][]) {
|
||||
this.state[k] = v.default;
|
||||
this.reactiveState[k] = ref(v.default);
|
||||
let _defaultState = v.default;
|
||||
if (
|
||||
defaultOverrides != null &&
|
||||
this.isPureObject(defaultOverrides) &&
|
||||
defaultOverrides[k] !== undefined // ←意図的にnullになっている可能性があるためundefined判定
|
||||
) {
|
||||
if (this.isPureObject(defaultOverrides[k]) && this.isPureObject(v.default)) {
|
||||
_defaultState = deepMerge(defaultOverrides[k], v.default);
|
||||
} else if (Array.isArray(defaultOverrides[k]) && Array.isArray(v.default)) {
|
||||
_defaultState = Array.from(new Set([...defaultOverrides[k], ...v.default]));
|
||||
} else {
|
||||
_defaultState = defaultOverrides[k];
|
||||
}
|
||||
if (_DEV_) console.log('defaultState', k, _defaultState);
|
||||
}
|
||||
|
||||
this.defaultState[k] = _defaultState;
|
||||
this.state[k] = _defaultState;
|
||||
this.reactiveState[k] = ref(_defaultState);
|
||||
}
|
||||
|
||||
this.ready = this.init();
|
||||
this.loaded = this.ready.then(() => this.load());
|
||||
}
|
||||
|
||||
private isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
|
||||
private isPureObject(value: unknown): value is Record<PropertyKey, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
private mergeState<X>(value: X, def: X): X {
|
||||
if (this.isPureObject(value) && this.isPureObject(def)) {
|
||||
const merged = deepMerge(value, def);
|
||||
const merged = deepMerge(value as DeepPartial<X>, def);
|
||||
|
||||
if (_DEV_) console.log('Merging state. Incoming: ', value, ' Default: ', def, ' Result: ', merged);
|
||||
|
||||
|
|
@ -105,14 +124,14 @@ export class Storage<T extends StateDef> {
|
|||
|
||||
for (const [k, v] of Object.entries(this.def) as [keyof T, T[keyof T]['default']][]) {
|
||||
if (v.where === 'device' && Object.prototype.hasOwnProperty.call(deviceState, k)) {
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], v.default);
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceState[k], this.defaultState[k]);
|
||||
} else if (v.where === 'account' && $i && Object.prototype.hasOwnProperty.call(registryCache, k)) {
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], v.default);
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(registryCache[k], this.defaultState[k]);
|
||||
} else if (v.where === 'deviceAccount' && Object.prototype.hasOwnProperty.call(deviceAccountState, k)) {
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], v.default);
|
||||
this.reactiveState[k].value = this.state[k] = this.mergeState<T[keyof T]['default']>(deviceAccountState[k], this.defaultState[k]);
|
||||
} else {
|
||||
this.reactiveState[k].value = this.state[k] = v.default;
|
||||
if (_DEV_) console.log('Use default value', k, v.default);
|
||||
this.reactiveState[k].value = this.state[k] = this.defaultState[k];
|
||||
if (_DEV_) console.log('Use default value', k, this.defaultState[k]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -486,6 +486,10 @@ const routes: RouteDef[] = [{
|
|||
path: '/system-webhook',
|
||||
name: 'system-webhook',
|
||||
component: page(() => import('@/pages/admin/system-webhook.vue')),
|
||||
}, {
|
||||
path: '/client-setting-overrides',
|
||||
name: 'client-setting-overrides',
|
||||
component: page(() => import('@/pages/admin/client-setting-overrides.vue')),
|
||||
}, {
|
||||
path: '/',
|
||||
component: page(() => import('@/pages/_empty_.vue')),
|
||||
|
|
|
|||
|
|
@ -7,10 +7,10 @@ import { deepClone } from './clone.js';
|
|||
import type { Cloneable } from './clone.js';
|
||||
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends Record<string | number | symbol, unknown> ? DeepPartial<T[P]> : T[P];
|
||||
[P in keyof T]?: T[P] extends Record<PropertyKey, unknown> ? DeepPartial<T[P]> : T[P];
|
||||
};
|
||||
|
||||
function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> {
|
||||
function isPureObject(value: unknown): value is Record<PropertyKey, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
||||
}
|
||||
|
||||
|
|
@ -18,14 +18,14 @@ function isPureObject(value: unknown): value is Record<string | number | symbol,
|
|||
* valueにないキーをdefからもらう(再帰的)\
|
||||
* nullはそのまま、undefinedはdefの値
|
||||
**/
|
||||
export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: DeepPartial<X>, def: X): X {
|
||||
export function deepMerge<X extends Record<PropertyKey, unknown>>(value: DeepPartial<X>, def: X): X {
|
||||
if (isPureObject(value) && isPureObject(def)) {
|
||||
const result = deepClone(value as Cloneable) as X;
|
||||
for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) {
|
||||
if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) {
|
||||
result[k] = v;
|
||||
} else if (isPureObject(v) && isPureObject(result[k])) {
|
||||
const child = deepClone(result[k] as Cloneable) as DeepPartial<X[keyof X] & Record<string | number | symbol, unknown>>;
|
||||
const child = deepClone(result[k] as Cloneable) as DeepPartial<X[keyof X] & Record<PropertyKey, unknown>>;
|
||||
result[k] = deepMerge<typeof v>(child, v);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ let isReloadConfirming = false;
|
|||
export async function reloadAsk(opts: {
|
||||
unison?: boolean;
|
||||
reason?: string;
|
||||
}) {
|
||||
} = {}) {
|
||||
if (isReloadConfirming) {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
33
packages/frontend/src/scripts/store-overrides.ts
Normal file
33
packages/frontend/src/scripts/store-overrides.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { initInstance, instance } from '@/instance.js';
|
||||
|
||||
export async function getDefaultStoreOverrides() {
|
||||
await initInstance();
|
||||
if (instance.defaultClientSettingOverrides != null) {
|
||||
try {
|
||||
const clientSettingOverrides = JSON.parse(instance.defaultClientSettingOverrides);
|
||||
const out = Object.fromEntries(Object.keys(clientSettingOverrides).filter(key => key.startsWith('defaultStore::')).map(key => [key.split('::')[1], clientSettingOverrides[key]]));
|
||||
return out;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getColdDeviceStorageOverrides() {
|
||||
if (instance.defaultClientSettingOverrides != null) {
|
||||
try {
|
||||
const clientSettingOverrides = JSON.parse(instance.defaultClientSettingOverrides);
|
||||
const out = Object.fromEntries(Object.keys(clientSettingOverrides).filter(key => key.startsWith('ColdDeviceStorage::')).map(key => [key.split('::')[1], clientSettingOverrides[key]]));
|
||||
return out;
|
||||
} catch (err) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import { miLocalStorage } from './local-storage.js';
|
|||
import type { SoundType } from '@/scripts/sound.js';
|
||||
import { Storage } from '@/pizzax.js';
|
||||
import type { Ast } from '@syuilo/aiscript';
|
||||
import { getColdDeviceStorageOverrides, getDefaultStoreOverrides } from '@/scripts/store-overrides.js';
|
||||
|
||||
interface PostFormAction {
|
||||
title: string,
|
||||
|
|
@ -502,7 +503,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'device',
|
||||
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
|
||||
},
|
||||
}));
|
||||
}, await getDefaultStoreOverrides() ?? undefined));
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
|
|
@ -548,7 +549,8 @@ export class ColdDeviceStorage {
|
|||
// (indexedDBはnullを保存できるため、ユーザーが意図してnullを格納した可能性がある)
|
||||
const value = miLocalStorage.getItem(`${PREFIX}${key}`);
|
||||
if (value == null) {
|
||||
return ColdDeviceStorage.default[key];
|
||||
const override = getColdDeviceStorageOverrides();
|
||||
return override != null ? override[key] ?? ColdDeviceStorage.default[key] : ColdDeviceStorage.default[key];
|
||||
} else {
|
||||
return JSON.parse(value);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue