348 lines
8.5 KiB
JavaScript
348 lines
8.5 KiB
JavaScript
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: 680,
|
|
height: 520,
|
|
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: 1760,
|
|
height: 1020,
|
|
minWidth: 1320,
|
|
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();
|
|
}
|
|
});
|