Files
MTranServer/desktop/app.js
2026-01-10 00:26:59 +08:00

312 lines
9.2 KiB
JavaScript

import { app, dialog, nativeImage, shell, Menu, screen } from 'electron';
import fs from 'fs/promises';
import fsSync from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import { loadDesktopConfig, saveDesktopConfig, resetDesktopConfig, getDefaultDesktopConfig } from './config.js';
import { resolveLocale, getMessages } from './i18n.js';
import { getFreePort, isPortAvailable } from './ports.js';
import { getServerStatus, startServerWithConfig, stopServerInstance } from './server.js';
import { createTray, updateTrayMenu } from './tray.js';
import { registerIpcHandlers } from './ipc.js';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
let desktopConfig = null;
let locale = 'en';
let messages = getMessages(locale);
let tray = null;
const repoUrl = 'https://github.com/xxnuo/MTranServer';
let portCheckPromise = null;
let desktopLogPath = null;
function getLocalHost(host) {
if (!host || host === '0.0.0.0') return '127.0.0.1';
return host;
}
function getUiUrl(server) {
return `http://${getLocalHost(server.host)}:${server.port}/ui/`;
}
function getDocsUrl(server) {
return `http://${getLocalHost(server.host)}:${server.port}/docs/`;
}
function getSettingsUrl(server) {
return `http://${getLocalHost(server.host)}:${server.port}/ui/settings`;
}
function getStatusLabel() {
const status = getServerStatus();
if (status === 'running') {
return `${messages.trayServiceRunning} (${desktopConfig.server.port})`;
}
if (status === 'starting') return messages.trayServiceRunning;
return messages.trayServiceStopped;
}
function getTrayIcon() {
if (process.platform === 'darwin') {
const iconPath16 = path.join(__dirname, '..', 'images', 'icons', '[email protected]');
const iconPath32 = path.join(__dirname, '..', 'images', 'icons', '[email protected]');
const icon = nativeImage.createFromPath(iconPath16);
if (fsSync.existsSync(iconPath32)) {
const icon2x = nativeImage.createFromPath(iconPath32);
icon.addRepresentation({
scaleFactor: 2,
width: 32,
height: 32,
buffer: icon2x.toPNG()
});
}
icon.setTemplateImage(true);
return icon;
}
const scaleFactor = screen.getPrimaryDisplay().scaleFactor || 1;
const size = scaleFactor >= 1.25 ? 32 : 16;
const iconPath = path.join(__dirname, '..', 'images', 'icons', `icon@${size}px.png`);
return nativeImage.createFromPath(iconPath);
}
function updateTray() {
if (!tray) return;
updateTrayMenu({
messages,
statusLabel: getStatusLabel(),
versionLabel: app.getVersion(),
onOpenBrowserUi: () => shell.openExternal(getUiUrl(desktopConfig.server)),
onOpenBrowserDocs: () => shell.openExternal(getDocsUrl(desktopConfig.server)),
onOpenRepo: () => shell.openExternal(repoUrl),
onOpenSettings: () => shell.openExternal(getSettingsUrl(desktopConfig.server)),
onRestart: restartServer,
onOpenModels: () => shell.openPath(desktopConfig.server.modelDir),
onOpenConfig: () => shell.openPath(desktopConfig.server.configDir),
onQuit: quitApp
});
}
async function logDesktop(message, error) {
if (!desktopLogPath) return;
const timestamp = new Date().toISOString();
const details = error ? ` ${error.stack || error.message || error}` : '';
try {
await fs.appendFile(desktopLogPath, `[${timestamp}] ${message}${details}\n`, 'utf8');
} catch {
return;
}
}
async function ensurePortAvailable() {
if (portCheckPromise) return portCheckPromise;
portCheckPromise = (async () => {
const host = desktopConfig.server.host || '0.0.0.0';
const available = await isPortAvailable(desktopConfig.server.port, host);
if (available) return true;
await logDesktop(`port in use ${desktopConfig.server.port}`);
const result = await dialog.showMessageBox({
type: 'warning',
buttons: [messages.portInUseUseRandom, messages.portInUseQuit],
defaultId: 0,
cancelId: 1,
message: messages.portInUseTitle,
detail: messages.portInUseDetail.replace('{port}', String(desktopConfig.server.port))
});
if (result.response === 0) {
const newPort = await getFreePort();
await logDesktop(`use random port ${newPort}`);
desktopConfig.server.port = newPort;
desktopConfig = await saveDesktopConfig(desktopConfig);
return true;
}
await logDesktop('quit after port in use');
await quitApp();
return false;
})();
try {
return await portCheckPromise;
} finally {
portCheckPromise = null;
}
}
async function startServer() {
const ok = await ensurePortAvailable();
if (!ok) return false;
try {
await logDesktop('starting server');
await startServerWithConfig(desktopConfig.server);
await logDesktop('server started');
return true;
} catch (error) {
await logDesktop('server start failed', error);
dialog.showMessageBox({
type: 'error',
message: messages.serverStartFailed,
detail: messages.serverStartFailedDetail
});
return false;
}
}
async function restartServer() {
try {
await logDesktop('restarting server');
await stopServerInstance();
await ensureWritableDirs();
const ok = await startServer();
if (!ok) return false;
updateTray();
return true;
} catch {
dialog.showMessageBox({
type: 'error',
message: messages.serverRestartFailed
});
return false;
}
}
async function quitApp() {
await logDesktop('quit app');
app.isQuitting = true;
const stopPromise = stopServerInstance();
const timeoutPromise = new Promise((resolve) => setTimeout(resolve, 5000));
await Promise.race([stopPromise, timeoutPromise]);
app.quit();
setTimeout(() => app.exit(0), 2000);
}
function updateLocale(nextLocale) {
locale = resolveLocale(nextLocale, app.getLocale());
messages = getMessages(locale);
updateTray();
}
async function ensureWritableDirs() {
const defaults = getDefaultDesktopConfig();
const updates = {};
const targets = [
['modelDir', defaults.server.modelDir],
['logDir', defaults.server.logDir],
['configDir', defaults.server.configDir]
];
for (const [key, fallback] of targets) {
const target = desktopConfig.server[key];
if (!target) {
updates[key] = fallback;
continue;
}
try {
await fs.mkdir(target, { recursive: true });
await fs.access(target, fsSync.constants.W_OK);
} catch {
updates[key] = fallback;
}
}
if (Object.keys(updates).length > 0) {
desktopConfig = await saveDesktopConfig({
...desktopConfig,
server: {
...desktopConfig.server,
...updates
}
});
}
}
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');
await fs.mkdir(desktopConfig.server.configDir, { recursive: true });
await logDesktop('desktop starting');
await ensureWritableDirs();
const trayIcon = getTrayIcon();
tray = createTray({
icon: trayIcon,
tooltip: messages.appName,
messages,
statusLabel: getStatusLabel(),
versionLabel: app.getVersion(),
onOpenBrowserUi: () => shell.openExternal(getUiUrl(desktopConfig.server)),
onOpenBrowserDocs: () => shell.openExternal(getDocsUrl(desktopConfig.server)),
onOpenRepo: () => shell.openExternal(repoUrl),
onOpenSettings: () => shell.openExternal(getSettingsUrl(desktopConfig.server)),
onRestart: restartServer,
onOpenModels: () => shell.openPath(desktopConfig.server.modelDir),
onOpenConfig: () => shell.openPath(desktopConfig.server.configDir),
onQuit: quitApp
});
const started = await startServer();
if (!started) {
app.quit();
return;
}
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: getConfigResponse,
applyConfig,
resetConfig,
restartServer: restartAndRespond,
getStatus: async () => ({ status: getServerStatus() }),
openExternal: async (url) => shell.openExternal(url),
openPath: async (targetPath) => shell.openPath(targetPath)
});
app.on('before-quit', (event) => {
if (app.isQuitting) return;
event.preventDefault();
quitApp();
});
updateTray();
}
export function focusMainWindow() {
}