Initial commit, basic functionality in place.
This commit is contained in:
199
main.js
Normal file
199
main.js
Normal file
@@ -0,0 +1,199 @@
|
||||
'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
|
||||
});
|
||||
Reference in New Issue
Block a user