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:
+113
-9
@@ -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()
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user