B5 frontend: public-page settings tab — theme store UI, chunked upload install, config editor; 28 i18n keys x13 locales
This commit is contained in:
@@ -127,6 +127,10 @@ async function performFetch(
|
||||
if (data instanceof FormData) {
|
||||
body = data;
|
||||
headers.delete('Content-Type');
|
||||
} else if (data instanceof Blob || data instanceof ArrayBuffer) {
|
||||
// Raw payload (e.g. one chunk of a theme upload): sent as-is.
|
||||
body = data as BodyInit;
|
||||
if (!headers.has('Content-Type')) headers.set('Content-Type', 'application/octet-stream');
|
||||
} else if (!SAFE_METHODS.has(upper)) {
|
||||
const declaredType = (headers.get('Content-Type') || '').toLowerCase();
|
||||
if (declaredType.startsWith('application/json')) {
|
||||
|
||||
@@ -251,6 +251,11 @@ export default function AppSidebar() {
|
||||
icon: <CloudServerOutlined />,
|
||||
label: t('pages.settings.subSettings'),
|
||||
},
|
||||
{
|
||||
key: '/settings#public-page',
|
||||
icon: <GlobalOutlined />,
|
||||
label: t('pages.settings.probeThemes.title'),
|
||||
},
|
||||
];
|
||||
if (showSubFormats) {
|
||||
children.push({
|
||||
|
||||
@@ -0,0 +1,590 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Input,
|
||||
InputNumber,
|
||||
Modal,
|
||||
Popconfirm,
|
||||
Row,
|
||||
Select,
|
||||
Space,
|
||||
Switch,
|
||||
Tag,
|
||||
Typography,
|
||||
message,
|
||||
} from 'antd';
|
||||
import { DeleteOutlined, SettingOutlined, UploadOutlined } from '@ant-design/icons';
|
||||
import { HttpUtil } from '@/utils';
|
||||
|
||||
// Item 7 B5: the public status page's settings + theme store. Like
|
||||
// the probe-meta section this surface is self-contained — it loads
|
||||
// and saves through its own endpoints and every change takes effect
|
||||
// immediately (no panel save, no restart). Theme packages are static
|
||||
// files served by the panel; installing one never runs its code here.
|
||||
interface ThemeConfigField {
|
||||
key: string;
|
||||
type: string;
|
||||
label: string;
|
||||
default?: unknown;
|
||||
help?: string;
|
||||
options?: { value: string; label: string }[];
|
||||
}
|
||||
|
||||
interface ThemeWire {
|
||||
name: string;
|
||||
short: string;
|
||||
description: string;
|
||||
version: string;
|
||||
author: string;
|
||||
url: string;
|
||||
config?: ThemeConfigField[];
|
||||
selected: boolean;
|
||||
builtin: boolean;
|
||||
preview: boolean;
|
||||
}
|
||||
|
||||
interface ProbeSettingsWire {
|
||||
publicPage: boolean;
|
||||
siteName: string;
|
||||
site: string;
|
||||
githubProxy: string;
|
||||
}
|
||||
|
||||
interface ThemesPayload {
|
||||
themes: ThemeWire[];
|
||||
settings: ProbeSettingsWire;
|
||||
favicon: string;
|
||||
touchIcon: string;
|
||||
}
|
||||
|
||||
const JSON_HEADERS = { headers: { 'Content-Type': 'application/json' } };
|
||||
const UPLOAD_CHUNK = 1 << 20;
|
||||
const MAX_ICON_BYTES = 20 * 1024;
|
||||
|
||||
function basePathPrefix(): string {
|
||||
const bp = window.X_UI_BASE_PATH;
|
||||
return typeof bp === 'string' && bp !== '/' ? bp.replace(/\/$/, '') : '';
|
||||
}
|
||||
|
||||
function readAsDataURL(blob: Blob): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => resolve(String(reader.result));
|
||||
reader.onerror = () => reject(reader.error);
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
}
|
||||
|
||||
// fileToIconDataURL fits a chosen image into the hub's 20 KiB site
|
||||
// icon budget: small files pass through untouched (the server judges
|
||||
// the bytes, not the declared type); a large raster is scaled down
|
||||
// until it fits; a large SVG cannot be scaled and is refused here.
|
||||
async function fileToIconDataURL(file: File): Promise<string> {
|
||||
if (file.size <= MAX_ICON_BYTES) return readAsDataURL(file);
|
||||
if (file.type === 'image/svg+xml' || file.type === 'image/x-icon' || file.type === 'image/gif') {
|
||||
throw new Error('tooLarge');
|
||||
}
|
||||
const bitmap = await createImageBitmap(file);
|
||||
try {
|
||||
for (const size of [256, 180, 128, 96, 64]) {
|
||||
const scale = Math.min(1, size / Math.max(bitmap.width, bitmap.height));
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = Math.max(1, Math.round(bitmap.width * scale));
|
||||
canvas.height = Math.max(1, Math.round(bitmap.height * scale));
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) break;
|
||||
ctx.drawImage(bitmap, 0, 0, canvas.width, canvas.height);
|
||||
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/png'));
|
||||
if (blob && blob.size <= MAX_ICON_BYTES) return readAsDataURL(blob);
|
||||
}
|
||||
} finally {
|
||||
bitmap.close();
|
||||
}
|
||||
throw new Error('tooLarge');
|
||||
}
|
||||
|
||||
export default function PublicPageTab() {
|
||||
const { t } = useTranslation();
|
||||
const [messageApi, messageContextHolder] = message.useMessage();
|
||||
const [themes, setThemes] = useState<ThemeWire[]>([]);
|
||||
const [settings, setSettings] = useState<ProbeSettingsWire>({
|
||||
publicPage: false,
|
||||
siteName: 'Monitor',
|
||||
site: '',
|
||||
githubProxy: '',
|
||||
});
|
||||
const [favicon, setFavicon] = useState('');
|
||||
const [touchIcon, setTouchIcon] = useState('');
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [installURL, setInstallURL] = useState('');
|
||||
const [installing, setInstalling] = useState(false);
|
||||
const [uploadPct, setUploadPct] = useState<number | null>(null);
|
||||
const [busyShort, setBusyShort] = useState('');
|
||||
const fileRef = useRef<HTMLInputElement>(null);
|
||||
const faviconRef = useRef<HTMLInputElement>(null);
|
||||
const touchRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const applyPayload = useCallback((payload: ThemesPayload) => {
|
||||
setThemes(payload.themes ?? []);
|
||||
setSettings(payload.settings);
|
||||
setFavicon(payload.favicon ?? '');
|
||||
setTouchIcon(payload.touchIcon ?? '');
|
||||
}, []);
|
||||
|
||||
const reload = useCallback(async () => {
|
||||
const msg = await HttpUtil.get<ThemesPayload>('/panel/api/probe/themes', { silent: true });
|
||||
if (msg?.success && msg.obj) applyPayload(msg.obj);
|
||||
}, [applyPayload]);
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
HttpUtil.get<ThemesPayload>('/panel/api/probe/themes', { silent: true }).then((msg) => {
|
||||
if (!cancelled && msg?.success && msg.obj) applyPayload(msg.obj);
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [applyPayload]);
|
||||
|
||||
async function saveSettings() {
|
||||
setSaving(true);
|
||||
try {
|
||||
const msg = await HttpUtil.put(
|
||||
'/panel/api/probe/settings',
|
||||
{ ...settings, favicon, touchIcon },
|
||||
{ silent: true, ...JSON_HEADERS },
|
||||
);
|
||||
if (msg?.success) messageApi.success(t('pages.settings.probeThemes.saved'));
|
||||
else messageApi.error(msg?.msg || t('pages.settings.probeThemes.saveFailed'));
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function pickIcon(which: 'favicon' | 'touchIcon', file: File | undefined) {
|
||||
if (!file) return;
|
||||
try {
|
||||
const url = await fileToIconDataURL(file);
|
||||
if (which === 'favicon') setFavicon(url);
|
||||
else setTouchIcon(url);
|
||||
} catch {
|
||||
messageApi.error(t('pages.settings.probeThemes.iconTooLarge'));
|
||||
}
|
||||
}
|
||||
|
||||
async function uploadTheme(file: File | undefined) {
|
||||
if (!file) return;
|
||||
if (file.size === 0) {
|
||||
messageApi.error(t('pages.settings.probeThemes.uploadFailed'));
|
||||
return;
|
||||
}
|
||||
setUploadPct(0);
|
||||
try {
|
||||
let offset = 0;
|
||||
while (offset < file.size) {
|
||||
const slice = file.slice(offset, offset + UPLOAD_CHUNK);
|
||||
const msg = await HttpUtil.post<{ received?: number; theme?: ThemeWire }>(
|
||||
'/panel/api/probe/theme-upload',
|
||||
slice,
|
||||
{
|
||||
silent: true,
|
||||
params: { offset, total: file.size },
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
},
|
||||
);
|
||||
if (!msg?.success) {
|
||||
messageApi.error(msg?.msg || t('pages.settings.probeThemes.uploadFailed'));
|
||||
return;
|
||||
}
|
||||
if (msg.obj?.theme) {
|
||||
messageApi.success(
|
||||
t('pages.settings.probeThemes.installed', { name: msg.obj.theme.name }),
|
||||
);
|
||||
await reload();
|
||||
return;
|
||||
}
|
||||
offset = msg.obj?.received ?? offset + slice.size;
|
||||
setUploadPct(Math.round((offset / file.size) * 100));
|
||||
}
|
||||
} finally {
|
||||
setUploadPct(null);
|
||||
if (fileRef.current) fileRef.current.value = '';
|
||||
}
|
||||
}
|
||||
|
||||
async function installByURL() {
|
||||
setInstalling(true);
|
||||
try {
|
||||
const msg = await HttpUtil.post<{ theme: ThemeWire }>(
|
||||
'/panel/api/probe/theme-install',
|
||||
{ url: installURL },
|
||||
{ silent: true, ...JSON_HEADERS },
|
||||
);
|
||||
if (msg?.success && msg.obj?.theme) {
|
||||
messageApi.success(t('pages.settings.probeThemes.installed', { name: msg.obj.theme.name }));
|
||||
setInstallURL('');
|
||||
await reload();
|
||||
} else {
|
||||
messageApi.error(msg?.msg || t('pages.settings.probeThemes.installFailed'));
|
||||
}
|
||||
} finally {
|
||||
setInstalling(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function themeAction(short: string, action: 'select' | 'update' | 'delete') {
|
||||
setBusyShort(short);
|
||||
try {
|
||||
if (action === 'delete') {
|
||||
const msg = await HttpUtil.delete(`/panel/api/probe/themes/${short}`, { silent: true });
|
||||
if (!msg?.success) {
|
||||
messageApi.error(msg?.msg || t('pages.settings.probeThemes.saveFailed'));
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
const msg = await HttpUtil.post<{ updated?: boolean; version?: string }>(
|
||||
`/panel/api/probe/themes/${short}/${action}`,
|
||||
undefined,
|
||||
{ silent: true },
|
||||
);
|
||||
if (!msg?.success) {
|
||||
messageApi.error(msg?.msg || t('pages.settings.probeThemes.saveFailed'));
|
||||
return;
|
||||
}
|
||||
if (action === 'update') {
|
||||
if (msg.obj?.updated) {
|
||||
messageApi.success(
|
||||
t('pages.settings.probeThemes.updatedTo', { version: msg.obj.version }),
|
||||
);
|
||||
} else {
|
||||
messageApi.success(t('pages.settings.probeThemes.alreadyLatest'));
|
||||
}
|
||||
}
|
||||
}
|
||||
await reload();
|
||||
} finally {
|
||||
setBusyShort('');
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{messageContextHolder}
|
||||
<Typography.Title level={5}>{t('pages.settings.probeThemes.settingsTitle')}</Typography.Title>
|
||||
<Row gutter={[16, 12]}>
|
||||
<Col xs={24} md={12}>
|
||||
<div style={{ marginBottom: 4 }}>{t('pages.settings.probeThemes.publicPage')}</div>
|
||||
<Switch
|
||||
checked={settings.publicPage}
|
||||
onChange={(v) => setSettings({ ...settings, publicPage: v })}
|
||||
/>
|
||||
<Typography.Text type="secondary" style={{ marginLeft: 8 }}>
|
||||
{t('pages.settings.probeThemes.publicPageHint')}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<div style={{ marginBottom: 4 }}>{t('pages.settings.probeThemes.siteName')}</div>
|
||||
<Input
|
||||
value={settings.siteName}
|
||||
onChange={(e) => setSettings({ ...settings, siteName: e.target.value })}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<div style={{ marginBottom: 4 }}>{t('pages.settings.probeThemes.site')}</div>
|
||||
<Input
|
||||
value={settings.site}
|
||||
placeholder="https://"
|
||||
onChange={(e) => setSettings({ ...settings, site: e.target.value })}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<div style={{ marginBottom: 4 }}>{t('pages.settings.probeThemes.githubProxy')}</div>
|
||||
<Input
|
||||
value={settings.githubProxy}
|
||||
placeholder="https://"
|
||||
onChange={(e) => setSettings({ ...settings, githubProxy: e.target.value })}
|
||||
/>
|
||||
<Typography.Text type="secondary">
|
||||
{t('pages.settings.probeThemes.githubProxyHint')}
|
||||
</Typography.Text>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<div style={{ marginBottom: 4 }}>{t('pages.settings.probeThemes.favicon')}</div>
|
||||
<Space>
|
||||
{favicon ? <img src={favicon} alt="" style={{ width: 24, height: 24 }} /> : null}
|
||||
<Button size="small" onClick={() => faviconRef.current?.click()}>
|
||||
{t('pages.settings.probeThemes.chooseImage')}
|
||||
</Button>
|
||||
{favicon ? (
|
||||
<Button size="small" onClick={() => setFavicon('')}>
|
||||
{t('clear')}
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
<input
|
||||
ref={faviconRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
hidden
|
||||
onChange={(e) => pickIcon('favicon', e.target.files?.[0])}
|
||||
/>
|
||||
</Col>
|
||||
<Col xs={24} md={12}>
|
||||
<div style={{ marginBottom: 4 }}>{t('pages.settings.probeThemes.touchIcon')}</div>
|
||||
<Space>
|
||||
{touchIcon ? <img src={touchIcon} alt="" style={{ width: 24, height: 24 }} /> : null}
|
||||
<Button size="small" onClick={() => touchRef.current?.click()}>
|
||||
{t('pages.settings.probeThemes.chooseImage')}
|
||||
</Button>
|
||||
{touchIcon ? (
|
||||
<Button size="small" onClick={() => setTouchIcon('')}>
|
||||
{t('clear')}
|
||||
</Button>
|
||||
) : null}
|
||||
</Space>
|
||||
<input
|
||||
ref={touchRef}
|
||||
type="file"
|
||||
accept="image/*"
|
||||
hidden
|
||||
onChange={(e) => pickIcon('touchIcon', e.target.files?.[0])}
|
||||
/>
|
||||
<div>
|
||||
<Typography.Text type="secondary">
|
||||
{t('pages.settings.probeThemes.iconHint')}
|
||||
</Typography.Text>
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Button type="primary" loading={saving} onClick={saveSettings}>
|
||||
{t('save')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<Typography.Title level={5} style={{ marginTop: 28 }}>
|
||||
{t('pages.settings.probeThemes.title')}
|
||||
</Typography.Title>
|
||||
<Space wrap style={{ marginBottom: 16 }}>
|
||||
<Button icon={<UploadOutlined />} onClick={() => fileRef.current?.click()}>
|
||||
{uploadPct === null
|
||||
? t('pages.settings.probeThemes.uploadInstall')
|
||||
: `${t('pages.settings.probeThemes.uploadInstall')} ${uploadPct}%`}
|
||||
</Button>
|
||||
<input
|
||||
ref={fileRef}
|
||||
type="file"
|
||||
accept=".gz,application/gzip"
|
||||
hidden
|
||||
onChange={(e) => uploadTheme(e.target.files?.[0])}
|
||||
/>
|
||||
<Input
|
||||
style={{ width: 340 }}
|
||||
value={installURL}
|
||||
placeholder={t('pages.settings.probeThemes.installByUrlPlaceholder')}
|
||||
onChange={(e) => setInstallURL(e.target.value)}
|
||||
/>
|
||||
<Button loading={installing} disabled={!installURL.trim()} onClick={installByURL}>
|
||||
{t('pages.settings.probeThemes.installByUrl')}
|
||||
</Button>
|
||||
</Space>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{themes.map((theme) => (
|
||||
<Col key={theme.short} xs={24} sm={12} lg={8}>
|
||||
<Card
|
||||
hoverable
|
||||
cover={
|
||||
theme.preview ? (
|
||||
<img
|
||||
alt={theme.name}
|
||||
src={`${basePathPrefix()}/panel/api/probe/themes/${theme.short}/preview`}
|
||||
style={{ aspectRatio: '16/9', objectFit: 'cover' }}
|
||||
/>
|
||||
) : undefined
|
||||
}
|
||||
actions={[
|
||||
theme.selected ? (
|
||||
<Tag key="on" color="green">
|
||||
{t('pages.settings.probeThemes.inUse')}
|
||||
</Tag>
|
||||
) : (
|
||||
<Button
|
||||
key="use"
|
||||
type="link"
|
||||
loading={busyShort === theme.short}
|
||||
onClick={() => themeAction(theme.short, 'select')}
|
||||
>
|
||||
{t('pages.settings.probeThemes.use')}
|
||||
</Button>
|
||||
),
|
||||
<Button
|
||||
key="update"
|
||||
type="link"
|
||||
loading={busyShort === theme.short}
|
||||
onClick={() => themeAction(theme.short, 'update')}
|
||||
>
|
||||
{t('update')}
|
||||
</Button>,
|
||||
...(theme.config && theme.config.length > 0
|
||||
? [
|
||||
<ThemeConfigButton
|
||||
key="config"
|
||||
theme={theme}
|
||||
onSaved={() => messageApi.success(t('pages.settings.probeThemes.saved'))}
|
||||
onError={(m) => messageApi.error(m)}
|
||||
/>,
|
||||
]
|
||||
: []),
|
||||
...(!theme.builtin
|
||||
? [
|
||||
<Popconfirm
|
||||
key="del"
|
||||
title={t('pages.settings.probeThemes.deleteConfirm')}
|
||||
onConfirm={() => themeAction(theme.short, 'delete')}
|
||||
>
|
||||
<Button type="link" danger icon={<DeleteOutlined />}>
|
||||
{t('delete')}
|
||||
</Button>
|
||||
</Popconfirm>,
|
||||
]
|
||||
: []),
|
||||
]}
|
||||
>
|
||||
<Card.Meta
|
||||
title={
|
||||
<Space>
|
||||
{theme.name}
|
||||
{theme.builtin ? <Tag>{t('pages.settings.probeThemes.builtin')}</Tag> : null}
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<div>
|
||||
<div>{theme.description}</div>
|
||||
<div>
|
||||
v{theme.version} · {theme.author}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ThemeConfigButton edits one theme's saved config. The form is
|
||||
// drawn from the theme.json config declaration (passed through
|
||||
// unparsed by the server); values load from and save to the contract
|
||||
// endpoint the themes themselves use, and only fields changed from
|
||||
// their declared defaults are stored (the hub's semantics).
|
||||
function ThemeConfigButton({
|
||||
theme,
|
||||
onSaved,
|
||||
onError,
|
||||
}: {
|
||||
theme: ThemeWire;
|
||||
onSaved: () => void;
|
||||
onError: (msg: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const [values, setValues] = useState<Record<string, unknown>>({});
|
||||
const [saving, setSaving] = useState(false);
|
||||
const fields = theme.config ?? [];
|
||||
|
||||
async function load() {
|
||||
let saved: Record<string, unknown> = {};
|
||||
try {
|
||||
const res = await fetch(`/api/themes/${theme.short}/config`, { credentials: 'same-origin' });
|
||||
if (res.ok) saved = (await res.json()) as Record<string, unknown>;
|
||||
} catch {
|
||||
/* an unreadable config edits from the defaults */
|
||||
}
|
||||
const merged: Record<string, unknown> = {};
|
||||
for (const f of fields) merged[f.key] = saved[f.key] ?? f.default;
|
||||
setValues(merged);
|
||||
setOpen(true);
|
||||
}
|
||||
|
||||
async function save() {
|
||||
setSaving(true);
|
||||
try {
|
||||
const changed: Record<string, unknown> = {};
|
||||
for (const f of fields) {
|
||||
if (JSON.stringify(values[f.key]) !== JSON.stringify(f.default)) {
|
||||
changed[f.key] = values[f.key];
|
||||
}
|
||||
}
|
||||
const res = await fetch(`/api/themes/${theme.short}/config`, {
|
||||
method: 'PUT',
|
||||
credentials: 'same-origin',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(changed),
|
||||
});
|
||||
if (res.ok) {
|
||||
setOpen(false);
|
||||
onSaved();
|
||||
} else {
|
||||
onError((await res.text()) || t('pages.settings.probeThemes.saveFailed'));
|
||||
}
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button type="link" icon={<SettingOutlined />} onClick={load}>
|
||||
{t('pages.settings.probeThemes.config')}
|
||||
</Button>
|
||||
<Modal
|
||||
open={open}
|
||||
title={`${theme.name} · ${t('pages.settings.probeThemes.config')}`}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={save}
|
||||
confirmLoading={saving}
|
||||
>
|
||||
{fields.map((f) => (
|
||||
<div key={f.key} style={{ marginBottom: 12 }}>
|
||||
<div style={{ marginBottom: 4 }}>{f.label}</div>
|
||||
{f.type === 'boolean' ? (
|
||||
<Switch
|
||||
checked={Boolean(values[f.key])}
|
||||
onChange={(v) => setValues({ ...values, [f.key]: v })}
|
||||
/>
|
||||
) : f.type === 'number' ? (
|
||||
<InputNumber
|
||||
value={typeof values[f.key] === 'number' ? (values[f.key] as number) : undefined}
|
||||
onChange={(v) => setValues({ ...values, [f.key]: v })}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
) : f.type === 'select' && f.options ? (
|
||||
<Select
|
||||
value={values[f.key] as string}
|
||||
options={f.options}
|
||||
onChange={(v) => setValues({ ...values, [f.key]: v })}
|
||||
style={{ width: '100%' }}
|
||||
/>
|
||||
) : (
|
||||
<Input
|
||||
value={typeof values[f.key] === 'string' ? (values[f.key] as string) : ''}
|
||||
onChange={(e) => setValues({ ...values, [f.key]: e.target.value })}
|
||||
/>
|
||||
)}
|
||||
{f.help ? (
|
||||
<div>
|
||||
<Typography.Text type="secondary">{f.help}</Typography.Text>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import SubscriptionGeneralTab from './SubscriptionGeneralTab';
|
||||
import SubscriptionFormatsTab from './SubscriptionFormatsTab';
|
||||
import SubscriptionBalancersTab from './SubscriptionBalancersTab';
|
||||
import TelegramTab from './TelegramTab';
|
||||
import PublicPageTab from './PublicPageTab';
|
||||
import './SettingsPage.css';
|
||||
|
||||
interface ApiMsg {
|
||||
@@ -42,6 +43,7 @@ const tabSlugs = [
|
||||
'subscription',
|
||||
'subscription-formats',
|
||||
'subscription-balancers',
|
||||
'public-page',
|
||||
];
|
||||
|
||||
function isIp(h: string): boolean {
|
||||
@@ -205,6 +207,8 @@ export default function SettingsPage() {
|
||||
return <SubscriptionFormatsTab allSetting={allSetting} updateSetting={updateSetting} />;
|
||||
case 'subscription-balancers':
|
||||
return <SubscriptionBalancersTab allSetting={allSetting} updateSetting={updateSetting} />;
|
||||
case 'public-page':
|
||||
return <PublicPageTab />;
|
||||
default:
|
||||
return <GeneralTab allSetting={allSetting} updateSetting={updateSetting} />;
|
||||
}
|
||||
|
||||
@@ -989,6 +989,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "افتراضي",
|
||||
"title": "إعدادات البانل",
|
||||
"save": "حفظ",
|
||||
|
||||
@@ -1129,6 +1129,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "Default",
|
||||
"title": "Panel Settings",
|
||||
"save": "Save",
|
||||
|
||||
@@ -989,6 +989,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "Predeterminado",
|
||||
"title": "Configuraciones",
|
||||
"save": "Guardar",
|
||||
|
||||
@@ -989,6 +989,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "پیشفرض",
|
||||
"title": "تنظیمات پنل",
|
||||
"save": "ذخیره",
|
||||
|
||||
@@ -989,6 +989,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "Bawaan",
|
||||
"title": "Pengaturan Panel",
|
||||
"save": "Simpan",
|
||||
|
||||
@@ -989,6 +989,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "デフォルト",
|
||||
"title": "パネル設定",
|
||||
"save": "保存",
|
||||
|
||||
@@ -989,6 +989,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "Padrão",
|
||||
"title": "Configurações do Painel",
|
||||
"save": "Salvar",
|
||||
|
||||
@@ -989,6 +989,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "По умолчанию",
|
||||
"title": "Настройки",
|
||||
"save": "Сохранить",
|
||||
|
||||
@@ -989,6 +989,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "Varsayılan",
|
||||
"title": "Panel Ayarları",
|
||||
"save": "Kaydet",
|
||||
|
||||
@@ -989,6 +989,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "Типово",
|
||||
"title": "Параметри панелі",
|
||||
"save": "Зберегти",
|
||||
|
||||
@@ -989,6 +989,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "Mặc định",
|
||||
"title": "Cài đặt",
|
||||
"save": "Lưu",
|
||||
|
||||
@@ -1011,6 +1011,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "主题商店",
|
||||
"settingsTitle": "公开状态页",
|
||||
"publicPage": "公开状态页",
|
||||
"publicPageHint": "关闭时,状态页及其接口只对已登录的面板开放。",
|
||||
"siteName": "站点名",
|
||||
"site": "站点地址",
|
||||
"githubProxy": "GitHub 代理",
|
||||
"githubProxyHint": "从 GitHub 下载主题包时用的前缀;留空直连。",
|
||||
"favicon": "站点图标(favicon)",
|
||||
"touchIcon": "触摸图标(apple-touch-icon)",
|
||||
"chooseImage": "选择图片",
|
||||
"iconHint": "支持 PNG、ICO、SVG、WebP、JPEG、GIF,不超过 20 KiB;大图会自动缩小。图标跨主题统一,切换主题不变。",
|
||||
"iconTooLarge": "这张图压不到 20 KiB 以内",
|
||||
"saved": "已保存",
|
||||
"saveFailed": "操作失败",
|
||||
"uploadInstall": "上传 theme.tar.gz",
|
||||
"installed": "主题「{{name}}」已安装",
|
||||
"uploadFailed": "上传失败",
|
||||
"installByUrl": "按地址安装",
|
||||
"installByUrlPlaceholder": "主题的 GitHub 仓库地址,如 https://github.com/作者/仓库",
|
||||
"installFailed": "安装失败",
|
||||
"inUse": "使用中",
|
||||
"use": "启用",
|
||||
"builtin": "内置",
|
||||
"deleteConfirm": "删除这个主题?",
|
||||
"updatedTo": "已更新到 {{version}}",
|
||||
"alreadyLatest": "已是最新版本",
|
||||
"config": "主题设置"
|
||||
},
|
||||
"defaultTag": "默认",
|
||||
"title": "面板设置",
|
||||
"save": "保存",
|
||||
|
||||
@@ -989,6 +989,36 @@
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"probeThemes": {
|
||||
"title": "Theme store",
|
||||
"settingsTitle": "Public status page",
|
||||
"publicPage": "Public page",
|
||||
"publicPageHint": "When off, the status page and its API answer only to the logged-in panel.",
|
||||
"siteName": "Site name",
|
||||
"site": "Site URL",
|
||||
"githubProxy": "GitHub proxy",
|
||||
"githubProxyHint": "Prefix used when downloading theme packages from GitHub; leave empty to connect directly.",
|
||||
"favicon": "Site icon (favicon)",
|
||||
"touchIcon": "Touch icon (apple-touch-icon)",
|
||||
"chooseImage": "Choose image",
|
||||
"iconHint": "PNG, ICO, SVG, WebP, JPEG or GIF, at most 20 KiB; larger photos are scaled down. The icon follows the site across theme switches.",
|
||||
"iconTooLarge": "The image cannot be fitted into 20 KiB",
|
||||
"saved": "Saved",
|
||||
"saveFailed": "Operation failed",
|
||||
"uploadInstall": "Upload theme.tar.gz",
|
||||
"installed": "Theme \"{{name}}\" installed",
|
||||
"uploadFailed": "Upload failed",
|
||||
"installByUrl": "Install by address",
|
||||
"installByUrlPlaceholder": "Theme GitHub repository, e.g. https://github.com/author/repo",
|
||||
"installFailed": "Install failed",
|
||||
"inUse": "In use",
|
||||
"use": "Use",
|
||||
"builtin": "Built-in",
|
||||
"deleteConfirm": "Delete this theme?",
|
||||
"updatedTo": "Updated to {{version}}",
|
||||
"alreadyLatest": "Already the latest version",
|
||||
"config": "Theme settings"
|
||||
},
|
||||
"defaultTag": "預設",
|
||||
"title": "面板設定",
|
||||
"save": "儲存",
|
||||
|
||||
Reference in New Issue
Block a user