feat: enhance inspector and settings UI with new components and styles

- Added inspector panel for timeline and selection with corresponding sections.
- Updated settings tab structure to include MIDI, Audio, Art-Net, sACN, OSC, MQTT, Serial, Modbus TCP, Modbus RTU, and KNX configurations.
- Improved layout and styles for better responsiveness and usability.
- Introduced new CSS classes for truncating text and managing overflow.
- Adjusted grid layouts for app shell and inspector to accommodate new elements.
This commit is contained in:
jhartworks
2026-06-17 09:01:25 +02:00
parent d282eb8b0b
commit 8d94f9b76b
157 changed files with 607691 additions and 59211 deletions
+113 -9
View File
@@ -3,9 +3,9 @@ const crypto = require("node:crypto");
const dgram = require("node:dgram");
const fs = require("node:fs/promises");
const os = require("node:os");
const path = require("node:path");
const { pathToFileURL } = require("node:url");
const mqtt = require("mqtt");
const { SerialPort } = require("serialport");
const ModbusRTU = require("modbus-serial");
const knx = require("knx");
@@ -15,6 +15,30 @@ const KNX_PORT = 3671;
const ARTNET_HEADER = Buffer.from("Art-Net\0", "ascii");
const ARTNET_OPCODE_DMX = 0x5000;
const ARTNET_OPCODE_TIMECODE = 0x9700;
const MAPPING_LIBRARY_STORE_FILE = "mapping-libraries.json";
function loadOptionalDependency(packageName) {
try {
return {
module: require(packageName),
error: null
};
} catch (error) {
console.error(`[time4midi] Optional dependency "${packageName}" failed to load:`, error);
return {
module: null,
error
};
}
}
function dependencyError(label, dependency) {
const detail = dependency.error?.message || "Modul nicht verfuegbar";
return new Error(`${label} Modul nicht verfuegbar: ${detail}`);
}
const serialportDependency = loadOptionalDependency("serialport");
const SerialPort = serialportDependency.module?.SerialPort || null;
function createStatusEmitter() {
return {
@@ -283,11 +307,9 @@ async function ensureUdpSocket(target, key, signatureKey, label, config, options
target[key] = socket;
target[signatureKey] = signature;
if (options.broadcast) {
socket.setBroadcast(true);
}
if (!config.localAddress) {
const bindAddress = config.localAddress || "0.0.0.0";
const requiresBinding = Boolean(config.localAddress || options.broadcast);
if (!requiresBinding) {
return socket;
}
@@ -313,7 +335,7 @@ async function ensureUdpSocket(target, key, signatureKey, label, config, options
socket.once("error", handleError);
socket.once("listening", handleListening);
try {
socket.bind(0, config.localAddress);
socket.bind(0, bindAddress);
} catch (error) {
socket.off("error", handleError);
socket.off("listening", handleListening);
@@ -321,6 +343,10 @@ async function ensureUdpSocket(target, key, signatureKey, label, config, options
}
});
if (options.broadcast) {
socket.setBroadcast(true);
}
emitStatus(target, "");
return socket;
}
@@ -710,6 +736,9 @@ async function configureSerial(nextConfig) {
}
async function ensureSerialPortOpen() {
if (!SerialPort) {
throw dependencyError("Serial", serialportDependency);
}
if (!serialState.config.enabled) {
throw new Error("Serial deaktiviert");
}
@@ -768,6 +797,9 @@ async function sendSerial(payload) {
}
async function listSerialPorts() {
if (!SerialPort) {
throw dependencyError("Serial", serialportDependency);
}
const ports = await SerialPort.list();
return ports.map((port) => ({
path: port.path,
@@ -789,7 +821,9 @@ async function listNetworkInterfaces() {
items.push({
id: entry.address,
name: `${name} - ${entry.address}`
name: `${name} - ${entry.address}`,
interfaceName: name,
address: entry.address
});
});
});
@@ -798,6 +832,70 @@ async function listNetworkInterfaces() {
return items;
}
async function readJsonFileSafe(filePath, fallbackValue) {
try {
const raw = await fs.readFile(filePath, "utf8");
return JSON.parse(raw);
} catch (error) {
if (error?.code === "ENOENT") {
return fallbackValue;
}
throw error;
}
}
async function writeJsonFile(filePath, data) {
await fs.mkdir(path.dirname(filePath), { recursive: true });
await fs.writeFile(filePath, JSON.stringify(data, null, 2), "utf8");
}
async function getMappingLibraryStorePath() {
const userDataPath = await ipcRenderer.invoke("app:getUserDataPath");
return path.join(userDataPath, MAPPING_LIBRARY_STORE_FILE);
}
async function loadStoredMappingLibraries() {
const filePath = await getMappingLibraryStorePath();
const data = await readJsonFileSafe(filePath, null);
return {
filePath,
data
};
}
async function saveStoredMappingLibraries(data) {
const filePath = await getMappingLibraryStorePath();
await writeJsonFile(filePath, data);
return { filePath };
}
async function importMappingLibrary(defaultPath) {
const result = await ipcRenderer.invoke("mappingLibrary:pickLoadFile", { defaultPath });
if (result?.canceled || !result?.filePath) {
return { canceled: true };
}
const data = await readJsonFileSafe(result.filePath, null);
return {
canceled: false,
filePath: result.filePath,
data
};
}
async function exportMappingLibrary(defaultPath, data) {
const result = await ipcRenderer.invoke("mappingLibrary:pickSaveFile", { defaultPath });
if (result?.canceled || !result?.filePath) {
return { canceled: true };
}
await writeJsonFile(result.filePath, data);
return {
canceled: false,
filePath: result.filePath
};
}
async function disconnectModbusClient(state) {
if (state.client?.isOpen) {
try {
@@ -877,7 +975,7 @@ async function writeModbus(client, payload) {
if (payload.registerType === "coil") {
await client.writeCoil(address, value >= 1);
} else {
await client.writeRegister(address, Math.max(0, Math.min(65535, Math.round(value))));
await client.writeRegister(address, Math.max(0, Math.min(32767, Math.round(value))));
}
}
@@ -1030,6 +1128,12 @@ contextBridge.exposeInMainWorld("time4midi", {
},
fileUrl: (filePath) => pathToFileURL(filePath).href
},
mappingLibraries: {
loadStore: () => loadStoredMappingLibraries(),
saveStore: (data) => saveStoredMappingLibraries(data),
importFile: (defaultPath) => importMappingLibrary(defaultPath),
exportFile: (defaultPath, data) => exportMappingLibrary(defaultPath, data)
},
network: {
listInterfaces: () => listNetworkInterfaces()
},