misskey/packages/frontend/src/pages/admin/client-setting-overrides.vue

270 lines
8.2 KiB
Vue

<!--
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>
<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';
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 {
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' ? JSON.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>