Refactor code structure for improved readability and maintainability
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
@@ -59,6 +59,24 @@ ipcMain.handle("project:load", async () => {
|
|||||||
return { canceled: false, filePath, data };
|
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]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
app.whenReady().then(() => {
|
app.whenReady().then(() => {
|
||||||
createWindow();
|
createWindow();
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@
|
|||||||
"main.js",
|
"main.js",
|
||||||
"preload.js",
|
"preload.js",
|
||||||
"src/**/*",
|
"src/**/*",
|
||||||
|
"JHArtworks2023_wh.png",
|
||||||
"package.json"
|
"package.json"
|
||||||
],
|
],
|
||||||
"directories": {
|
"directories": {
|
||||||
|
|||||||
+179
-36
@@ -1,6 +1,9 @@
|
|||||||
const { contextBridge, ipcRenderer } = require("electron");
|
const { contextBridge, ipcRenderer } = require("electron");
|
||||||
const crypto = require("node:crypto");
|
const crypto = require("node:crypto");
|
||||||
const dgram = require("node:dgram");
|
const dgram = require("node:dgram");
|
||||||
|
const fs = require("node:fs/promises");
|
||||||
|
const os = require("node:os");
|
||||||
|
const { pathToFileURL } = require("node:url");
|
||||||
const mqtt = require("mqtt");
|
const mqtt = require("mqtt");
|
||||||
const { SerialPort } = require("serialport");
|
const { SerialPort } = require("serialport");
|
||||||
const ModbusRTU = require("modbus-serial");
|
const ModbusRTU = require("modbus-serial");
|
||||||
@@ -44,11 +47,13 @@ function addStatusApi(target, handler) {
|
|||||||
|
|
||||||
const artnet = {
|
const artnet = {
|
||||||
...createStatusEmitter(),
|
...createStatusEmitter(),
|
||||||
sendSocket: dgram.createSocket("udp4"),
|
sendSocket: null,
|
||||||
|
sendSocketSignature: "",
|
||||||
listenSocket: null,
|
listenSocket: null,
|
||||||
listeners: new Set(),
|
listeners: new Set(),
|
||||||
config: {
|
config: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
|
localAddress: "",
|
||||||
host: "255.255.255.255",
|
host: "255.255.255.255",
|
||||||
port: ARTNET_PORT,
|
port: ARTNET_PORT,
|
||||||
broadcast: true,
|
broadcast: true,
|
||||||
@@ -59,9 +64,11 @@ const artnet = {
|
|||||||
|
|
||||||
const sacn = {
|
const sacn = {
|
||||||
...createStatusEmitter(),
|
...createStatusEmitter(),
|
||||||
socket: dgram.createSocket("udp4"),
|
socket: null,
|
||||||
|
socketSignature: "",
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
localAddress: "",
|
||||||
host: "",
|
host: "",
|
||||||
port: SACN_PORT,
|
port: SACN_PORT,
|
||||||
multicast: true,
|
multicast: true,
|
||||||
@@ -73,9 +80,11 @@ const sacn = {
|
|||||||
|
|
||||||
const osc = {
|
const osc = {
|
||||||
...createStatusEmitter(),
|
...createStatusEmitter(),
|
||||||
socket: dgram.createSocket("udp4"),
|
socket: null,
|
||||||
|
socketSignature: "",
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
localAddress: "",
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
port: 8000
|
port: 8000
|
||||||
}
|
}
|
||||||
@@ -87,6 +96,7 @@ const mqttState = {
|
|||||||
connected: false,
|
connected: false,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
localAddress: "",
|
||||||
brokerUrl: "mqtt://127.0.0.1:1883",
|
brokerUrl: "mqtt://127.0.0.1:1883",
|
||||||
username: "",
|
username: "",
|
||||||
password: "",
|
password: "",
|
||||||
@@ -115,6 +125,7 @@ const modbusTcpState = {
|
|||||||
signature: "",
|
signature: "",
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
localAddress: "",
|
||||||
host: "127.0.0.1",
|
host: "127.0.0.1",
|
||||||
port: 502,
|
port: 502,
|
||||||
timeout: 1000
|
timeout: 1000
|
||||||
@@ -144,6 +155,7 @@ const knxState = {
|
|||||||
connected: false,
|
connected: false,
|
||||||
config: {
|
config: {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
interface: "",
|
||||||
ipAddr: "224.0.23.12",
|
ipAddr: "224.0.23.12",
|
||||||
ipPort: KNX_PORT,
|
ipPort: KNX_PORT,
|
||||||
physAddr: "15.15.15",
|
physAddr: "15.15.15",
|
||||||
@@ -154,6 +166,7 @@ const knxState = {
|
|||||||
function normalizeArtNetConfig(nextConfig = {}) {
|
function normalizeArtNetConfig(nextConfig = {}) {
|
||||||
return {
|
return {
|
||||||
enabled: nextConfig.enabled !== false,
|
enabled: nextConfig.enabled !== false,
|
||||||
|
localAddress: String(nextConfig.localAddress || "").trim(),
|
||||||
host: nextConfig.host || "255.255.255.255",
|
host: nextConfig.host || "255.255.255.255",
|
||||||
port: Number.isFinite(Number(nextConfig.port)) ? Number(nextConfig.port) : ARTNET_PORT,
|
port: Number.isFinite(Number(nextConfig.port)) ? Number(nextConfig.port) : ARTNET_PORT,
|
||||||
broadcast: Boolean(nextConfig.broadcast),
|
broadcast: Boolean(nextConfig.broadcast),
|
||||||
@@ -166,6 +179,7 @@ function normalizeArtNetConfig(nextConfig = {}) {
|
|||||||
function normalizeSacnConfig(nextConfig = {}) {
|
function normalizeSacnConfig(nextConfig = {}) {
|
||||||
return {
|
return {
|
||||||
enabled: Boolean(nextConfig.enabled),
|
enabled: Boolean(nextConfig.enabled),
|
||||||
|
localAddress: String(nextConfig.localAddress || "").trim(),
|
||||||
host: String(nextConfig.host || ""),
|
host: String(nextConfig.host || ""),
|
||||||
port: Number.isFinite(Number(nextConfig.port)) ? Number(nextConfig.port) : SACN_PORT,
|
port: Number.isFinite(Number(nextConfig.port)) ? Number(nextConfig.port) : SACN_PORT,
|
||||||
multicast: nextConfig.multicast !== false,
|
multicast: nextConfig.multicast !== false,
|
||||||
@@ -176,6 +190,7 @@ function normalizeSacnConfig(nextConfig = {}) {
|
|||||||
function normalizeOscConfig(nextConfig = {}) {
|
function normalizeOscConfig(nextConfig = {}) {
|
||||||
return {
|
return {
|
||||||
enabled: Boolean(nextConfig.enabled),
|
enabled: Boolean(nextConfig.enabled),
|
||||||
|
localAddress: String(nextConfig.localAddress || "").trim(),
|
||||||
host: nextConfig.host || "127.0.0.1",
|
host: nextConfig.host || "127.0.0.1",
|
||||||
port: Number.isFinite(Number(nextConfig.port)) ? Number(nextConfig.port) : 8000
|
port: Number.isFinite(Number(nextConfig.port)) ? Number(nextConfig.port) : 8000
|
||||||
};
|
};
|
||||||
@@ -184,6 +199,7 @@ function normalizeOscConfig(nextConfig = {}) {
|
|||||||
function normalizeMqttConfig(nextConfig = {}) {
|
function normalizeMqttConfig(nextConfig = {}) {
|
||||||
return {
|
return {
|
||||||
enabled: Boolean(nextConfig.enabled),
|
enabled: Boolean(nextConfig.enabled),
|
||||||
|
localAddress: String(nextConfig.localAddress || "").trim(),
|
||||||
brokerUrl: String(nextConfig.brokerUrl || "").trim() || "mqtt://127.0.0.1:1883",
|
brokerUrl: String(nextConfig.brokerUrl || "").trim() || "mqtt://127.0.0.1:1883",
|
||||||
username: String(nextConfig.username || ""),
|
username: String(nextConfig.username || ""),
|
||||||
password: String(nextConfig.password || ""),
|
password: String(nextConfig.password || ""),
|
||||||
@@ -206,6 +222,7 @@ function normalizeSerialConfig(nextConfig = {}) {
|
|||||||
function normalizeModbusTcpConfig(nextConfig = {}) {
|
function normalizeModbusTcpConfig(nextConfig = {}) {
|
||||||
return {
|
return {
|
||||||
enabled: Boolean(nextConfig.enabled),
|
enabled: Boolean(nextConfig.enabled),
|
||||||
|
localAddress: String(nextConfig.localAddress || "").trim(),
|
||||||
host: String(nextConfig.host || "127.0.0.1"),
|
host: String(nextConfig.host || "127.0.0.1"),
|
||||||
port: Number.isFinite(Number(nextConfig.port)) ? Number(nextConfig.port) : 502,
|
port: Number.isFinite(Number(nextConfig.port)) ? Number(nextConfig.port) : 502,
|
||||||
timeout: Number.isFinite(Number(nextConfig.timeout)) ? Number(nextConfig.timeout) : 1000
|
timeout: Number.isFinite(Number(nextConfig.timeout)) ? Number(nextConfig.timeout) : 1000
|
||||||
@@ -227,6 +244,7 @@ function normalizeModbusRtuConfig(nextConfig = {}) {
|
|||||||
function normalizeKnxConfig(nextConfig = {}) {
|
function normalizeKnxConfig(nextConfig = {}) {
|
||||||
return {
|
return {
|
||||||
enabled: Boolean(nextConfig.enabled),
|
enabled: Boolean(nextConfig.enabled),
|
||||||
|
interface: String(nextConfig.interface || "").trim(),
|
||||||
ipAddr: String(nextConfig.ipAddr || "224.0.23.12"),
|
ipAddr: String(nextConfig.ipAddr || "224.0.23.12"),
|
||||||
ipPort: Number.isFinite(Number(nextConfig.ipPort)) ? Number(nextConfig.ipPort) : KNX_PORT,
|
ipPort: Number.isFinite(Number(nextConfig.ipPort)) ? Number(nextConfig.ipPort) : KNX_PORT,
|
||||||
physAddr: String(nextConfig.physAddr || "15.15.15"),
|
physAddr: String(nextConfig.physAddr || "15.15.15"),
|
||||||
@@ -234,6 +252,79 @@ function normalizeKnxConfig(nextConfig = {}) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function createUdpSocket(target, label) {
|
||||||
|
const socket = dgram.createSocket("udp4");
|
||||||
|
socket.on("error", (error) => {
|
||||||
|
emitStatus(target, `${label} Socket Fehler: ${error.message}`);
|
||||||
|
});
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeUdpSocket(target, key, signatureKey) {
|
||||||
|
if (target[key]) {
|
||||||
|
try {
|
||||||
|
target[key].close();
|
||||||
|
} catch (_error) {
|
||||||
|
// Ignore close races while reconfiguring sockets.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
target[key] = null;
|
||||||
|
target[signatureKey] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureUdpSocket(target, key, signatureKey, label, config, options = {}) {
|
||||||
|
const signature = `${config.localAddress || ""}|${options.broadcast ? "1" : "0"}`;
|
||||||
|
if (target[key] && target[signatureKey] === signature) {
|
||||||
|
return target[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
closeUdpSocket(target, key, signatureKey);
|
||||||
|
const socket = createUdpSocket(target, label);
|
||||||
|
target[key] = socket;
|
||||||
|
target[signatureKey] = signature;
|
||||||
|
|
||||||
|
if (options.broadcast) {
|
||||||
|
socket.setBroadcast(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.localAddress) {
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
let settled = false;
|
||||||
|
const handleError = (error) => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
socket.off("listening", handleListening);
|
||||||
|
reject(error);
|
||||||
|
};
|
||||||
|
const handleListening = () => {
|
||||||
|
if (settled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
settled = true;
|
||||||
|
socket.off("error", handleError);
|
||||||
|
resolve(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
socket.once("error", handleError);
|
||||||
|
socket.once("listening", handleListening);
|
||||||
|
try {
|
||||||
|
socket.bind(0, config.localAddress);
|
||||||
|
} catch (error) {
|
||||||
|
socket.off("error", handleError);
|
||||||
|
socket.off("listening", handleListening);
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
emitStatus(target, "");
|
||||||
|
return socket;
|
||||||
|
}
|
||||||
|
|
||||||
function closeArtNetListener() {
|
function closeArtNetListener() {
|
||||||
if (artnet.listenSocket) {
|
if (artnet.listenSocket) {
|
||||||
try {
|
try {
|
||||||
@@ -273,10 +364,7 @@ function ensureArtNetListener() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
artnet.listenSocket = dgram.createSocket("udp4");
|
artnet.listenSocket = createUdpSocket(artnet, "Art-Net");
|
||||||
artnet.listenSocket.on("error", (error) => {
|
|
||||||
emitStatus(artnet, `Art-Net Listen Fehler: ${error.message}`);
|
|
||||||
});
|
|
||||||
artnet.listenSocket.on("listening", () => {
|
artnet.listenSocket.on("listening", () => {
|
||||||
emitStatus(artnet, "");
|
emitStatus(artnet, "");
|
||||||
});
|
});
|
||||||
@@ -289,7 +377,11 @@ function ensureArtNetListener() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
if (artnet.config.localAddress) {
|
||||||
|
artnet.listenSocket.bind(artnet.config.timecodeListenPort, artnet.config.localAddress);
|
||||||
|
} else {
|
||||||
artnet.listenSocket.bind(artnet.config.timecodeListenPort);
|
artnet.listenSocket.bind(artnet.config.timecodeListenPort);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
emitStatus(artnet, `Art-Net Listen Fehler: ${error.message}`);
|
emitStatus(artnet, `Art-Net Listen Fehler: ${error.message}`);
|
||||||
}
|
}
|
||||||
@@ -297,32 +389,40 @@ function ensureArtNetListener() {
|
|||||||
|
|
||||||
function configureArtNet(nextConfig) {
|
function configureArtNet(nextConfig) {
|
||||||
const previousPort = artnet.config.timecodeListenPort;
|
const previousPort = artnet.config.timecodeListenPort;
|
||||||
|
const previousLocalAddress = artnet.config.localAddress;
|
||||||
artnet.config = normalizeArtNetConfig(nextConfig);
|
artnet.config = normalizeArtNetConfig(nextConfig);
|
||||||
|
|
||||||
try {
|
|
||||||
artnet.sendSocket.setBroadcast(artnet.config.broadcast);
|
|
||||||
} catch (error) {
|
|
||||||
emitStatus(artnet, `Art-Net Konfig Fehler: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!artnet.config.enabled) {
|
if (!artnet.config.enabled) {
|
||||||
closeArtNetListener();
|
closeArtNetListener();
|
||||||
|
closeUdpSocket(artnet, "sendSocket", "sendSocketSignature");
|
||||||
emitStatus(artnet, "");
|
emitStatus(artnet, "");
|
||||||
return artnet.config;
|
return artnet.config;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previousPort !== artnet.config.timecodeListenPort) {
|
if (
|
||||||
|
previousPort !== artnet.config.timecodeListenPort
|
||||||
|
|| previousLocalAddress !== artnet.config.localAddress
|
||||||
|
) {
|
||||||
closeArtNetListener();
|
closeArtNetListener();
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
artnet.sendSocket
|
||||||
|
&& (
|
||||||
|
previousLocalAddress !== artnet.config.localAddress
|
||||||
|
|| Boolean(artnet.config.broadcast) !== artnet.sendSocketSignature.endsWith("|1")
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
closeUdpSocket(artnet, "sendSocket", "sendSocketSignature");
|
||||||
|
}
|
||||||
if (artnet.listeners.size > 0) {
|
if (artnet.listeners.size > 0) {
|
||||||
ensureArtNetListener();
|
ensureArtNetListener();
|
||||||
}
|
}
|
||||||
return artnet.config;
|
return artnet.config;
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendArtNetDmx(payload) {
|
async function sendArtNetDmx(payload) {
|
||||||
if (!artnet.config.enabled) {
|
if (!artnet.config.enabled) {
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const universe = Math.max(0, Math.min(32767, Number(payload.universe) || 0));
|
const universe = Math.max(0, Math.min(32767, Number(payload.universe) || 0));
|
||||||
@@ -344,8 +444,17 @@ function sendArtNetDmx(payload) {
|
|||||||
}
|
}
|
||||||
artnet.sequence = artnet.sequence >= 255 ? 1 : artnet.sequence + 1;
|
artnet.sequence = artnet.sequence >= 255 ? 1 : artnet.sequence + 1;
|
||||||
|
|
||||||
|
const socket = await ensureUdpSocket(
|
||||||
|
artnet,
|
||||||
|
"sendSocket",
|
||||||
|
"sendSocketSignature",
|
||||||
|
"Art-Net",
|
||||||
|
artnet.config,
|
||||||
|
{ broadcast: artnet.config.broadcast }
|
||||||
|
);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
artnet.sendSocket.send(packet, 0, packet.length, artnet.config.port, artnet.config.host, (error) => {
|
socket.send(packet, 0, packet.length, artnet.config.port, artnet.config.host, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
emitStatus(artnet, `Art-Net Send Fehler: ${error.message}`);
|
emitStatus(artnet, `Art-Net Send Fehler: ${error.message}`);
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -358,7 +467,11 @@ function sendArtNetDmx(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function configureOsc(nextConfig) {
|
function configureOsc(nextConfig) {
|
||||||
|
const previousLocalAddress = osc.config.localAddress;
|
||||||
osc.config = normalizeOscConfig(nextConfig);
|
osc.config = normalizeOscConfig(nextConfig);
|
||||||
|
if (previousLocalAddress !== osc.config.localAddress) {
|
||||||
|
closeUdpSocket(osc, "socket", "socketSignature");
|
||||||
|
}
|
||||||
emitStatus(osc, "");
|
emitStatus(osc, "");
|
||||||
return osc.config;
|
return osc.config;
|
||||||
}
|
}
|
||||||
@@ -383,9 +496,9 @@ function encodeOscArgument(value, argType) {
|
|||||||
return { tag: "f", data };
|
return { tag: "f", data };
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendOsc(payload) {
|
async function sendOsc(payload) {
|
||||||
if (!osc.config.enabled) {
|
if (!osc.config.enabled) {
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
const address = String(payload.address || "/time4midi");
|
const address = String(payload.address || "/time4midi");
|
||||||
const normalizedAddress = address.startsWith("/") ? address : `/${address}`;
|
const normalizedAddress = address.startsWith("/") ? address : `/${address}`;
|
||||||
@@ -395,8 +508,9 @@ function sendOsc(payload) {
|
|||||||
encodeOscString(`,${argument.tag}`),
|
encodeOscString(`,${argument.tag}`),
|
||||||
argument.data
|
argument.data
|
||||||
]);
|
]);
|
||||||
|
const socket = await ensureUdpSocket(osc, "socket", "socketSignature", "OSC", osc.config);
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
osc.socket.send(packet, 0, packet.length, osc.config.port, osc.config.host, (error) => {
|
socket.send(packet, 0, packet.length, osc.config.port, osc.config.host, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
emitStatus(osc, `OSC Send Fehler: ${error.message}`);
|
emitStatus(osc, `OSC Send Fehler: ${error.message}`);
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -409,7 +523,11 @@ function sendOsc(payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function configureSacn(nextConfig) {
|
function configureSacn(nextConfig) {
|
||||||
|
const previousLocalAddress = sacn.config.localAddress;
|
||||||
sacn.config = normalizeSacnConfig(nextConfig);
|
sacn.config = normalizeSacnConfig(nextConfig);
|
||||||
|
if (previousLocalAddress !== sacn.config.localAddress) {
|
||||||
|
closeUdpSocket(sacn, "socket", "socketSignature");
|
||||||
|
}
|
||||||
emitStatus(sacn, "");
|
emitStatus(sacn, "");
|
||||||
return sacn.config;
|
return sacn.config;
|
||||||
}
|
}
|
||||||
@@ -437,9 +555,9 @@ function writeFixedString(buffer, offset, size, value) {
|
|||||||
Buffer.from(String(value || ""), "utf8").copy(buffer, offset, 0, size);
|
Buffer.from(String(value || ""), "utf8").copy(buffer, offset, 0, size);
|
||||||
}
|
}
|
||||||
|
|
||||||
function sendSacnDmx(payload) {
|
async function sendSacnDmx(payload) {
|
||||||
if (!sacn.config.enabled) {
|
if (!sacn.config.enabled) {
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const universe = clampSacnUniverse(payload.universe);
|
const universe = clampSacnUniverse(payload.universe);
|
||||||
@@ -480,9 +598,10 @@ function sendSacnDmx(payload) {
|
|||||||
|
|
||||||
sacn.sequence = sacn.sequence >= 255 ? 1 : sacn.sequence + 1;
|
sacn.sequence = sacn.sequence >= 255 ? 1 : sacn.sequence + 1;
|
||||||
const host = sacnDestination(universe);
|
const host = sacnDestination(universe);
|
||||||
|
const socket = await ensureUdpSocket(sacn, "socket", "socketSignature", "sACN", sacn.config);
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
sacn.socket.send(packet, 0, packet.length, sacn.config.port, host, (error) => {
|
socket.send(packet, 0, packet.length, sacn.config.port, host, (error) => {
|
||||||
if (error) {
|
if (error) {
|
||||||
emitStatus(sacn, `sACN Send Fehler: ${error.message}`);
|
emitStatus(sacn, `sACN Send Fehler: ${error.message}`);
|
||||||
reject(error);
|
reject(error);
|
||||||
@@ -520,6 +639,7 @@ function configureMqtt(nextConfig) {
|
|||||||
if (mqttState.config.clientId) options.clientId = mqttState.config.clientId;
|
if (mqttState.config.clientId) options.clientId = mqttState.config.clientId;
|
||||||
if (mqttState.config.username) options.username = mqttState.config.username;
|
if (mqttState.config.username) options.username = mqttState.config.username;
|
||||||
if (mqttState.config.password) options.password = mqttState.config.password;
|
if (mqttState.config.password) options.password = mqttState.config.password;
|
||||||
|
if (mqttState.config.localAddress) options.localAddress = mqttState.config.localAddress;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mqttState.client = mqtt.connect(mqttState.config.brokerUrl, options);
|
mqttState.client = mqtt.connect(mqttState.config.brokerUrl, options);
|
||||||
@@ -656,6 +776,28 @@ async function listSerialPorts() {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function listNetworkInterfaces() {
|
||||||
|
const interfaces = os.networkInterfaces();
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
Object.entries(interfaces).forEach(([name, entries]) => {
|
||||||
|
(entries || []).forEach((entry) => {
|
||||||
|
const family = typeof entry.family === "string" ? entry.family : String(entry.family);
|
||||||
|
if (family !== "IPv4" || entry.internal || !entry.address) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
items.push({
|
||||||
|
id: entry.address,
|
||||||
|
name: `${name} - ${entry.address}`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
items.sort((left, right) => left.name.localeCompare(right.name));
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
async function disconnectModbusClient(state) {
|
async function disconnectModbusClient(state) {
|
||||||
if (state.client?.isOpen) {
|
if (state.client?.isOpen) {
|
||||||
try {
|
try {
|
||||||
@@ -693,7 +835,8 @@ async function ensureModbusTcpConnected() {
|
|||||||
await disconnectModbusClient(modbusTcpState);
|
await disconnectModbusClient(modbusTcpState);
|
||||||
await modbusTcpState.client.connectTCP(modbusTcpState.config.host, {
|
await modbusTcpState.client.connectTCP(modbusTcpState.config.host, {
|
||||||
port: modbusTcpState.config.port,
|
port: modbusTcpState.config.port,
|
||||||
timeout: modbusTcpState.config.timeout
|
timeout: modbusTcpState.config.timeout,
|
||||||
|
localAddress: modbusTcpState.config.localAddress || undefined
|
||||||
});
|
});
|
||||||
modbusTcpState.client.setTimeout(modbusTcpState.config.timeout);
|
modbusTcpState.client.setTimeout(modbusTcpState.config.timeout);
|
||||||
modbusTcpState.connected = true;
|
modbusTcpState.connected = true;
|
||||||
@@ -806,6 +949,7 @@ async function ensureKnxConnection() {
|
|||||||
const connection = knx.Connection({
|
const connection = knx.Connection({
|
||||||
ipAddr: knxState.config.ipAddr,
|
ipAddr: knxState.config.ipAddr,
|
||||||
ipPort: knxState.config.ipPort,
|
ipPort: knxState.config.ipPort,
|
||||||
|
interface: knxState.config.interface || undefined,
|
||||||
physAddr: knxState.config.physAddr,
|
physAddr: knxState.config.physAddr,
|
||||||
forceTunneling: knxState.config.forceTunneling,
|
forceTunneling: knxState.config.forceTunneling,
|
||||||
handlers: {
|
handlers: {
|
||||||
@@ -871,18 +1015,6 @@ async function sendKnx(payload) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
artnet.sendSocket.on("error", (error) => {
|
|
||||||
emitStatus(artnet, `Art-Net Socket Fehler: ${error.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
osc.socket.on("error", (error) => {
|
|
||||||
emitStatus(osc, `OSC Socket Fehler: ${error.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
sacn.socket.on("error", (error) => {
|
|
||||||
emitStatus(sacn, `sACN Socket Fehler: ${error.message}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
configureMqtt(mqttState.config);
|
configureMqtt(mqttState.config);
|
||||||
|
|
||||||
contextBridge.exposeInMainWorld("time4midi", {
|
contextBridge.exposeInMainWorld("time4midi", {
|
||||||
@@ -890,6 +1022,17 @@ contextBridge.exposeInMainWorld("time4midi", {
|
|||||||
save: (payload) => ipcRenderer.invoke("project:save", payload),
|
save: (payload) => ipcRenderer.invoke("project:save", payload),
|
||||||
load: () => ipcRenderer.invoke("project:load")
|
load: () => ipcRenderer.invoke("project:load")
|
||||||
},
|
},
|
||||||
|
audio: {
|
||||||
|
pickFile: () => ipcRenderer.invoke("audio:pickFile"),
|
||||||
|
readFile: async (filePath) => {
|
||||||
|
const buffer = await fs.readFile(filePath);
|
||||||
|
return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
|
||||||
|
},
|
||||||
|
fileUrl: (filePath) => pathToFileURL(filePath).href
|
||||||
|
},
|
||||||
|
network: {
|
||||||
|
listInterfaces: () => listNetworkInterfaces()
|
||||||
|
},
|
||||||
artnet: {
|
artnet: {
|
||||||
configure: (config) => configureArtNet(config),
|
configure: (config) => configureArtNet(config),
|
||||||
sendDmx: (payload) => sendArtNetDmx(payload),
|
sendDmx: (payload) => sendArtNetDmx(payload),
|
||||||
|
|||||||
+852
-39
File diff suppressed because it is too large
Load Diff
+156
-46
@@ -11,12 +11,10 @@
|
|||||||
<aside class="sidebar">
|
<aside class="sidebar">
|
||||||
<div class="panel brand-panel">
|
<div class="panel brand-panel">
|
||||||
<div class="brand-lockup">
|
<div class="brand-lockup">
|
||||||
<div class="logo-mark" aria-hidden="true">
|
<img class="brand-logo" src="../JHArtworks2023_wh.png" alt="J.H. Artworks Logo">
|
||||||
<span></span>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<h1>Time4Midi</h1>
|
<h1>Time4Midi</h1>
|
||||||
<div class="brand-credit">a J.H. Artworks Product</div>
|
<div class="brand-credit">by J.H. Artworks</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -62,6 +60,7 @@
|
|||||||
<button id="addKnxTrackButton" class="ghost">+ KNX</button>
|
<button id="addKnxTrackButton" class="ghost">+ KNX</button>
|
||||||
<button id="addOscTrackButton" class="ghost">+ OSC</button>
|
<button id="addOscTrackButton" class="ghost">+ OSC</button>
|
||||||
<button id="addMqttTrackButton" class="ghost">+ MQTT</button>
|
<button id="addMqttTrackButton" class="ghost">+ MQTT</button>
|
||||||
|
<button id="addAudioTrackButton" class="ghost">+ Audio</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="trackList" class="track-list"></div>
|
<div id="trackList" class="track-list"></div>
|
||||||
@@ -88,6 +87,7 @@
|
|||||||
<strong id="transportStatusLabel">Idle</strong>
|
<strong id="transportStatusLabel">Idle</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="transport-controls">
|
<div class="transport-controls">
|
||||||
|
<button id="openSettingsButton" class="ghost">Einstellungen</button>
|
||||||
<button id="playButton">Play</button>
|
<button id="playButton">Play</button>
|
||||||
<button id="pauseButton" class="ghost">Pause</button>
|
<button id="pauseButton" class="ghost">Pause</button>
|
||||||
<button id="stopButton" class="ghost">Stop</button>
|
<button id="stopButton" class="ghost">Stop</button>
|
||||||
@@ -112,14 +112,37 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
<aside class="inspector" id="inspectorPane">
|
<div id="settingsModal" class="settings-modal" aria-hidden="true">
|
||||||
<button id="inspectorToggleButton" class="inspector-toggle ghost" title="Rechte Leiste ein-/ausklappen">
|
<div id="settingsBackdrop" class="settings-backdrop"></div>
|
||||||
>
|
<section class="settings-dialog panel" role="dialog" aria-modal="true" aria-labelledby="settingsTitle">
|
||||||
</button>
|
<div class="settings-dialog-header">
|
||||||
<div class="panel">
|
<div>
|
||||||
|
<div class="eyebrow">Setup</div>
|
||||||
|
<h2 id="settingsTitle">Einstellungen</h2>
|
||||||
|
</div>
|
||||||
|
<div class="panel-actions">
|
||||||
|
<button id="closeSettingsButton" class="ghost" type="button">Schliessen</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-tabbar" role="tablist" aria-label="Einstellungen">
|
||||||
|
<button class="settings-tab active" type="button" data-settings-tab="routing">Routing</button>
|
||||||
|
<button class="settings-tab" type="button" data-settings-tab="timeline">Timeline</button>
|
||||||
|
<button class="settings-tab" type="button" data-settings-tab="selection">Auswahl</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="settings-content">
|
||||||
|
<div class="settings-tab-panel active" data-settings-panel="routing">
|
||||||
|
<div class="settings-stack">
|
||||||
|
<div class="info-card">
|
||||||
|
Adapter = lokale Netzwerkkarte bzw. lokale IP. Leer bedeutet automatische Auswahl durch das Betriebssystem.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2>Routing</h2>
|
<h2>MIDI</h2>
|
||||||
</div>
|
</div>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>MIDI Input</span>
|
<span>MIDI Input</span>
|
||||||
@@ -129,14 +152,34 @@
|
|||||||
<span>MIDI Output</span>
|
<span>MIDI Output</span>
|
||||||
<select id="midiOutputSelect"></select>
|
<select id="midiOutputSelect"></select>
|
||||||
</label>
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Audio</h2>
|
||||||
|
</div>
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Art-Net Ziel</span>
|
<span>Audio Output</span>
|
||||||
<input id="artnetHostInput" type="text" placeholder="255.255.255.255">
|
<select id="audioOutputSelect"></select>
|
||||||
</label>
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Art-Net</h2>
|
||||||
|
</div>
|
||||||
<label class="field checkbox-row">
|
<label class="field checkbox-row">
|
||||||
<span>Art-Net aktiv</span>
|
<span>Art-Net aktiv</span>
|
||||||
<input id="artnetEnabledInput" type="checkbox">
|
<input id="artnetEnabledInput" type="checkbox">
|
||||||
</label>
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Netzwerkadapter</span>
|
||||||
|
<select id="artnetAdapterSelect"></select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Art-Net Ziel</span>
|
||||||
|
<input id="artnetHostInput" type="text" placeholder="255.255.255.255">
|
||||||
|
</label>
|
||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Port</span>
|
<span>Port</span>
|
||||||
@@ -157,19 +200,19 @@
|
|||||||
<input id="artnetBroadcastInput" type="checkbox">
|
<input id="artnetBroadcastInput" type="checkbox">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-grid">
|
</section>
|
||||||
<label class="field">
|
|
||||||
<span>OSC Host</span>
|
<section class="settings-section">
|
||||||
<input id="oscHostInput" type="text" placeholder="127.0.0.1">
|
<div class="panel-header">
|
||||||
</label>
|
<h2>sACN</h2>
|
||||||
<label class="field">
|
|
||||||
<span>OSC Port</span>
|
|
||||||
<input id="oscPortInput" type="number" min="1" max="65535">
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
<label class="field checkbox-row">
|
<label class="field checkbox-row">
|
||||||
<span>OSC aktiv</span>
|
<span>sACN aktiv</span>
|
||||||
<input id="oscEnabledInput" type="checkbox">
|
<input id="sacnEnabledInput" type="checkbox">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Netzwerkadapter</span>
|
||||||
|
<select id="sacnAdapterSelect"></select>
|
||||||
</label>
|
</label>
|
||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@@ -181,10 +224,6 @@
|
|||||||
<input id="sacnPortInput" type="number" min="1" max="65535">
|
<input id="sacnPortInput" type="number" min="1" max="65535">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="field checkbox-row">
|
|
||||||
<span>sACN aktiv</span>
|
|
||||||
<input id="sacnEnabledInput" type="checkbox">
|
|
||||||
</label>
|
|
||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>sACN Source</span>
|
<span>sACN Source</span>
|
||||||
@@ -195,14 +234,48 @@
|
|||||||
<input id="sacnMulticastInput" type="checkbox">
|
<input id="sacnMulticastInput" type="checkbox">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="field">
|
</section>
|
||||||
<span>MQTT Broker</span>
|
|
||||||
<input id="mqttBrokerInput" type="text" placeholder="mqtt://127.0.0.1:1883">
|
<section class="settings-section">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>OSC</h2>
|
||||||
|
</div>
|
||||||
|
<label class="field checkbox-row">
|
||||||
|
<span>OSC aktiv</span>
|
||||||
|
<input id="oscEnabledInput" type="checkbox">
|
||||||
</label>
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Netzwerkadapter</span>
|
||||||
|
<select id="oscAdapterSelect"></select>
|
||||||
|
</label>
|
||||||
|
<div class="field-grid">
|
||||||
|
<label class="field">
|
||||||
|
<span>OSC Host</span>
|
||||||
|
<input id="oscHostInput" type="text" placeholder="127.0.0.1">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>OSC Port</span>
|
||||||
|
<input id="oscPortInput" type="number" min="1" max="65535">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>MQTT</h2>
|
||||||
|
</div>
|
||||||
<label class="field checkbox-row">
|
<label class="field checkbox-row">
|
||||||
<span>MQTT aktiv</span>
|
<span>MQTT aktiv</span>
|
||||||
<input id="mqttEnabledInput" type="checkbox">
|
<input id="mqttEnabledInput" type="checkbox">
|
||||||
</label>
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Netzwerkadapter</span>
|
||||||
|
<select id="mqttAdapterSelect"></select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>MQTT Broker</span>
|
||||||
|
<input id="mqttBrokerInput" type="text" placeholder="mqtt://127.0.0.1:1883">
|
||||||
|
</label>
|
||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>MQTT User</span>
|
<span>MQTT User</span>
|
||||||
@@ -217,6 +290,16 @@
|
|||||||
<span>MQTT Client ID</span>
|
<span>MQTT Client ID</span>
|
||||||
<input id="mqttClientIdInput" type="text" placeholder="time4midi-desktop">
|
<input id="mqttClientIdInput" type="text" placeholder="time4midi-desktop">
|
||||||
</label>
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Serial</h2>
|
||||||
|
</div>
|
||||||
|
<label class="field checkbox-row">
|
||||||
|
<span>Serial aktiv</span>
|
||||||
|
<input id="serialEnabledInput" type="checkbox">
|
||||||
|
</label>
|
||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Serial Port</span>
|
<span>Serial Port</span>
|
||||||
@@ -244,9 +327,19 @@
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Modbus TCP</h2>
|
||||||
|
</div>
|
||||||
<label class="field checkbox-row">
|
<label class="field checkbox-row">
|
||||||
<span>Serial aktiv</span>
|
<span>Modbus TCP aktiv</span>
|
||||||
<input id="serialEnabledInput" type="checkbox">
|
<input id="modbusTcpEnabledInput" type="checkbox">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Netzwerkadapter</span>
|
||||||
|
<select id="modbusTcpAdapterSelect"></select>
|
||||||
</label>
|
</label>
|
||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@@ -258,9 +351,15 @@
|
|||||||
<input id="modbusTcpPortInput" type="number" min="1" max="65535">
|
<input id="modbusTcpPortInput" type="number" min="1" max="65535">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="settings-section">
|
||||||
|
<div class="panel-header">
|
||||||
|
<h2>Modbus RTU</h2>
|
||||||
|
</div>
|
||||||
<label class="field checkbox-row">
|
<label class="field checkbox-row">
|
||||||
<span>Modbus TCP aktiv</span>
|
<span>Modbus RTU aktiv</span>
|
||||||
<input id="modbusTcpEnabledInput" type="checkbox">
|
<input id="modbusRtuEnabledInput" type="checkbox">
|
||||||
</label>
|
</label>
|
||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
@@ -272,7 +371,6 @@
|
|||||||
<input id="modbusRtuBaudInput" type="number" min="300" max="921600">
|
<input id="modbusRtuBaudInput" type="number" min="300" max="921600">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-grid">
|
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>Parity</span>
|
<span>Parity</span>
|
||||||
<select id="modbusRtuParitySelect">
|
<select id="modbusRtuParitySelect">
|
||||||
@@ -281,11 +379,20 @@
|
|||||||
<option value="odd">Odd</option>
|
<option value="odd">Odd</option>
|
||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
<label class="field checkbox-row">
|
</section>
|
||||||
<span>Modbus RTU aktiv</span>
|
|
||||||
<input id="modbusRtuEnabledInput" type="checkbox">
|
<section class="settings-section">
|
||||||
</label>
|
<div class="panel-header">
|
||||||
|
<h2>KNX</h2>
|
||||||
</div>
|
</div>
|
||||||
|
<label class="field checkbox-row">
|
||||||
|
<span>KNX aktiv</span>
|
||||||
|
<input id="knxEnabledInput" type="checkbox">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Netzwerkadapter</span>
|
||||||
|
<select id="knxAdapterSelect"></select>
|
||||||
|
</label>
|
||||||
<div class="field-grid">
|
<div class="field-grid">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>KNX IP</span>
|
<span>KNX IP</span>
|
||||||
@@ -306,26 +413,29 @@
|
|||||||
<input id="knxTunnelingInput" type="checkbox">
|
<input id="knxTunnelingInput" type="checkbox">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<label class="field checkbox-row">
|
</section>
|
||||||
<span>KNX aktiv</span>
|
</div>
|
||||||
<input id="knxEnabledInput" type="checkbox">
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="settings-tab-panel" data-settings-panel="timeline">
|
||||||
|
<section class="settings-section">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2>Timeline</h2>
|
<h2>Timeline</h2>
|
||||||
</div>
|
</div>
|
||||||
<div id="timelineInspector"></div>
|
<div id="timelineInspector"></div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="panel">
|
<div class="settings-tab-panel" data-settings-panel="selection">
|
||||||
|
<section class="settings-section">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
<h2>Auswahl</h2>
|
<h2>Auswahl</h2>
|
||||||
</div>
|
</div>
|
||||||
<div id="selectionInspector"></div>
|
<div id="selectionInspector"></div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</div>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="./app.js"></script>
|
<script src="./app.js"></script>
|
||||||
|
|||||||
+224
-139
@@ -7,13 +7,14 @@
|
|||||||
--panel-border: rgba(255, 255, 255, 0.08);
|
--panel-border: rgba(255, 255, 255, 0.08);
|
||||||
--text: #ecf2ff;
|
--text: #ecf2ff;
|
||||||
--muted: #a4b2cc;
|
--muted: #a4b2cc;
|
||||||
--accent: #6ef2c6;
|
--accent: #6ec2f2;
|
||||||
--accent-strong: #17c98d;
|
--accent-strong: #1782c9;
|
||||||
--danger: #ff7e87;
|
--danger: #ff7e87;
|
||||||
--grid: rgba(255, 255, 255, 0.08);
|
--grid: rgba(255, 255, 255, 0.08);
|
||||||
--midi: #61c8ff;
|
--midi: #61c8ff;
|
||||||
--artnet: #ffb35c;
|
--artnet: #ffb35c;
|
||||||
--shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
--shadow: 0 20px 60px rgba(0, 0, 0, 0.35);
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -25,9 +26,9 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
background:
|
background:
|
||||||
radial-gradient(circle at top left, rgba(110, 242, 198, 0.14), transparent 32%),
|
radial-gradient(circle at top left, rgba(110, 194, 242, 0.18), transparent 34%),
|
||||||
radial-gradient(circle at top right, rgba(97, 200, 255, 0.14), transparent 28%),
|
radial-gradient(circle at top right, rgba(23, 130, 201, 0.16), transparent 28%),
|
||||||
linear-gradient(180deg, #0a0d13 0%, #111722 100%);
|
linear-gradient(180deg, #09111b 0%, #101a28 100%);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family: "Trebuchet MS", "Segoe UI Variable Text", "Aptos", sans-serif;
|
font-family: "Trebuchet MS", "Segoe UI Variable Text", "Aptos", sans-serif;
|
||||||
}
|
}
|
||||||
@@ -44,17 +45,18 @@ select {
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
background: linear-gradient(180deg, rgba(110, 242, 198, 0.2), rgba(23, 201, 141, 0.15));
|
background: linear-gradient(180deg, rgba(110, 194, 242, 0.2), rgba(23, 130, 201, 0.15));
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
padding: 0.65rem 0.95rem;
|
padding: 0.5rem 0.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: transform 120ms ease, border-color 120ms ease, background 120ms ease;
|
transition: transform 120ms ease, border-color 120ms ease, background 120ms ease;
|
||||||
|
font-size: 0.92rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
transform: translateY(-1px);
|
transform: translateY(-1px);
|
||||||
border-color: rgba(110, 242, 198, 0.45);
|
border-color: rgba(110, 222, 242, 0.45);
|
||||||
}
|
}
|
||||||
|
|
||||||
button.ghost {
|
button.ghost {
|
||||||
@@ -65,68 +67,35 @@ input,
|
|||||||
select {
|
select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||||
border-radius: 12px;
|
border-radius: 10px;
|
||||||
padding: 0.7rem 0.8rem;
|
padding: 0.56rem 0.7rem;
|
||||||
background: rgba(0, 0, 0, 0.22);
|
background: rgba(0, 0, 0, 0.22);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
|
font-size: 0.92rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-shell {
|
.app-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 332px minmax(0, 1fr) 292px;
|
grid-template-columns: 286px minmax(0, 1fr);
|
||||||
gap: 18px;
|
gap: 12px;
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
padding: 18px;
|
padding: 12px;
|
||||||
}
|
|
||||||
|
|
||||||
.app-shell.inspector-collapsed {
|
|
||||||
grid-template-columns: 332px minmax(0, 1fr) 54px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar,
|
.sidebar,
|
||||||
.workspace,
|
.workspace {
|
||||||
.inspector {
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 18px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel {
|
.panel {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border: 1px solid var(--panel-border);
|
border: 1px solid var(--panel-border);
|
||||||
border-radius: 24px;
|
border-radius: 18px;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
backdrop-filter: blur(18px);
|
backdrop-filter: blur(18px);
|
||||||
padding: 18px;
|
padding: 14px;
|
||||||
}
|
|
||||||
|
|
||||||
.inspector {
|
|
||||||
position: relative;
|
|
||||||
gap: 12px;
|
|
||||||
min-width: 0;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding-right: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inspector .panel {
|
|
||||||
padding: 12px;
|
|
||||||
border-radius: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.inspector-toggle {
|
|
||||||
align-self: flex-end;
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
padding: 0;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-shell.inspector-collapsed .inspector .panel {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app-shell.inspector-collapsed .inspector {
|
|
||||||
align-items: center;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-panel {
|
.brand-panel {
|
||||||
@@ -140,41 +109,12 @@ select {
|
|||||||
gap: 14px;
|
gap: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.logo-mark {
|
.brand-logo {
|
||||||
width: 64px;
|
width: 96px;
|
||||||
height: 64px;
|
max-width: 36%;
|
||||||
border-radius: 18px;
|
height: auto;
|
||||||
position: relative;
|
object-fit: contain;
|
||||||
background:
|
filter: drop-shadow(0 10px 24px rgba(0, 0, 0, 0.35));
|
||||||
linear-gradient(160deg, rgba(110, 242, 198, 0.3), rgba(97, 200, 255, 0.18)),
|
|
||||||
rgba(255, 255, 255, 0.04);
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-mark::before,
|
|
||||||
.logo-mark::after,
|
|
||||||
.logo-mark span {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
bottom: 12px;
|
|
||||||
width: 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: linear-gradient(180deg, #6ef2c6, #61c8ff);
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-mark::before {
|
|
||||||
left: 16px;
|
|
||||||
height: 22px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-mark span {
|
|
||||||
left: 28px;
|
|
||||||
height: 34px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-mark::after {
|
|
||||||
left: 40px;
|
|
||||||
height: 46px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-panel::after {
|
.brand-panel::after {
|
||||||
@@ -183,14 +123,14 @@ select {
|
|||||||
inset: auto -80px -100px auto;
|
inset: auto -80px -100px auto;
|
||||||
width: 220px;
|
width: 220px;
|
||||||
height: 220px;
|
height: 220px;
|
||||||
background: radial-gradient(circle, rgba(110, 242, 198, 0.24), transparent 68%);
|
background: radial-gradient(circle, rgba(110, 194, 242, 0.24), transparent 68%);
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.eyebrow {
|
.eyebrow {
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.18em;
|
||||||
font-size: 0.72rem;
|
font-size: 0.64rem;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,25 +140,26 @@ h2 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 1.6rem;
|
font-size: 1.24rem;
|
||||||
line-height: 1.15;
|
line-height: 1.15;
|
||||||
}
|
}
|
||||||
|
|
||||||
.brand-credit {
|
.brand-credit {
|
||||||
margin-top: 0.35rem;
|
margin-top: 0.2rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 0.72rem;
|
font-size: 0.62rem;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.02rem;
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
p {
|
p {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
line-height: 1.5;
|
line-height: 1.4;
|
||||||
|
font-size: 0.9rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-header,
|
.panel-header,
|
||||||
@@ -227,32 +168,32 @@ p {
|
|||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.panel-actions,
|
.panel-actions,
|
||||||
.transport-controls,
|
.transport-controls,
|
||||||
.editor-hints {
|
.editor-hints {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 6px;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-hints span,
|
.editor-hints span,
|
||||||
.meta-row span,
|
.meta-row span,
|
||||||
.transport-label {
|
.transport-label {
|
||||||
font-size: 0.78rem;
|
font-size: 0.7rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field,
|
.field,
|
||||||
.field-grid {
|
.field-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.45rem;
|
gap: 0.32rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field {
|
.field {
|
||||||
margin-top: 12px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-row {
|
.checkbox-row {
|
||||||
@@ -261,18 +202,18 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.checkbox-row input {
|
.checkbox-row input {
|
||||||
width: 20px;
|
width: 16px;
|
||||||
height: 20px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.field span {
|
.field span {
|
||||||
font-size: 0.82rem;
|
font-size: 0.72rem;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-grid {
|
.field-grid {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
gap: 12px;
|
gap: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.checkbox {
|
.checkbox {
|
||||||
@@ -280,14 +221,14 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.checkbox input {
|
.checkbox input {
|
||||||
width: 20px;
|
width: 16px;
|
||||||
height: 20px;
|
height: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.meta-row {
|
.meta-row {
|
||||||
margin-top: 12px;
|
margin-top: 8px;
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.3rem;
|
gap: 0.18rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.path-label {
|
.path-label {
|
||||||
@@ -303,16 +244,16 @@ p {
|
|||||||
#timelineInspector,
|
#timelineInspector,
|
||||||
#selectionInspector {
|
#selectionInspector {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
margin-top: 12px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-card,
|
.timeline-card,
|
||||||
.track-card,
|
.track-card,
|
||||||
.info-card {
|
.info-card {
|
||||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
border-radius: 18px;
|
border-radius: 14px;
|
||||||
padding: 12px 14px;
|
padding: 10px 12px;
|
||||||
background: rgba(255, 255, 255, 0.03);
|
background: rgba(255, 255, 255, 0.03);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,16 +263,28 @@ p {
|
|||||||
|
|
||||||
.timeline-card.active,
|
.timeline-card.active,
|
||||||
.track-card.active {
|
.track-card.active {
|
||||||
border-color: rgba(110, 242, 198, 0.52);
|
border-color: rgba(110, 194, 242, 0.52);
|
||||||
background: rgba(110, 242, 198, 0.09);
|
background: rgba(110, 194, 242, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-card.drag-source {
|
||||||
|
opacity: 0.62;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-card.drag-before {
|
||||||
|
box-shadow: inset 0 3px 0 rgba(110, 194, 242, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-card.drag-after {
|
||||||
|
box-shadow: inset 0 -3px 0 rgba(110, 194, 242, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-pill {
|
.track-pill {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
padding: 0.2rem 0.55rem;
|
padding: 0.14rem 0.44rem;
|
||||||
font-size: 0.72rem;
|
font-size: 0.62rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
color: #081118;
|
color: #081118;
|
||||||
}
|
}
|
||||||
@@ -345,7 +298,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.track-pill.midicc {
|
.track-pill.midicc {
|
||||||
background: #8fd86f;
|
background: #78cfff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-pill.osc {
|
.track-pill.osc {
|
||||||
@@ -356,12 +309,16 @@ p {
|
|||||||
background: #9ca8ff;
|
background: #9ca8ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.track-pill.audio {
|
||||||
|
background: #8ee8c8;
|
||||||
|
}
|
||||||
|
|
||||||
.track-pill.serial {
|
.track-pill.serial {
|
||||||
background: #ff9e6b;
|
background: #ff9e6b;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-pill.modbus {
|
.track-pill.modbus {
|
||||||
background: #63e0d3;
|
background: #6fc2de;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-pill.knx {
|
.track-pill.knx {
|
||||||
@@ -374,7 +331,7 @@ p {
|
|||||||
|
|
||||||
.track-card-top {
|
.track-card-top {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 8px;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,14 +341,38 @@ p {
|
|||||||
|
|
||||||
.track-card-actions {
|
.track-card-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.track-action {
|
.track-action {
|
||||||
min-width: 34px;
|
min-width: 28px;
|
||||||
height: 34px;
|
height: 28px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 10px;
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-grip {
|
||||||
|
position: relative;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-grip:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.track-grip::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 9px;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(236, 242, 255, 0.72);
|
||||||
|
box-shadow:
|
||||||
|
0 6px 0 rgba(236, 242, 255, 0.72),
|
||||||
|
6px 0 0 rgba(236, 242, 255, 0.72),
|
||||||
|
6px 6px 0 rgba(236, 242, 255, 0.72);
|
||||||
}
|
}
|
||||||
|
|
||||||
.workspace {
|
.workspace {
|
||||||
@@ -399,7 +380,7 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.transport-bar {
|
.transport-bar {
|
||||||
padding: 16px 18px;
|
padding: 12px 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.transport-main {
|
.transport-main {
|
||||||
@@ -408,11 +389,11 @@ p {
|
|||||||
|
|
||||||
.transport-block {
|
.transport-block {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 4px;
|
gap: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#timecodeLabel {
|
#timecodeLabel {
|
||||||
font-size: 1.55rem;
|
font-size: 1.2rem;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -424,10 +405,10 @@ p {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.canvas-frame {
|
.canvas-frame {
|
||||||
margin-top: 14px;
|
margin-top: 10px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
border-radius: 20px;
|
border-radius: 16px;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent),
|
linear-gradient(180deg, rgba(255, 255, 255, 0.03), transparent),
|
||||||
rgba(5, 8, 12, 0.62);
|
rgba(5, 8, 12, 0.62);
|
||||||
@@ -443,31 +424,122 @@ p {
|
|||||||
|
|
||||||
.empty-state {
|
.empty-state {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 0.92rem;
|
font-size: 0.82rem;
|
||||||
line-height: 1.5;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inspector-grid {
|
.settings-modal {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
transition: opacity 180ms ease;
|
||||||
|
z-index: 40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-modal.open {
|
||||||
|
opacity: 1;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(5, 9, 16, 0.72);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-dialog {
|
||||||
|
position: relative;
|
||||||
|
width: min(1080px, calc(100vw - 32px));
|
||||||
|
max-height: calc(100vh - 32px);
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(110, 194, 242, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-dialog-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tabbar {
|
||||||
|
display: flex;
|
||||||
|
gap: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab {
|
||||||
|
min-width: 92px;
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab.active {
|
||||||
|
border-color: rgba(110, 194, 242, 0.48);
|
||||||
|
background: linear-gradient(180deg, rgba(110, 194, 242, 0.22), rgba(23, 130, 201, 0.18));
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-content {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab-panel {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-tab-panel.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-stack {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-section {
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 12px;
|
||||||
|
background: rgba(255, 255, 255, 0.03);
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card {
|
||||||
|
border: 1px solid rgba(110, 194, 242, 0.14);
|
||||||
|
border-radius: 14px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
background: linear-gradient(180deg, rgba(110, 194, 242, 0.08), rgba(255, 255, 255, 0.02));
|
||||||
|
color: var(--muted);
|
||||||
|
line-height: 1.4;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-card strong {
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inspector-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.section-label {
|
.section-label {
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 0.8rem;
|
font-size: 0.68rem;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.14em;
|
letter-spacing: 0.14em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 1320px) {
|
@media (max-width: 1320px) {
|
||||||
.app-shell {
|
.app-shell {
|
||||||
grid-template-columns: 320px minmax(0, 1fr);
|
grid-template-columns: 270px minmax(0, 1fr);
|
||||||
}
|
|
||||||
|
|
||||||
.inspector {
|
|
||||||
grid-column: 1 / -1;
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -476,7 +548,20 @@ p {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inspector {
|
.transport-main,
|
||||||
|
.editor-header,
|
||||||
|
.settings-dialog-header {
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-dialog {
|
||||||
|
width: calc(100vw - 20px);
|
||||||
|
max-height: calc(100vh - 20px);
|
||||||
|
padding: 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user