const { app, BrowserWindow, dialog, ipcMain } = require("electron"); const fs = require("node:fs/promises"); const path = require("node:path"); app.commandLine.appendSwitch("enable-blink-features", "WebMidi"); let mainWindow = null; let diagnosticsWindow = null; let splashWindow = null; const diagnosticsState = { artnet: { universes: {} }, sacn: { universes: {} }, mqtt: { topics: {} }, knx: { groupAddresses: {} }, recentEvents: [] }; function rememberRecentEvent(entry) { diagnosticsState.recentEvents.unshift(entry); diagnosticsState.recentEvents = diagnosticsState.recentEvents.slice(0, 200); } function sameChannelValues(left, right) { if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length) { return false; } for (let index = 0; index < left.length; index += 1) { if (Number(left[index] || 0) !== Number(right[index] || 0)) { return false; } } return true; } function updateDmxDiagnostics(transport, record) { const safeTransport = transport === "artnet" ? "artnet" : "sacn"; const previous = diagnosticsState[safeTransport].universes[String(record.universe)]; const nextValues = Array.isArray(record.values) ? record.values.slice(0, 512) : []; const changed = !previous || !sameChannelValues(previous.values, nextValues); diagnosticsState[safeTransport].universes[String(record.universe)] = { universe: Number(record.universe) || 0, values: nextValues, updatedAt: record.timestamp }; if (changed) { rememberRecentEvent({ kind: "dmx-frame", transport: safeTransport, universe: Number(record.universe) || 0, activeChannels: nextValues.filter((value) => Number(value) > 0).length, timestamp: record.timestamp }); } } function updateMqttDiagnostics(record) { diagnosticsState.mqtt.topics[String(record.topic || "")] = { topic: String(record.topic || ""), value: String(record.value ?? ""), updatedAt: record.timestamp }; rememberRecentEvent({ kind: "mqtt-publish", topic: String(record.topic || ""), value: String(record.value ?? ""), timestamp: record.timestamp }); } function updateKnxDiagnostics(record) { const key = `${record.groupAddress || ""}:${record.dpt || ""}`; diagnosticsState.knx.groupAddresses[key] = { groupAddress: String(record.groupAddress || ""), dpt: String(record.dpt || ""), value: record.value, updatedAt: record.timestamp }; rememberRecentEvent({ kind: "knx-write", groupAddress: String(record.groupAddress || ""), dpt: String(record.dpt || ""), value: record.value, timestamp: record.timestamp }); } function applyDiagnosticsRecord(record) { if (!record || typeof record !== "object") { return; } if (record.kind === "dmx-frame") { updateDmxDiagnostics(record.transport, record); } else if (record.kind === "mqtt-publish") { updateMqttDiagnostics(record); } else if (record.kind === "knx-write") { updateKnxDiagnostics(record); } } function sendDiagnosticsUpdate(record) { if (!diagnosticsWindow || diagnosticsWindow.isDestroyed()) { return; } diagnosticsWindow.webContents.send("diagnostics:update", record); } function closeSplashWindow() { if (!splashWindow || splashWindow.isDestroyed()) { splashWindow = null; return; } splashWindow.close(); splashWindow = null; } function createSplashWindow() { if (splashWindow && !splashWindow.isDestroyed()) { return splashWindow; } splashWindow = new BrowserWindow({ width: 520, height: 420, frame: false, resizable: false, minimizable: false, maximizable: false, movable: true, show: true, center: true, backgroundColor: "#0f1117", icon: path.join(__dirname, "time4midi.png"), webPreferences: { contextIsolation: true, sandbox: false } }); splashWindow.on("closed", () => { splashWindow = null; }); splashWindow.loadFile(path.join(__dirname, "src", "splash.html")); return splashWindow; } function createWindow() { mainWindow = new BrowserWindow({ width: 1600, height: 980, minWidth: 1200, minHeight: 760, backgroundColor: "#0f1117", icon: path.join(__dirname, "time4midi.png"), show: false, webPreferences: { contextIsolation: true, sandbox: false, preload: path.join(__dirname, "preload.js") } }); mainWindow.on("closed", () => { mainWindow = null; }); mainWindow.once("ready-to-show", () => { setTimeout(() => { closeSplashWindow(); if (mainWindow && !mainWindow.isDestroyed()) { mainWindow.show(); } }, 650); }); mainWindow.loadFile(path.join(__dirname, "src", "index.html")); } function createDiagnosticsWindow() { if (diagnosticsWindow && !diagnosticsWindow.isDestroyed()) { diagnosticsWindow.focus(); return diagnosticsWindow; } diagnosticsWindow = new BrowserWindow({ width: 1100, height: 820, minWidth: 900, minHeight: 640, backgroundColor: "#0f1117", icon: path.join(__dirname, "time4midi.png"), title: "Time4Midi Diagnose", webPreferences: { contextIsolation: true, sandbox: false, preload: path.join(__dirname, "preload.js") } }); diagnosticsWindow.on("closed", () => { diagnosticsWindow = null; }); diagnosticsWindow.loadFile(path.join(__dirname, "src", "diagnostics.html")); return diagnosticsWindow; } ipcMain.handle("project:save", async (_event, payload) => { if (payload?.filePath) { await fs.writeFile(payload.filePath, payload.data, "utf8"); return { canceled: false, filePath: payload.filePath }; } const result = await dialog.showSaveDialog({ defaultPath: payload?.defaultPath || "time4midi-project.json", filters: [ { name: "Time4Midi Project", extensions: ["json"] } ] }); if (result.canceled || !result.filePath) { return { canceled: true }; } await fs.writeFile(result.filePath, payload.data, "utf8"); return { canceled: false, filePath: result.filePath }; }); ipcMain.handle("project:load", async () => { const result = await dialog.showOpenDialog({ properties: ["openFile"], filters: [ { name: "Time4Midi Project", extensions: ["json"] } ] }); if (result.canceled || result.filePaths.length === 0) { return { canceled: true }; } const filePath = result.filePaths[0]; const data = await fs.readFile(filePath, "utf8"); return { canceled: false, filePath, data }; }); ipcMain.handle("audio:pickFile", async () => { const result = await dialog.showOpenDialog({ properties: ["openFile"], filters: [ { name: "Audio", extensions: ["mp3", "m4a", "flac", "wma"] } ] }); if (result.canceled || result.filePaths.length === 0) { return { canceled: true }; } return { canceled: false, filePath: result.filePaths[0] }; }); ipcMain.handle("app:getUserDataPath", () => { return app.getPath("userData"); }); ipcMain.handle("mappingLibrary:pickLoadFile", async (_event, payload) => { const result = await dialog.showOpenDialog({ properties: ["openFile"], defaultPath: payload?.defaultPath || "time4midi-mappings.json", filters: [ { name: "Time4Midi Mapping", extensions: ["json"] } ] }); if (result.canceled || result.filePaths.length === 0) { return { canceled: true }; } return { canceled: false, filePath: result.filePaths[0] }; }); ipcMain.handle("mappingLibrary:pickSaveFile", async (_event, payload) => { const result = await dialog.showSaveDialog({ defaultPath: payload?.defaultPath || "time4midi-mappings.json", filters: [ { name: "Time4Midi Mapping", extensions: ["json"] } ] }); if (result.canceled || !result.filePath) { return { canceled: true }; } return { canceled: false, filePath: result.filePath }; }); ipcMain.handle("diagnostics:openWindow", () => { createDiagnosticsWindow(); return { ok: true }; }); ipcMain.handle("diagnostics:getSnapshot", () => { return diagnosticsState; }); ipcMain.on("diagnostics:record", (_event, record) => { const normalizedRecord = { ...record, timestamp: Number(record?.timestamp) || Date.now() }; applyDiagnosticsRecord(normalizedRecord); sendDiagnosticsUpdate(normalizedRecord); }); app.whenReady().then(() => { createSplashWindow(); createWindow(); app.on("activate", () => { if (!mainWindow || mainWindow.isDestroyed()) { createSplashWindow(); createWindow(); } }); }); app.on("window-all-closed", () => { closeSplashWindow(); if (process.platform !== "darwin") { app.quit(); } });