B5 frontend: public-page settings tab — theme store UI, chunked upload install, config editor; 28 i18n keys x13 locales

This commit is contained in:
Muse
2026-10-22 23:19:35 +00:00
parent 000731c4e0
commit 2ee20b9691
17 changed files with 993 additions and 0 deletions
+4
View File
@@ -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')) {
+5
View File
@@ -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} />;
}
+30
View File
@@ -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": "حفظ",
+30
View File
@@ -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",
+30
View File
@@ -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",
+30
View File
@@ -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": "ذخیره",
+30
View File
@@ -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",
+30
View File
@@ -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": "保存",
+30
View File
@@ -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",
+30
View File
@@ -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": "Сохранить",
+30
View File
@@ -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",
+30
View File
@@ -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": "Зберегти",
+30
View File
@@ -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",
+30
View File
@@ -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": "保存",
+30
View File
@@ -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": "儲存",