Files

200 lines
5.9 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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
});