From 2ee20b9691de414dbad9aecfc575e80476d26cfa Mon Sep 17 00:00:00 2001 From: Muse Date: Thu, 22 Oct 2026 23:19:35 +0000 Subject: [PATCH] =?UTF-8?q?B5=20frontend:=20public-page=20settings=20tab?= =?UTF-8?q?=20=E2=80=94=20theme=20store=20UI,=20chunked=20upload=20install?= =?UTF-8?q?,=20config=20editor;=2028=20i18n=20keys=20x13=20locales?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/http-init.ts | 4 + frontend/src/layouts/AppSidebar.tsx | 5 + frontend/src/pages/settings/PublicPageTab.tsx | 590 ++++++++++++++++++ frontend/src/pages/settings/SettingsPage.tsx | 4 + internal/web/translation/ar-EG.json | 30 + internal/web/translation/en-US.json | 30 + internal/web/translation/es-ES.json | 30 + internal/web/translation/fa-IR.json | 30 + internal/web/translation/id-ID.json | 30 + internal/web/translation/ja-JP.json | 30 + internal/web/translation/pt-BR.json | 30 + internal/web/translation/ru-RU.json | 30 + internal/web/translation/tr-TR.json | 30 + internal/web/translation/uk-UA.json | 30 + internal/web/translation/vi-VN.json | 30 + internal/web/translation/zh-CN.json | 30 + internal/web/translation/zh-TW.json | 30 + 17 files changed, 993 insertions(+) create mode 100644 frontend/src/pages/settings/PublicPageTab.tsx diff --git a/frontend/src/api/http-init.ts b/frontend/src/api/http-init.ts index f5db3c1..d05fe27 100644 --- a/frontend/src/api/http-init.ts +++ b/frontend/src/api/http-init.ts @@ -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')) { diff --git a/frontend/src/layouts/AppSidebar.tsx b/frontend/src/layouts/AppSidebar.tsx index 5d57094..370ae54 100644 --- a/frontend/src/layouts/AppSidebar.tsx +++ b/frontend/src/layouts/AppSidebar.tsx @@ -251,6 +251,11 @@ export default function AppSidebar() { icon: , label: t('pages.settings.subSettings'), }, + { + key: '/settings#public-page', + icon: , + label: t('pages.settings.probeThemes.title'), + }, ]; if (showSubFormats) { children.push({ diff --git a/frontend/src/pages/settings/PublicPageTab.tsx b/frontend/src/pages/settings/PublicPageTab.tsx new file mode 100644 index 0000000..96bf6bd --- /dev/null +++ b/frontend/src/pages/settings/PublicPageTab.tsx @@ -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 { + 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 { + 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((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([]); + const [settings, setSettings] = useState({ + 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(null); + const [busyShort, setBusyShort] = useState(''); + const fileRef = useRef(null); + const faviconRef = useRef(null); + const touchRef = useRef(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('/panel/api/probe/themes', { silent: true }); + if (msg?.success && msg.obj) applyPayload(msg.obj); + }, [applyPayload]); + + useEffect(() => { + let cancelled = false; + HttpUtil.get('/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 ( +
+ {messageContextHolder} + {t('pages.settings.probeThemes.settingsTitle')} + + +
{t('pages.settings.probeThemes.publicPage')}
+ setSettings({ ...settings, publicPage: v })} + /> + + {t('pages.settings.probeThemes.publicPageHint')} + + + +
{t('pages.settings.probeThemes.siteName')}
+ setSettings({ ...settings, siteName: e.target.value })} + /> + + +
{t('pages.settings.probeThemes.site')}
+ setSettings({ ...settings, site: e.target.value })} + /> + + +
{t('pages.settings.probeThemes.githubProxy')}
+ setSettings({ ...settings, githubProxy: e.target.value })} + /> + + {t('pages.settings.probeThemes.githubProxyHint')} + + + +
{t('pages.settings.probeThemes.favicon')}
+ + {favicon ? : null} + + {favicon ? ( + + ) : null} + + pickIcon('favicon', e.target.files?.[0])} + /> + + +
{t('pages.settings.probeThemes.touchIcon')}
+ + {touchIcon ? : null} + + {touchIcon ? ( + + ) : null} + + pickIcon('touchIcon', e.target.files?.[0])} + /> +
+ + {t('pages.settings.probeThemes.iconHint')} + +
+ +
+
+ +
+ + + {t('pages.settings.probeThemes.title')} + + + + uploadTheme(e.target.files?.[0])} + /> + setInstallURL(e.target.value)} + /> + + + + + {themes.map((theme) => ( + + + ) : undefined + } + actions={[ + theme.selected ? ( + + {t('pages.settings.probeThemes.inUse')} + + ) : ( + + ), + , + ...(theme.config && theme.config.length > 0 + ? [ + messageApi.success(t('pages.settings.probeThemes.saved'))} + onError={(m) => messageApi.error(m)} + />, + ] + : []), + ...(!theme.builtin + ? [ + themeAction(theme.short, 'delete')} + > + + , + ] + : []), + ]} + > + + {theme.name} + {theme.builtin ? {t('pages.settings.probeThemes.builtin')} : null} + + } + description={ +
+
{theme.description}
+
+ v{theme.version} · {theme.author} +
+
+ } + /> +
+ + ))} +
+
+ ); +} + +// 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>({}); + const [saving, setSaving] = useState(false); + const fields = theme.config ?? []; + + async function load() { + let saved: Record = {}; + try { + const res = await fetch(`/api/themes/${theme.short}/config`, { credentials: 'same-origin' }); + if (res.ok) saved = (await res.json()) as Record; + } catch { + /* an unreadable config edits from the defaults */ + } + const merged: Record = {}; + 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 = {}; + 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 ( + <> + + setOpen(false)} + onOk={save} + confirmLoading={saving} + > + {fields.map((f) => ( +
+
{f.label}
+ {f.type === 'boolean' ? ( + setValues({ ...values, [f.key]: v })} + /> + ) : f.type === 'number' ? ( + setValues({ ...values, [f.key]: v })} + style={{ width: '100%' }} + /> + ) : f.type === 'select' && f.options ? ( + setValues({ ...values, [f.key]: e.target.value })} + /> + )} + {f.help ? ( +
+ {f.help} +
+ ) : null} +
+ ))} +
+ + ); +} diff --git a/frontend/src/pages/settings/SettingsPage.tsx b/frontend/src/pages/settings/SettingsPage.tsx index 9db973d..668f73c 100644 --- a/frontend/src/pages/settings/SettingsPage.tsx +++ b/frontend/src/pages/settings/SettingsPage.tsx @@ -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 ; case 'subscription-balancers': return ; + case 'public-page': + return ; default: return ; } diff --git a/internal/web/translation/ar-EG.json b/internal/web/translation/ar-EG.json index f42ae08..ff6268b 100644 --- a/internal/web/translation/ar-EG.json +++ b/internal/web/translation/ar-EG.json @@ -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": "حفظ", diff --git a/internal/web/translation/en-US.json b/internal/web/translation/en-US.json index 4c61c79..b4f7f3a 100644 --- a/internal/web/translation/en-US.json +++ b/internal/web/translation/en-US.json @@ -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", diff --git a/internal/web/translation/es-ES.json b/internal/web/translation/es-ES.json index 8a7d67f..120680c 100644 --- a/internal/web/translation/es-ES.json +++ b/internal/web/translation/es-ES.json @@ -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", diff --git a/internal/web/translation/fa-IR.json b/internal/web/translation/fa-IR.json index aa66fe0..828803e 100644 --- a/internal/web/translation/fa-IR.json +++ b/internal/web/translation/fa-IR.json @@ -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": "ذخیره", diff --git a/internal/web/translation/id-ID.json b/internal/web/translation/id-ID.json index 2b5b0b7..5acb4dc 100644 --- a/internal/web/translation/id-ID.json +++ b/internal/web/translation/id-ID.json @@ -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", diff --git a/internal/web/translation/ja-JP.json b/internal/web/translation/ja-JP.json index ad3cf75..87be6c0 100644 --- a/internal/web/translation/ja-JP.json +++ b/internal/web/translation/ja-JP.json @@ -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": "保存", diff --git a/internal/web/translation/pt-BR.json b/internal/web/translation/pt-BR.json index 5f9d10a..75a9d40 100644 --- a/internal/web/translation/pt-BR.json +++ b/internal/web/translation/pt-BR.json @@ -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", diff --git a/internal/web/translation/ru-RU.json b/internal/web/translation/ru-RU.json index ecddc29..37559d0 100644 --- a/internal/web/translation/ru-RU.json +++ b/internal/web/translation/ru-RU.json @@ -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": "Сохранить", diff --git a/internal/web/translation/tr-TR.json b/internal/web/translation/tr-TR.json index 7c9676e..6cb1e55 100644 --- a/internal/web/translation/tr-TR.json +++ b/internal/web/translation/tr-TR.json @@ -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", diff --git a/internal/web/translation/uk-UA.json b/internal/web/translation/uk-UA.json index cd14b86..e0f656f 100644 --- a/internal/web/translation/uk-UA.json +++ b/internal/web/translation/uk-UA.json @@ -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": "Зберегти", diff --git a/internal/web/translation/vi-VN.json b/internal/web/translation/vi-VN.json index 09c92d0..d85bbed 100644 --- a/internal/web/translation/vi-VN.json +++ b/internal/web/translation/vi-VN.json @@ -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", diff --git a/internal/web/translation/zh-CN.json b/internal/web/translation/zh-CN.json index 2d59afa..60ba24e 100644 --- a/internal/web/translation/zh-CN.json +++ b/internal/web/translation/zh-CN.json @@ -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": "保存", diff --git a/internal/web/translation/zh-TW.json b/internal/web/translation/zh-TW.json index 1ad93f0..bc0acf1 100644 --- a/internal/web/translation/zh-TW.json +++ b/internal/web/translation/zh-TW.json @@ -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": "儲存",