diff --git a/desktop/app.js b/desktop/app.js index 9fd7da3..536547f 100644 --- a/desktop/app.js +++ b/desktop/app.js @@ -18,8 +18,10 @@ let locale = 'en'; let messages = getMessages(locale); let tray = null; const repoUrl = 'https://github.com/xxnuo/MTranServer'; +const releasesUrl = 'https://github.com/xxnuo/MTranServer/releases'; let portCheckPromise = null; let desktopLogPath = null; +let newVersionAvailable = null; function getLocalHost(host) { if (!host || host === '0.0.0.0') return '127.0.0.1'; @@ -38,6 +40,38 @@ function getSettingsUrl(server) { return `http://${getLocalHost(server.host)}:${server.port}/ui/settings`; } +function compareVersions(current, latest) { + const parseCurrent = current.replace(/^v/, '').split('.').map(Number); + const parseLatest = latest.replace(/^v/, '').split('.').map(Number); + for (let i = 0; i < Math.max(parseCurrent.length, parseLatest.length); i++) { + const a = parseCurrent[i] || 0; + const b = parseLatest[i] || 0; + if (a < b) return -1; + if (a > b) return 1; + } + return 0; +} + +async function checkForUpdate() { + try { + const response = await fetch(releasesUrl, { + headers: { 'Accept': 'text/html' }, + redirect: 'follow', + }); + if (!response.ok) return; + const html = await response.text(); + const match = html.match(/\/xxnuo\/MTranServer\/releases\/tag\/v?([\d.]+)/); + if (!match) return; + const latestVersion = match[1]; + const currentVersion = app.getVersion(); + if (compareVersions(currentVersion, latestVersion) < 0) { + newVersionAvailable = latestVersion; + updateTray(); + } + } catch { + } +} + function getStatusLabel() { const status = getServerStatus(); if (status === 'running') { @@ -76,6 +110,8 @@ function updateTray() { messages, statusLabel: getStatusLabel(), versionLabel: app.getVersion(), + newVersionLabel: newVersionAvailable, + autoStartEnabled: app.getLoginItemSettings().openAtLogin, onOpenBrowserUi: () => shell.openExternal(getUiUrl(desktopConfig.server)), onOpenBrowserDocs: () => shell.openExternal(getDocsUrl(desktopConfig.server)), onOpenRepo: () => shell.openExternal(repoUrl), @@ -83,10 +119,18 @@ function updateTray() { onRestart: restartServer, onOpenModels: () => shell.openPath(desktopConfig.server.modelDir), onOpenConfig: () => shell.openPath(desktopConfig.server.configDir), + onToggleAutoStart: toggleAutoStart, + onOpenReleasePage: () => shell.openExternal(releasesUrl), onQuit: quitApp }); } +function toggleAutoStart() { + const current = app.getLoginItemSettings().openAtLogin; + app.setLoginItemSettings({ openAtLogin: !current }); + updateTray(); +} + async function logDesktop(message, error) { if (!desktopLogPath) return; const timestamp = new Date().toISOString(); @@ -244,6 +288,8 @@ export async function startDesktop() { messages, statusLabel: getStatusLabel(), versionLabel: app.getVersion(), + newVersionLabel: newVersionAvailable, + autoStartEnabled: app.getLoginItemSettings().openAtLogin, onOpenBrowserUi: () => shell.openExternal(getUiUrl(desktopConfig.server)), onOpenBrowserDocs: () => shell.openExternal(getDocsUrl(desktopConfig.server)), onOpenRepo: () => shell.openExternal(repoUrl), @@ -251,6 +297,8 @@ export async function startDesktop() { onRestart: restartServer, onOpenModels: () => shell.openPath(desktopConfig.server.modelDir), onOpenConfig: () => shell.openPath(desktopConfig.server.configDir), + onToggleAutoStart: toggleAutoStart, + onOpenReleasePage: () => shell.openExternal(releasesUrl), onQuit: quitApp }); @@ -260,6 +308,8 @@ export async function startDesktop() { return; } + checkForUpdate(); + const getConfigResponse = async () => buildDesktopResponse(); const applyConfig = async (config) => { desktopConfig = await saveDesktopConfig(config); diff --git a/desktop/config.js b/desktop/config.js index 21be224..d2e95c1 100644 --- a/desktop/config.js +++ b/desktop/config.js @@ -76,28 +76,78 @@ function normalizeConfig(input) { }; } +function getDiffConfig(config) { + const defaults = getDefaultDesktopConfig(); + const diff = {}; + if (config.locale !== defaults.locale) { + diff.locale = config.locale; + } + const serverDiff = {}; + const serverKeys = Object.keys(defaults.server); + for (const key of serverKeys) { + if (config.server[key] !== defaults.server[key]) { + serverDiff[key] = config.server[key]; + } + } + if (Object.keys(serverDiff).length > 0) { + diff.server = serverDiff; + } + return diff; +} + +function isConfigDefault(config) { + const diff = getDiffConfig(config); + return Object.keys(diff).length === 0; +} + export async function loadDesktopConfig() { try { const raw = await fsPromises.readFile(desktopConfigPath, 'utf8'); const parsed = YAML.parse(raw); - return normalizeConfig(parsed); - } catch (error) { - const defaults = getDefaultDesktopConfig(); - await saveDesktopConfig(defaults); - return defaults; + const normalized = normalizeConfig(parsed); + const diff = getDiffConfig(normalized); + if (Object.keys(diff).length === 0) { + try { + await fsPromises.unlink(desktopConfigPath); + } catch { + } + } else { + const currentDiff = getDiffConfig(normalizeConfig(parsed)); + const parsedKeys = Object.keys(parsed?.server || {}); + const defaultKeys = Object.keys(getDefaultDesktopConfig().server); + const hasRemovedKeys = parsedKeys.some(k => !defaultKeys.includes(k)); + if (hasRemovedKeys) { + await fsPromises.mkdir(serverConfigDir, { recursive: true }); + const data = YAML.stringify(diff); + await fsPromises.writeFile(desktopConfigPath, data, 'utf8'); + } + } + return normalized; + } catch { + return getDefaultDesktopConfig(); } } export async function saveDesktopConfig(config) { const normalized = normalizeConfig(config); - await fsPromises.mkdir(serverConfigDir, { recursive: true }); - const data = YAML.stringify(normalized); - await fsPromises.writeFile(desktopConfigPath, data, 'utf8'); + const diff = getDiffConfig(normalized); + if (Object.keys(diff).length === 0) { + try { + await fsPromises.unlink(desktopConfigPath); + } catch { + } + } else { + await fsPromises.mkdir(serverConfigDir, { recursive: true }); + const data = YAML.stringify(diff); + await fsPromises.writeFile(desktopConfigPath, data, 'utf8'); + } return normalized; } export async function resetDesktopConfig() { - const defaults = getDefaultDesktopConfig(); - await saveDesktopConfig(defaults); - return defaults; + try { + await fsPromises.unlink(desktopConfigPath); + } catch { + } + return getDefaultDesktopConfig(); } diff --git a/desktop/i18n.js b/desktop/i18n.js index 32e8e9f..4a7d834 100644 --- a/desktop/i18n.js +++ b/desktop/i18n.js @@ -15,6 +15,8 @@ const messages = { trayOpenModels: 'Open Model Folder', trayOpenConfig: 'Open Config Folder', trayVersion: 'Version', + trayNewVersion: 'New version available: v{version}', + trayAutoStart: 'Start at Login', trayQuit: 'Quit', appName: 'MTranServer', portInUseTitle: 'Port is in use', @@ -41,6 +43,8 @@ const messages = { trayOpenModels: '打开模型目录', trayOpenConfig: '打开配置目录', trayVersion: '版本号', + trayNewVersion: '发现新版本: v{version}', + trayAutoStart: '开机启动', trayQuit: '退出程序', appName: 'MTranServer', portInUseTitle: '端口被占用', @@ -67,6 +71,8 @@ const messages = { trayOpenModels: 'モデルフォルダを開く', trayOpenConfig: '設定フォルダを開く', trayVersion: 'バージョン', + trayNewVersion: '新しいバージョンがあります: v{version}', + trayAutoStart: 'ログイン時に起動', trayQuit: '終了', appName: 'MTranServer', portInUseTitle: 'ポートが使用中です', diff --git a/desktop/tray.js b/desktop/tray.js index ebe4d68..22b8718 100644 --- a/desktop/tray.js +++ b/desktop/tray.js @@ -8,6 +8,8 @@ export function createTray({ messages, statusLabel, versionLabel, + newVersionLabel, + autoStartEnabled, onOpenBrowserUi, onOpenBrowserDocs, onOpenRepo, @@ -15,6 +17,8 @@ export function createTray({ onRestart, onOpenModels, onOpenConfig, + onToggleAutoStart, + onOpenReleasePage, onQuit }) { if (tray) return tray; @@ -25,6 +29,8 @@ export function createTray({ messages, statusLabel, versionLabel, + newVersionLabel, + autoStartEnabled, onOpenBrowserUi, onOpenBrowserDocs, onOpenRepo, @@ -32,6 +38,8 @@ export function createTray({ onRestart, onOpenModels, onOpenConfig, + onToggleAutoStart, + onOpenReleasePage, onQuit }); return tray; @@ -41,6 +49,8 @@ export function updateTrayMenu({ messages, statusLabel, versionLabel, + newVersionLabel, + autoStartEnabled, onOpenBrowserUi, onOpenBrowserDocs, onOpenRepo, @@ -48,10 +58,12 @@ export function updateTrayMenu({ onRestart, onOpenModels, onOpenConfig, + onToggleAutoStart, + onOpenReleasePage, onQuit }) { if (!tray) return; - const contextMenu = Menu.buildFromTemplate([ + const menuItems = [ { label: messages.trayOpenUi, click: onOpenBrowserUi @@ -76,6 +88,12 @@ export function updateTrayMenu({ { label: messages.trayRestart, click: onRestart } ] }, + { + label: messages.trayAutoStart, + type: 'checkbox', + checked: autoStartEnabled, + click: onToggleAutoStart + }, { type: 'separator' }, { label: messages.trayData, @@ -87,11 +105,18 @@ export function updateTrayMenu({ { label: `${messages.trayVersion}: ${versionLabel}`, enabled: false - }, - { - label: messages.trayQuit, - click: onQuit } - ]); + ]; + if (newVersionLabel) { + menuItems.push({ + label: messages.trayNewVersion.replace('{version}', newVersionLabel), + click: onOpenReleasePage + }); + } + menuItems.push({ + label: messages.trayQuit, + click: onQuit + }); + const contextMenu = Menu.buildFromTemplate(menuItems); tray.setContextMenu(contextMenu); }