Files
Time4Midi/main.js
T
jhartworks b5c6d33c2d feat: add diagnostics page and functionality, enhance transport settings in index.html
- Introduced a new diagnostics page (diagnostics.html) with real-time monitoring for Art-Net, sACN, MQTT, and KNX.
- Added JavaScript functionality (diagnostics.js) to handle diagnostics data and render updates.
- Enhanced index.html with new transport settings including timeline length, beat snap options, and diagnostics button.
- Updated styles.css to accommodate new diagnostics layout and transport settings.
- Added splash screen (splash.html) for improved user experience during startup.
- Included new assets: Time4Midi.psd and updated executable files for version 0.3.6.
2026-06-17 14:57:38 +02:00

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: 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();
}
});