200 lines
5.9 KiB
JavaScript
200 lines
5.9 KiB
JavaScript
'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
|
||
});
|