'use strict'; const { app, BrowserWindow, Tray, Menu, ipcMain, nativeImage } = require('electron'); const path = require('path'); const fs = require('fs'); // --------------------------------------------------------------------------- // CLI argument parsing // --------------------------------------------------------------------------- function parseCliArgs() { const args = process.argv.slice(2); const overrides = {}; for (let i = 0; i < args.length; i++) { switch (args[i]) { case '--host': if (args[i + 1]) overrides.host = args[++i]; break; case '--port': { const p = parseInt(args[++i], 10); if (!isNaN(p)) overrides.port = p; break; } case '--volume': { const v = parseInt(args[++i], 10); if (!isNaN(v)) overrides.volume = Math.min(100, Math.max(0, v)); break; } } } return overrides; } // --------------------------------------------------------------------------- // Config persistence // --------------------------------------------------------------------------- const CONFIG_DEFAULTS = { host: 'localhost', port: 8888, volume: 80 }; function getConfigPath() { return path.join(app.getPath('userData'), 'config.json'); } function loadConfig() { try { const raw = fs.readFileSync(getConfigPath(), 'utf8'); return Object.assign({}, CONFIG_DEFAULTS, JSON.parse(raw)); } catch { return Object.assign({}, CONFIG_DEFAULTS); } } function writeConfig(cfg) { try { fs.writeFileSync(getConfigPath(), JSON.stringify(cfg, null, 2), 'utf8'); } catch (e) { console.error('Failed to write config:', e); } } // --------------------------------------------------------------------------- // App state // --------------------------------------------------------------------------- let mainWindow = null; let tray = null; const cliArgs = parseCliArgs(); // savedConfig loaded in app.whenReady() — app.getPath('userData') not available before then let savedConfig = { ...CONFIG_DEFAULTS }; // Effective runtime config = saved + CLI overrides (CLI does not persist). // Only include cliArgs keys that are explicitly set (not undefined). function getRuntimeConfig() { const definedCli = Object.fromEntries( Object.entries(cliArgs).filter(([, v]) => v !== undefined) ); return Object.assign({}, savedConfig, definedCli); } // Show and focus the main window from any context. function showWindow() { if (mainWindow) { mainWindow.show(); mainWindow.focus(); } } // --------------------------------------------------------------------------- // Tray icon — generated programmatically from raw RGBA pixels (16×16 cyan circle) // --------------------------------------------------------------------------- function createTrayIcon() { // Build a 16×16 RGBA buffer with a filled circle in --cyan (#12c2e9) const SIZE = 16; const buf = Buffer.alloc(SIZE * SIZE * 4, 0); // fully transparent const cx = 7.5, cy = 7.5, r = 6.5; for (let y = 0; y < SIZE; y++) { for (let x = 0; x < SIZE; x++) { const dx = x - cx, dy = y - cy; if (dx * dx + dy * dy <= r * r) { const i = (y * SIZE + x) * 4; buf[i] = 0x12; // R buf[i + 1] = 0xc2; // G buf[i + 2] = 0xe9; // B buf[i + 3] = 0xff; // A } } } return nativeImage.createFromBuffer(buf, { width: SIZE, height: SIZE }); } // --------------------------------------------------------------------------- // Window creation // --------------------------------------------------------------------------- function createWindow() { mainWindow = new BrowserWindow({ width: 360, height: 540, resizable: true, webPreferences: { preload: path.join(__dirname, 'preload.js'), contextIsolation: true, nodeIntegration: false, }, title: 'Kokoro Widget', backgroundColor: '#0d1220', }); mainWindow.setMenuBarVisibility(false); mainWindow.loadFile(path.join(__dirname, 'renderer', 'index.html')); // Hide to tray on close instead of quitting mainWindow.on('close', (event) => { if (!app.isQuitting) { event.preventDefault(); mainWindow.hide(); } }); } // --------------------------------------------------------------------------- // Tray creation // --------------------------------------------------------------------------- function createTray() { const icon = createTrayIcon(); tray = new Tray(icon); tray.setToolTip('Kokoro Widget'); const contextMenu = Menu.buildFromTemplate([ { label: 'Show', click: showWindow, }, { type: 'separator' }, { label: 'Quit', click: () => { app.isQuitting = true; app.quit(); }, }, ]); tray.setContextMenu(contextMenu); tray.on('double-click', showWindow); // On macOS, single click shows the window if (process.platform === 'darwin') { tray.on('click', showWindow); } } // --------------------------------------------------------------------------- // IPC handlers // --------------------------------------------------------------------------- ipcMain.handle('get-config', () => { return getRuntimeConfig(); }); ipcMain.handle('save-config', (_event, newConfig) => { // Merge new values into savedConfig savedConfig = Object.assign({}, savedConfig, newConfig); writeConfig(savedConfig); // Return effective runtime config (saved + CLI overrides) return getRuntimeConfig(); }); // --------------------------------------------------------------------------- // App lifecycle // --------------------------------------------------------------------------- app.whenReady().then(() => { savedConfig = loadConfig(); // safe here — app.getPath('userData') now available createWindow(); createTray(); app.on('activate', showWindow); }); // Do NOT quit when all windows are closed — tray keeps the app alive app.on('window-all-closed', () => { // intentionally empty — tray icon keeps the process running });