From 104a73b48265461f9f98920261cdd2b56914253d Mon Sep 17 00:00:00 2001 From: xxnuo <54252779+xxnuo@users.noreply.github.com> Date: Sat, 10 Jan 2026 00:26:59 +0800 Subject: [PATCH] feat(desktop): simplify --- desktop/app.js | 121 +++++++--------------- desktop/tray.js | 21 ++-- src/config/index.ts | 78 +++++++++++--- src/server/index.ts | 115 +++++++++++++++++++- ui/src/components/DesktopSettingsPage.tsx | 73 ++++++------- ui/src/lib/desktop.ts | 28 +++++ 6 files changed, 281 insertions(+), 155 deletions(-) diff --git a/desktop/app.js b/desktop/app.js index 2eac8d4..9fd7da3 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -1,4 +1,4 @@ -import { app, dialog, nativeImage, shell, Menu } from 'electron'; +import { app, dialog, nativeImage, shell, Menu, screen } from 'electron'; import fs from 'fs/promises'; import fsSync from 'fs'; import path from 'path'; @@ -7,13 +7,6 @@ import { loadDesktopConfig, saveDesktopConfig, resetDesktopConfig, getDefaultDes import { resolveLocale, getMessages } from './i18n.js'; import { getFreePort, isPortAvailable } from './ports.js'; import { getServerStatus, startServerWithConfig, stopServerInstance } from './server.js'; -import { - createMainWindow, - createSettingsWindow, - showMainWindow, - showSettingsWindow, - updateWindowUrls -} from './windows.js'; import { createTray, updateTrayMenu } from './tray.js'; import { registerIpcHandlers } from './ipc.js'; @@ -83,11 +76,10 @@ function updateTray() { messages, statusLabel: getStatusLabel(), versionLabel: app.getVersion(), - onOpenMain: showMainWindow, onOpenBrowserUi: () => shell.openExternal(getUiUrl(desktopConfig.server)), onOpenBrowserDocs: () => shell.openExternal(getDocsUrl(desktopConfig.server)), onOpenRepo: () => shell.openExternal(repoUrl), - onOpenSettings: showSettingsWindow, + onOpenSettings: () => shell.openExternal(getSettingsUrl(desktopConfig.server)), onRestart: restartServer, onOpenModels: () => shell.openPath(desktopConfig.server.modelDir), onOpenConfig: () => shell.openPath(desktopConfig.server.configDir), @@ -167,10 +159,6 @@ async function restartServer() { await ensureWritableDirs(); const ok = await startServer(); if (!ok) return false; - updateWindowUrls({ - mainUrl: getUiUrl(desktopConfig.server), - settingsUrl: getSettingsUrl(desktopConfig.server) - }); updateTray(); return true; } catch { @@ -232,40 +220,16 @@ async function ensureWritableDirs() { } } -function getLoadingUrl() { - const html = ` - - - - - - ${messages.appName} - - - -
-
-
${messages.appName}
-
${messages.trayServiceStatus}: ${messages.trayServiceRunning} ...
-
-
- - - `; - return `data:text/html;charset=utf-8,${encodeURIComponent(html)}`; +function buildDesktopResponse() { + return { config: desktopConfig, status: getServerStatus(), version: app.getVersion() }; } export async function startDesktop() { app.isQuitting = false; Menu.setApplicationMenu(null); + if (app.dock?.hide) { + app.dock.hide(); + } desktopConfig = await loadDesktopConfig(); updateLocale(desktopConfig.locale); desktopLogPath = path.join(desktopConfig.server.configDir, 'desktop.log'); @@ -273,12 +237,6 @@ export async function startDesktop() { await logDesktop('desktop starting'); await ensureWritableDirs(); - const preloadPath = path.join(__dirname, 'preload.cjs'); - const loadingUrl = getLoadingUrl(); - const mainWindow = createMainWindow({ url: loadingUrl, preload: preloadPath }); - createSettingsWindow({ url: loadingUrl, preload: preloadPath, parent: mainWindow }); - mainWindow.once('ready-to-show', () => mainWindow.show()); - const trayIcon = getTrayIcon(); tray = createTray({ icon: trayIcon, @@ -286,11 +244,10 @@ export async function startDesktop() { messages, statusLabel: getStatusLabel(), versionLabel: app.getVersion(), - onOpenMain: showMainWindow, onOpenBrowserUi: () => shell.openExternal(getUiUrl(desktopConfig.server)), onOpenBrowserDocs: () => shell.openExternal(getDocsUrl(desktopConfig.server)), onOpenRepo: () => shell.openExternal(repoUrl), - onOpenSettings: showSettingsWindow, + onOpenSettings: () => shell.openExternal(getSettingsUrl(desktopConfig.server)), onRestart: restartServer, onOpenModels: () => shell.openPath(desktopConfig.server.modelDir), onOpenConfig: () => shell.openPath(desktopConfig.server.configDir), @@ -303,52 +260,52 @@ export async function startDesktop() { return; } - updateWindowUrls({ - mainUrl: getUiUrl(desktopConfig.server), - settingsUrl: getSettingsUrl(desktopConfig.server) - }); + const getConfigResponse = async () => buildDesktopResponse(); + const applyConfig = async (config) => { + desktopConfig = await saveDesktopConfig(config); + updateLocale(desktopConfig.locale); + const ok = await restartServer(); + if (!ok) return buildDesktopResponse(); + return buildDesktopResponse(); + }; + const resetConfig = async () => { + desktopConfig = await resetDesktopConfig(); + updateLocale(desktopConfig.locale); + const ok = await restartServer(); + if (!ok) return buildDesktopResponse(); + return buildDesktopResponse(); + }; + const restartAndRespond = async () => { + const ok = await restartServer(); + if (!ok) return buildDesktopResponse(); + return buildDesktopResponse(); + }; + + globalThis.mtranDesktopControl = { + getConfig: getConfigResponse, + applyConfig, + resetConfig, + restartServer: restartAndRespond + }; registerIpcHandlers({ - getConfig: async () => ({ - config: desktopConfig, - status: getServerStatus(), - version: app.getVersion() - }), - applyConfig: async (config) => { - desktopConfig = await saveDesktopConfig(config); - updateLocale(desktopConfig.locale); - const ok = await restartServer(); - if (!ok) return { config: desktopConfig, status: getServerStatus(), version: app.getVersion() }; - return { config: desktopConfig, status: getServerStatus(), version: app.getVersion() }; - }, - resetConfig: async () => { - desktopConfig = await resetDesktopConfig(); - updateLocale(desktopConfig.locale); - const ok = await restartServer(); - if (!ok) return { config: desktopConfig, status: getServerStatus(), version: app.getVersion() }; - return { config: desktopConfig, status: getServerStatus(), version: app.getVersion() }; - }, - restartServer: async () => { - const ok = await restartServer(); - if (!ok) return { config: desktopConfig, status: getServerStatus(), version: app.getVersion() }; - return { config: desktopConfig, status: getServerStatus(), version: app.getVersion() }; - }, + getConfig: getConfigResponse, + applyConfig, + resetConfig, + restartServer: restartAndRespond, getStatus: async () => ({ status: getServerStatus() }), openExternal: async (url) => shell.openExternal(url), openPath: async (targetPath) => shell.openPath(targetPath) }); - app.on('activate', () => showMainWindow()); app.on('before-quit', (event) => { if (app.isQuitting) return; event.preventDefault(); quitApp(); }); - app.on('window-all-closed', (event) => event.preventDefault()); updateTray(); } export function focusMainWindow() { - showMainWindow(); } diff --git a/desktop/tray.js b/desktop/tray.js index d9fea4c..ebe4d68 100644 --- a/desktop/tray.js +++ b/desktop/tray.js @@ -8,7 +8,6 @@ export function createTray({ messages, statusLabel, versionLabel, - onOpenMain, onOpenBrowserUi, onOpenBrowserDocs, onOpenRepo, @@ -21,12 +20,11 @@ export function createTray({ if (tray) return tray; tray = new Tray(icon); tray.setToolTip(tooltip); - tray.on('click', () => onOpenMain()); + tray.on('click', () => tray.popUpContextMenu()); updateTrayMenu({ messages, statusLabel, versionLabel, - onOpenMain, onOpenBrowserUi, onOpenBrowserDocs, onOpenRepo, @@ -43,7 +41,6 @@ export function updateTrayMenu({ messages, statusLabel, versionLabel, - onOpenMain, onOpenBrowserUi, onOpenBrowserDocs, onOpenRepo, @@ -56,16 +53,16 @@ export function updateTrayMenu({ if (!tray) return; const contextMenu = Menu.buildFromTemplate([ { - label: messages.trayOpenMain, - click: onOpenMain + label: messages.trayOpenUi, + click: onOpenBrowserUi }, { - label: messages.trayOpenBrowser, - submenu: [ - { label: messages.trayOpenUi, click: onOpenBrowserUi }, - { label: messages.trayOpenDocs, click: onOpenBrowserDocs }, - { label: messages.trayOpenRepo, click: onOpenRepo } - ] + label: messages.trayOpenDocs, + click: onOpenBrowserDocs + }, + { + label: messages.trayOpenRepo, + click: onOpenRepo }, { type: 'separator' }, { diff --git a/src/config/index.ts b/src/config/index.ts index 203be7a..db16c7b 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -24,6 +24,27 @@ export interface Config { } let globalConfig: Config | null = null; +let fileConfigCache: Partial | null = null; + +function getConfigFilePath(homeDir: string) { + return path.join(homeDir, 'server.json'); +} + +function readConfigFile(homeDir: string): Partial { + if (fileConfigCache) return fileConfigCache; + const configPath = getConfigFilePath(homeDir); + try { + const raw = fs.readFileSync(configPath, 'utf8'); + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') { + fileConfigCache = parsed as Partial; + return fileConfigCache; + } + } catch { + return {}; + } + return {}; +} function getArgValue(flag: string): string | null { const index = process.argv.indexOf(flag); @@ -88,12 +109,13 @@ export function getConfig(): Config { } const homeDir = path.join(os.homedir(), '.config', 'mtran'); + const fileConfig = readConfigFile(homeDir); - const configDir = getString('--config-dir', 'MT_CONFIG_DIR', path.join(homeDir, 'server')); + const configDir = getString('--config-dir', 'MT_CONFIG_DIR', fileConfig.configDir || path.join(homeDir, 'server')); const localModelsDir = path.join(process.cwd(), 'models'); const defaultModelDir = fs.existsSync(localModelsDir) ? localModelsDir : path.join(homeDir, 'models'); - const modelDir = getString('--model-dir', 'MT_MODEL_DIR', defaultModelDir); - const logDir = getString('--log-dir', 'MT_LOG_DIR', path.join(homeDir, 'logs')); + const modelDir = getString('--model-dir', 'MT_MODEL_DIR', fileConfig.modelDir || defaultModelDir); + const logDir = getString('--log-dir', 'MT_LOG_DIR', fileConfig.logDir || path.join(homeDir, 'logs')); globalConfig = { homeDir, @@ -101,26 +123,26 @@ export function getConfig(): Config { modelDir, logDir, - logLevel: getString('--log-level', 'MT_LOG_LEVEL', 'warn'), - host: getString('--host', 'MT_HOST', '0.0.0.0'), - port: getString('--port', 'MT_PORT', '8989'), + logLevel: getString('--log-level', 'MT_LOG_LEVEL', fileConfig.logLevel || 'warn'), + host: getString('--host', 'MT_HOST', fileConfig.host || '0.0.0.0'), + port: getString('--port', 'MT_PORT', fileConfig.port || '8989'), - enableWebUI: getBool('--ui', 'MT_ENABLE_UI', true), - enableOfflineMode: getBool('--offline', 'MT_OFFLINE', false), + enableWebUI: getBool('--ui', 'MT_ENABLE_UI', fileConfig.enableWebUI ?? true), + enableOfflineMode: getBool('--offline', 'MT_OFFLINE', fileConfig.enableOfflineMode ?? false), - workerIdleTimeout: getInt('--worker-idle-timeout', 'MT_WORKER_IDLE_TIMEOUT', 60), - workersPerLanguage: getInt('--workers-per-language', 'MT_WORKERS_PER_LANGUAGE', 1), - maxLengthBreak: getInt('--max-length-break', 'MT_MAX_LENGTH_BREAK', 128), + workerIdleTimeout: getInt('--worker-idle-timeout', 'MT_WORKER_IDLE_TIMEOUT', fileConfig.workerIdleTimeout ?? 60), + workersPerLanguage: getInt('--workers-per-language', 'MT_WORKERS_PER_LANGUAGE', fileConfig.workersPerLanguage ?? 1), + maxLengthBreak: getInt('--max-length-break', 'MT_MAX_LENGTH_BREAK', fileConfig.maxLengthBreak ?? 128), - apiToken: getString('--api-token', 'MT_API_TOKEN', ''), + apiToken: getString('--api-token', 'MT_API_TOKEN', fileConfig.apiToken || ''), - logToFile: getBool('--log-to-file', 'MT_LOG_TO_FILE', false), - logConsole: getBool('--log-console', 'MT_LOG_CONSOLE', true), - logRequests: getBool('--log-requests', 'MT_LOG_REQUESTS', false), + logToFile: getBool('--log-to-file', 'MT_LOG_TO_FILE', fileConfig.logToFile ?? false), + logConsole: getBool('--log-console', 'MT_LOG_CONSOLE', fileConfig.logConsole ?? true), + logRequests: getBool('--log-requests', 'MT_LOG_REQUESTS', fileConfig.logRequests ?? false), - checkUpdate: getBool('--check-update', 'MT_CHECK_UPDATE', true), + checkUpdate: getBool('--check-update', 'MT_CHECK_UPDATE', fileConfig.checkUpdate ?? true), - cacheSize: getInt('--cache-size', 'MT_CACHE_SIZE', 1000), + cacheSize: getInt('--cache-size', 'MT_CACHE_SIZE', fileConfig.cacheSize ?? 1000), }; return globalConfig; @@ -133,4 +155,26 @@ export function setConfig(config: Partial) { export function resetConfig() { globalConfig = null; + fileConfigCache = null; +} + +export function saveConfigFile(config: Partial) { + const current = getConfig(); + const next = { ...current, ...config }; + const configPath = getConfigFilePath(current.homeDir); + fs.mkdirSync(current.homeDir, { recursive: true }); + fs.writeFileSync(configPath, JSON.stringify(next, null, 2), 'utf8'); + fileConfigCache = next; +} + +export function clearConfigFile() { + const homeDir = path.join(os.homedir(), '.config', 'mtran'); + const configPath = getConfigFilePath(homeDir); + try { + if (fs.existsSync(configPath)) { + fs.unlinkSync(configPath); + } + } catch { + return; + } } diff --git a/src/server/index.ts b/src/server/index.ts index 5f83be8..348b135 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1,7 +1,7 @@ import express, { Request, Response, NextFunction } from 'express'; import fs from 'fs/promises'; import swaggerUi from 'swagger-ui-express'; -import { getConfig } from '@/config/index.js'; +import { getConfig, setConfig, resetConfig, saveConfigFile, clearConfigFile } from '@/config/index.js'; import * as logger from '@/logger/index.js'; import { initRecords } from '@/models/index.js'; import { cleanupAllEngines } from '@/services/index.js'; @@ -36,6 +36,119 @@ export async function startServer({ handleSignals = true } = {}) { app.use(requestLogger()); } + const getDesktopControl = () => (globalThis as any).mtranDesktopControl; + const getSettingsPayload = () => { + const current = getConfig(); + return { + config: { + locale: 'system', + server: { + host: current.host, + port: Number(current.port), + logLevel: current.logLevel, + enableWebUI: current.enableWebUI, + enableOfflineMode: current.enableOfflineMode, + workerIdleTimeout: current.workerIdleTimeout, + workersPerLanguage: current.workersPerLanguage, + apiToken: current.apiToken, + logDir: current.logDir, + logToFile: current.logToFile, + logConsole: current.logConsole, + logRequests: current.logRequests, + maxLengthBreak: current.maxLengthBreak, + checkUpdate: current.checkUpdate, + cacheSize: current.cacheSize, + modelDir: current.modelDir, + configDir: current.configDir + } + }, + status: 'running', + version: VERSION + }; + }; + const toNumber = (value: unknown, fallback: number) => { + if (typeof value === 'number' && Number.isFinite(value)) return value; + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; + }; + const toBool = (value: unknown, fallback: boolean) => { + if (typeof value === 'boolean') return value; + return fallback; + }; + const toString = (value: unknown, fallback: string) => { + if (typeof value === 'string') return value; + return fallback; + }; + const applyServerConfig = (input: any) => { + const current = getConfig(); + const next = { + host: toString(input.host, current.host), + port: String(toNumber(input.port, Number(current.port))), + logLevel: toString(input.logLevel, current.logLevel), + enableWebUI: toBool(input.enableWebUI, current.enableWebUI), + enableOfflineMode: toBool(input.enableOfflineMode, current.enableOfflineMode), + workerIdleTimeout: toNumber(input.workerIdleTimeout, current.workerIdleTimeout), + workersPerLanguage: toNumber(input.workersPerLanguage, current.workersPerLanguage), + apiToken: toString(input.apiToken, current.apiToken), + logDir: toString(input.logDir, current.logDir), + logToFile: toBool(input.logToFile, current.logToFile), + logConsole: toBool(input.logConsole, current.logConsole), + logRequests: toBool(input.logRequests, current.logRequests), + maxLengthBreak: toNumber(input.maxLengthBreak, current.maxLengthBreak), + checkUpdate: toBool(input.checkUpdate, current.checkUpdate), + cacheSize: toNumber(input.cacheSize, current.cacheSize), + modelDir: toString(input.modelDir, current.modelDir), + configDir: toString(input.configDir, current.configDir) + }; + setConfig(next); + saveConfigFile(next); + logger.setLogLevel(next.logLevel as any); + }; + + app.get('/ui/api/settings', async (_, res) => { + const control = getDesktopControl(); + if (control?.getConfig) { + const payload = await control.getConfig(); + res.json(payload); + return; + } + res.json(getSettingsPayload()); + }); + + app.post('/ui/api/settings/apply', async (req, res) => { + const control = getDesktopControl(); + if (control?.applyConfig) { + const payload = await control.applyConfig(req.body?.config || req.body); + res.json(payload); + return; + } + const input = req.body?.config?.server || req.body?.server || {}; + applyServerConfig(input); + res.json(getSettingsPayload()); + }); + + app.post('/ui/api/settings/reset', async (_, res) => { + const control = getDesktopControl(); + if (control?.resetConfig) { + const payload = await control.resetConfig(); + res.json(payload); + return; + } + clearConfigFile(); + resetConfig(); + res.json(getSettingsPayload()); + }); + + app.post('/ui/api/settings/restart', async (_, res) => { + const control = getDesktopControl(); + if (control?.restartServer) { + const payload = await control.restartServer(); + res.json(payload); + return; + } + res.json(getSettingsPayload()); + }); + RegisterRoutes(app); app.use('/ui', (req: Request, res: Response, next: NextFunction) => { diff --git a/ui/src/components/DesktopSettingsPage.tsx b/ui/src/components/DesktopSettingsPage.tsx index 5ed8f25..bf39c3b 100644 --- a/ui/src/components/DesktopSettingsPage.tsx +++ b/ui/src/components/DesktopSettingsPage.tsx @@ -6,14 +6,17 @@ import { Switch } from '@/components/ui/switch' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Separator } from '@/components/ui/separator' -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert' import { toast } from 'sonner' import { applyDesktopConfig, getDesktopConfig, isDesktop, resetDesktopConfig, - restartDesktopServer + restartDesktopServer, + applyWebConfig, + getWebConfig, + resetWebConfig, + restartWebServer } from '@/lib/desktop' import type { DesktopConfig } from '@/lib/desktop' @@ -73,6 +76,10 @@ function toNumber(value: string, fallback: number) { export function DesktopSettingsPage() { const { t } = useTranslation() const desktopAvailable = isDesktop() + const getConfigData = desktopAvailable ? getDesktopConfig : getWebConfig + const applyConfig = desktopAvailable ? applyDesktopConfig : applyWebConfig + const resetConfig = desktopAvailable ? resetDesktopConfig : resetWebConfig + const restartServer = desktopAvailable ? restartDesktopServer : restartWebServer const [config, setConfig] = useState(null) const [form, setForm] = useState(null) const [status, setStatus] = useState('') @@ -81,11 +88,7 @@ export function DesktopSettingsPage() { const [saving, setSaving] = useState(false) useEffect(() => { - if (!desktopAvailable) { - setLoading(false) - return - } - getDesktopConfig() + getConfigData() .then((response) => { if (!response) return setConfig(response.config) @@ -97,7 +100,7 @@ export function DesktopSettingsPage() { toast.error(t('loadingConfigFailed')) }) .finally(() => setLoading(false)) - }, [desktopAvailable, t]) + }, [getConfigData, t]) const serverStatusLabel = useMemo(() => { if (!status) return '' @@ -131,7 +134,7 @@ export function DesktopSettingsPage() { } } try { - const response = await applyDesktopConfig(nextConfig) + const response = await applyConfig(nextConfig) if (response) { setConfig(response.config) setForm(toForm(response.config)) @@ -147,10 +150,9 @@ export function DesktopSettingsPage() { } const handleRestart = async () => { - if (!desktopAvailable) return setSaving(true) try { - const response = await restartDesktopServer() + const response = await restartServer() if (response) { setConfig(response.config) setForm(toForm(response.config)) @@ -166,10 +168,9 @@ export function DesktopSettingsPage() { } const handleReset = async () => { - if (!desktopAvailable) return setSaving(true) try { - const response = await resetDesktopConfig() + const response = await resetConfig() if (response) { setConfig(response.config) setForm(toForm(response.config)) @@ -184,22 +185,6 @@ export function DesktopSettingsPage() { } } - if (!desktopAvailable) { - return ( -
-
- - {t('desktopOnly')} - {t('desktopOnlyDesc')} - -
- -
-
-
- ) - } - if (loading || !form || !config) { return (
@@ -250,20 +235,22 @@ export function DesktopSettingsPage() {
-
- - -
+ {desktopAvailable && ( +
+ + +
+ )}
diff --git a/ui/src/lib/desktop.ts b/ui/src/lib/desktop.ts index 42f3b8f..a02fd59 100644 --- a/ui/src/lib/desktop.ts +++ b/ui/src/lib/desktop.ts @@ -50,3 +50,31 @@ export async function restartDesktopServer() { if (!window.mtranDesktop) return null return window.mtranDesktop.restartServer() } + +async function fetchSettings(path: string, body?: unknown) { + const res = await fetch(`/ui/api/settings${path}`, { + method: body ? 'POST' : 'GET', + headers: body ? { 'Content-Type': 'application/json' } : undefined, + body: body ? JSON.stringify(body) : undefined + }) + if (!res.ok) { + throw new Error(`Request failed: ${res.status}`) + } + return res.json() +} + +export async function getWebConfig() { + return fetchSettings('') +} + +export async function applyWebConfig(config: DesktopConfig) { + return fetchSettings('/apply', { config }) +} + +export async function resetWebConfig() { + return fetchSettings('/reset', {}) +} + +export async function restartWebServer() { + return fetchSettings('/restart', {}) +}