815 lines
36 KiB
JavaScript
815 lines
36 KiB
JavaScript
document.addEventListener("DOMContentLoaded", () => {
|
||
// per-slider tag storage
|
||
const sliderIds = ["slider0", "slider1", "slider2"];
|
||
let tags = { slider0: [], slider1: [], slider2: [] };
|
||
|
||
// slider values (existing logic) - start at 0
|
||
const sliderValues = [0, 0, 0];
|
||
|
||
// receive volume helper logs from main early so they're always visible in DevTools
|
||
if (window.api && window.api.onVolumeLog) {
|
||
let _lastVolumeLogAt = 0;
|
||
const _volumeLogThrottleMs = 200; // show at most 5 logs/sec to keep DevTools responsive
|
||
window.api.onVolumeLog((msg) => {
|
||
try {
|
||
const now = Date.now();
|
||
if (now - _lastVolumeLogAt < _volumeLogThrottleMs) return;
|
||
_lastVolumeLogAt = now;
|
||
console.log("Volume log:", msg);
|
||
} catch (e) { /* ignore */ }
|
||
});
|
||
}
|
||
|
||
// --- helper: apply device-specific view mapping (show/hide device-content blocks) ---
|
||
function applyDeviceView(device) {
|
||
try {
|
||
const key = device ? String(device).trim().toLowerCase() : "";
|
||
// show matching device-content blocks, hide the rest
|
||
document.querySelectorAll(".device-content").forEach(el => {
|
||
const elDev = (el.getAttribute("data-device") || "").toLowerCase();
|
||
if (elDev === key) {
|
||
el.style.display = ""; // let CSS decide (block/inline)
|
||
} else {
|
||
el.style.display = "none";
|
||
}
|
||
});
|
||
// also store current device as a body data attribute for optional CSS usage
|
||
if (key) document.body.setAttribute("data-device", key);
|
||
else document.body.removeAttribute("data-device");
|
||
} catch (e) { /* ignore */ }
|
||
}
|
||
|
||
// load config (tags per slider)
|
||
window.api.loadTags().then((data) => {
|
||
tags.slider0 = (data && Array.isArray(data.slider0)) ? data.slider0.slice() : [];
|
||
tags.slider1 = (data && Array.isArray(data.slider1)) ? data.slider1.slice() : [];
|
||
tags.slider2 = (data && Array.isArray(data.slider2)) ? data.slider2.slice() : [];
|
||
// initial render for each slider's tag container
|
||
sliderIds.forEach(id => renderTags(id));
|
||
|
||
// set initial device selection if present
|
||
try {
|
||
const deviceSelect = document.getElementById("device-select");
|
||
if (deviceSelect && data && typeof data.device === "string") {
|
||
deviceSelect.value = data.device;
|
||
// APPLY saved device-specific content visibility
|
||
applyDeviceView(deviceSelect.value);
|
||
}
|
||
|
||
// populate COM port select and restore previously saved selection
|
||
const comSelect = document.getElementById("com-port-select");
|
||
const comRefreshBtn = document.getElementById("com-refresh-btn");
|
||
async function populateComPorts(selected) {
|
||
if (!window.api || !window.api.listSerialPorts) return;
|
||
if (!comSelect) return;
|
||
// show searching placeholder
|
||
comSelect.innerHTML = '<option value="">Searching...</option>';
|
||
try {
|
||
const ports = await window.api.listSerialPorts(); // can be array of strings or {value,label}
|
||
console.log("listSerialPorts ->", ports);
|
||
// reset select, always keep explicit "(none)"
|
||
comSelect.innerHTML = '<option value="">(none)</option>';
|
||
if (!ports || ports.length === 0) {
|
||
// leave only (none)
|
||
if (selected) {
|
||
// if user had a saved selection that didn't appear, re-add it so it can be restored
|
||
const opt = document.createElement("option");
|
||
opt.value = String(selected);
|
||
opt.textContent = String(selected);
|
||
comSelect.appendChild(opt);
|
||
comSelect.value = selected;
|
||
}
|
||
return;
|
||
}
|
||
ports.forEach(p => {
|
||
let value, label;
|
||
if (typeof p === "string") {
|
||
value = label = p;
|
||
} else if (p && typeof p === "object") {
|
||
value = String(p.value);
|
||
label = p.label || p.value;
|
||
} else {
|
||
return;
|
||
}
|
||
const opt = document.createElement("option");
|
||
opt.value = value;
|
||
opt.textContent = label;
|
||
comSelect.appendChild(opt);
|
||
});
|
||
if (selected) {
|
||
// restore if present; otherwise keep default
|
||
try { comSelect.value = selected; } catch (e) { /* ignore */ }
|
||
}
|
||
} catch (err) {
|
||
console.warn("populateComPorts failed", err);
|
||
// show fallback
|
||
comSelect.innerHTML = '<option value="">(none)</option>';
|
||
}
|
||
}
|
||
if (comRefreshBtn) {
|
||
comRefreshBtn.addEventListener("click", () => {
|
||
populateComPorts(comSelect ? comSelect.value : undefined);
|
||
}, { passive: true });
|
||
}
|
||
// restore saved COM selection if present
|
||
if (data && typeof data.comPort === "string") {
|
||
populateComPorts(data.comPort);
|
||
} else {
|
||
populateComPorts();
|
||
}
|
||
// wire change to save configuration
|
||
if (comSelect) {
|
||
comSelect.addEventListener("change", () => saveConfig());
|
||
}
|
||
|
||
// --- NEW: initialize the three checkboxes from loaded config and wire them to save ---
|
||
const autoEl = document.getElementById("autoconnect");
|
||
const startEl = document.getElementById("startup-with-system");
|
||
const trayEl = document.getElementById("close-to-tray");
|
||
if (autoEl) autoEl.checked = !!(data && data.autoconnect);
|
||
if (startEl) startEl.checked = !!(data && data.startupWithSystem);
|
||
if (trayEl) trayEl.checked = !!(data && data.closeToTray);
|
||
|
||
if (autoEl) autoEl.addEventListener("change", () => saveConfig());
|
||
if (startEl) startEl.addEventListener("change", () => saveConfig());
|
||
if (trayEl) trayEl.addEventListener("change", () => saveConfig());
|
||
|
||
// --- RESTORE customisation values into color inputs (if present) ---
|
||
try {
|
||
const cust = data && data.customisation ? data.customisation : {};
|
||
const colorIds = ['cust-bg','cust-sidebar','cust-button','cust-pot','cust-accent'];
|
||
colorIds.forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
if (cust && typeof cust[id] === 'string') {
|
||
try { el.value = cust[id]; } catch (e) { /* ignore */ }
|
||
}
|
||
});
|
||
// apply any restored customisation immediately
|
||
if (window.applyCustomisation && typeof window.applyCustomisation === 'function') {
|
||
const payload = {};
|
||
if (cust['cust-bg']) payload.background = cust['cust-bg'];
|
||
if (cust['cust-sidebar']) payload.sidebar = cust['cust-sidebar'];
|
||
if (cust['cust-button']) payload.button = cust['cust-button'];
|
||
if (cust['cust-pot']) payload.potentiometer = cust['cust-pot'];
|
||
if (cust['cust-accent']) payload.accent = cust['cust-accent'];
|
||
applyCustomisation(payload);
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
|
||
// existing device select is already wired above
|
||
} catch (err) { /* ignore */ }
|
||
});
|
||
|
||
function saveConfig() {
|
||
// persist tags under slider0/slider1/slider2 and current device selection
|
||
const deviceEl = document.getElementById("device-select");
|
||
const deviceVal = deviceEl ? String(deviceEl.value) : undefined;
|
||
const comEl = document.getElementById("com-port-select");
|
||
const comVal = comEl ? String(comEl.value) : undefined;
|
||
|
||
// read new checkbox states
|
||
const autoEl = document.getElementById("autoconnect");
|
||
const startEl = document.getElementById("startup-with-system");
|
||
const trayEl = document.getElementById("close-to-tray");
|
||
const autoVal = autoEl ? Boolean(autoEl.checked) : false;
|
||
const startVal = startEl ? Boolean(startEl.checked) : false;
|
||
const trayVal = trayEl ? Boolean(trayEl.checked) : false;
|
||
|
||
const payload = {
|
||
slider0: tags.slider0,
|
||
slider1: tags.slider1,
|
||
slider2: tags.slider2
|
||
};
|
||
if (deviceVal !== undefined) payload.device = deviceVal;
|
||
if (comVal !== undefined) payload.comPort = comVal;
|
||
// include new checkbox values
|
||
payload.autoconnect = autoVal;
|
||
payload.startupWithSystem = startVal;
|
||
payload.closeToTray = trayVal;
|
||
|
||
// --- NEW: read customisation inputs and include in payload ---
|
||
try {
|
||
const colorIds = ['cust-bg','cust-sidebar','cust-button','cust-pot','cust-accent'];
|
||
const cust = {};
|
||
colorIds.forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (!el) return;
|
||
// store by element id for straightforward restore (keeps mapping simple)
|
||
cust[id] = String(el.value || el.getAttribute('data-default') || '');
|
||
});
|
||
// only include if any value present
|
||
if (Object.keys(cust).length > 0) payload.customisation = cust;
|
||
} catch (e) { /* ignore */ }
|
||
|
||
window.api.saveTags(payload);
|
||
}
|
||
// wire device dropdown to save on change
|
||
(function wireDeviceSelect() {
|
||
const deviceEl = document.getElementById("device-select");
|
||
if (!deviceEl) return;
|
||
deviceEl.addEventListener("change", () => {
|
||
// update visible device-specific fragments (do not switch tabs)
|
||
applyDeviceView(deviceEl.value);
|
||
saveConfig();
|
||
});
|
||
})();
|
||
|
||
function addTag(sliderId, value) {
|
||
// support passing an array or a comma-separated string
|
||
if (!value) return;
|
||
const arr = tags[sliderId] || [];
|
||
|
||
// normalize incoming values into an array of trimmed non-empty strings
|
||
let items = [];
|
||
if (Array.isArray(value)) {
|
||
items = value.map(v => String(v).trim()).filter(v => v !== "");
|
||
} else {
|
||
items = String(value)
|
||
.split(",")
|
||
.map(s => s.trim())
|
||
.filter(s => s !== "");
|
||
}
|
||
|
||
if (items.length === 0) return;
|
||
|
||
// insert each item at the beginning, preserving the order from left-to-right
|
||
// (so we reverse before unshifting)
|
||
items.slice().reverse().forEach(item => {
|
||
if (!arr.includes(item)) {
|
||
arr.unshift(item);
|
||
}
|
||
});
|
||
|
||
tags[sliderId] = arr;
|
||
renderTags(sliderId);
|
||
saveConfig();
|
||
}
|
||
|
||
function removeTag(sliderId, index) {
|
||
const arr = tags[sliderId] || [];
|
||
arr.splice(index, 1);
|
||
tags[sliderId] = arr;
|
||
renderTags(sliderId);
|
||
saveConfig();
|
||
}
|
||
|
||
function renderTags(sliderId) {
|
||
const container = document.getElementById(`tag-container-${sliderId}`);
|
||
if (!container) return;
|
||
container.innerHTML = "";
|
||
const arr = tags[sliderId] || [];
|
||
arr.forEach((tag, idx) => {
|
||
const tagElement = document.createElement("div");
|
||
tagElement.classList.add("tag");
|
||
|
||
// create remove button on the LEFT (now includes a separator after the X)
|
||
const removeBtn = document.createElement("span");
|
||
removeBtn.classList.add("remove-tag");
|
||
removeBtn.setAttribute("data-slider", sliderId);
|
||
removeBtn.setAttribute("data-index", String(idx));
|
||
// keep the button content minimal; separator will come from CSS ::after
|
||
removeBtn.textContent = "\u00D7"; // "×"
|
||
|
||
// create text node (safe) for the tag label
|
||
const textSpan = document.createElement("span");
|
||
textSpan.classList.add("tag-text");
|
||
textSpan.textContent = tag;
|
||
|
||
// append remove button first, then text
|
||
tagElement.appendChild(removeBtn);
|
||
tagElement.appendChild(textSpan);
|
||
|
||
container.appendChild(tagElement);
|
||
});
|
||
|
||
// wire remove buttons
|
||
container.querySelectorAll(".remove-tag").forEach(btn => {
|
||
btn.addEventListener("click", (e) => {
|
||
const s = e.target.getAttribute("data-slider");
|
||
const i = Number(e.target.getAttribute("data-index"));
|
||
removeTag(s, i);
|
||
});
|
||
});
|
||
}
|
||
|
||
// wire per-slider inputs/buttons (improved: supports Enter/comma split, blur)
|
||
sliderIds.forEach((id) => {
|
||
const input = document.getElementById(`tag-input-${id}`);
|
||
const addBtn = document.getElementById(`tag-add-${id}`);
|
||
|
||
if (input) {
|
||
// add on Enter or comma key (prevents the comma char from being inserted)
|
||
input.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter" || e.key === ",") {
|
||
e.preventDefault();
|
||
addTag(id, input.value);
|
||
input.value = "";
|
||
}
|
||
});
|
||
|
||
// also add remaining text when the field loses focus
|
||
input.addEventListener("blur", () => {
|
||
if (input.value && input.value.trim() !== "") {
|
||
addTag(id, input.value);
|
||
input.value = "";
|
||
}
|
||
});
|
||
}
|
||
|
||
if (addBtn) {
|
||
addBtn.addEventListener("click", () => {
|
||
if (input && input.value && input.value.trim() !== "") {
|
||
addTag(id, input.value);
|
||
input.value = "";
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// --- existing slider logic (wire vertical sliders) ---
|
||
// keep original slider wiring so UI updates values
|
||
sliderIds.forEach((id, idx) => {
|
||
const slider = document.getElementById(id);
|
||
const valueBubble = document.getElementById(`${id}-value-bubble`);
|
||
if (slider && valueBubble) {
|
||
// helper: update bubble text and position
|
||
function updateBubble(val) {
|
||
const v = Number(val);
|
||
sliderValues[idx] = v;
|
||
if (valueBubble) valueBubble.textContent = String(v);
|
||
|
||
// position bubble next to the slider thumb using bounding rects and bubble height
|
||
try {
|
||
const min = Number(slider.min) || 0;
|
||
const max = Number(slider.max) || 100;
|
||
const pct = (v - min) / (max - min || 100);
|
||
|
||
// estimate thumb height (matches CSS .vertical-slider thumb)
|
||
const thumbH = 28;
|
||
|
||
// extension in pixels to lengthen the bubble rail beyond actual thumb travel
|
||
const ext = 4; // reduced by 4px as requested
|
||
|
||
const sliderRect = slider.getBoundingClientRect();
|
||
const parentRect = slider.parentElement.getBoundingClientRect();
|
||
|
||
// compute new min/max for thumb center including extension
|
||
const centerMin = (thumbH / 2) - ext;
|
||
const centerMax = (sliderRect.height - thumbH / 2) + ext;
|
||
const available = Math.max(0, centerMax - centerMin);
|
||
|
||
// thumb center within the slider rect including extension
|
||
const yWithinSliderRect = centerMin + available * (1 - pct);
|
||
|
||
// convert to parent (.slider-controls) coordinate and center the bubble by subtracting half its height
|
||
const bubbleHalf = valueBubble.offsetHeight ? (valueBubble.offsetHeight / 2) : 0;
|
||
const top = (sliderRect.top - parentRect.top) + yWithinSliderRect - bubbleHalf;
|
||
|
||
// Use transform for positioning (GPU-accelerated). Keep top at 0 in CSS.
|
||
valueBubble.style.transform = `translateY(${Math.round(top)}px)`;
|
||
} catch (err) {
|
||
// ignore position errors
|
||
}
|
||
// removed per-input console.log to keep UI responsive
|
||
}
|
||
|
||
slider.addEventListener("input", (e) => {
|
||
updateBubble(e.target.value);
|
||
});
|
||
|
||
// set initial position/value once DOM is ready
|
||
requestAnimationFrame(() => {
|
||
updateBubble(slider.value);
|
||
requestAnimationFrame(() => updateBubble(slider.value));
|
||
setTimeout(() => updateBubble(slider.value), 60);
|
||
});
|
||
}
|
||
});
|
||
|
||
// recompute bubble positions on resize (keeps them aligned if layout changes)
|
||
window.addEventListener("resize", () => {
|
||
sliderIds.forEach((id, idx) => {
|
||
const slider = document.getElementById(id);
|
||
const valueBubble = document.getElementById(`${id}-value-bubble`);
|
||
if (!slider || !valueBubble) return;
|
||
|
||
try {
|
||
const min = Number(slider.min) || 0;
|
||
const max = Number(slider.max) || 100;
|
||
const v = Number(slider.value);
|
||
const pct = (v - min) / (max - min || 100);
|
||
|
||
const thumbH = 28;
|
||
const ext = 4; // keep same extension as updateBubble (reduced)
|
||
const sliderRect = slider.getBoundingClientRect();
|
||
const parentRect = slider.parentElement.getBoundingClientRect();
|
||
|
||
const centerMin = (thumbH / 2) - ext;
|
||
const centerMax = (sliderRect.height - thumbH / 2) + ext;
|
||
const available = Math.max(0, centerMax - centerMin);
|
||
const yWithinSliderRect = centerMin + available * (1 - pct);
|
||
|
||
const bubbleHalf = valueBubble.offsetHeight ? (valueBubble.offsetHeight / 2) : 0;
|
||
const top = (sliderRect.top - parentRect.top) + yWithinSliderRect - bubbleHalf;
|
||
|
||
// Use transform to reposition instantly and smoothly
|
||
valueBubble.style.transform = `translateY(${Math.round(top)}px)`;
|
||
} catch (err) {
|
||
// ignore
|
||
}
|
||
});
|
||
}, { passive: true });
|
||
|
||
// --- add thumb dragging class while a slider is being grabbed ---
|
||
(function wireThumbDragging() {
|
||
// remove class on global pointerup/leave to ensure cleanup
|
||
function clearAll() {
|
||
sliderIds.forEach(id => {
|
||
const s = document.getElementById(id);
|
||
if (s && s.classList.contains("thumb-dragging")) s.classList.remove("thumb-dragging");
|
||
});
|
||
}
|
||
|
||
sliderIds.forEach((id) => {
|
||
const slider = document.getElementById(id);
|
||
if (!slider) return;
|
||
|
||
slider.addEventListener("pointerdown", (e) => {
|
||
// add class immediately when pointer interacts with the slider
|
||
slider.classList.add("thumb-dragging");
|
||
// capture pointer so we still receive the up event even if pointer leaves the thumb
|
||
try { slider.setPointerCapture && slider.setPointerCapture(e.pointerId); } catch (err) { /* ignore */ }
|
||
}, { passive: true });
|
||
|
||
// remove when pointer is released on the slider itself
|
||
slider.addEventListener("pointerup", () => {
|
||
slider.classList.remove("thumb-dragging");
|
||
});
|
||
|
||
// ensure we clear if pointer is canceled
|
||
slider.addEventListener("pointercancel", () => {
|
||
slider.classList.remove("thumb-dragging");
|
||
});
|
||
|
||
// also clear on global pointerup (in case pointerup happens outside element)
|
||
window.addEventListener("pointerup", () => {
|
||
slider.classList.remove("thumb-dragging");
|
||
}, { passive: true });
|
||
});
|
||
|
||
// clear on leave/unload to avoid stuck state
|
||
window.addEventListener("blur", clearAll);
|
||
window.addEventListener("pointerleave", clearAll);
|
||
})();
|
||
|
||
// --- Tab switching logic (unchanged) ---
|
||
const tabButtons = document.querySelectorAll(".tab-button");
|
||
const tabPanels = document.querySelectorAll(".tab-content");
|
||
function showTab(targetId) {
|
||
tabPanels.forEach(p => p.classList.toggle("active", p.id === targetId));
|
||
tabButtons.forEach(b => b.classList.toggle("active", b.getAttribute("data-target") === targetId));
|
||
|
||
// If the Sliders tab becomes visible, force a layout-stable recompute of bubble positions.
|
||
// We reuse the existing resize handler logic by dispatching a synthetic resize event
|
||
// after layout has had a chance to settle (double rAF + small timeout fallback).
|
||
if (targetId === "tab-3") {
|
||
requestAnimationFrame(() => {
|
||
requestAnimationFrame(() => {
|
||
window.dispatchEvent(new Event("resize"));
|
||
});
|
||
setTimeout(() => window.dispatchEvent(new Event("resize")), 80);
|
||
});
|
||
}
|
||
}
|
||
tabButtons.forEach(btn => btn.addEventListener("click", () => showTab(btn.getAttribute("data-target"))));
|
||
showTab("tab-1");
|
||
|
||
// --- Settings overlay logic (unchanged) ---
|
||
const settingsBtn = document.getElementById("settings-btn");
|
||
const settingsOverlay = document.getElementById("settings-overlay");
|
||
const closeSettings = document.getElementById("close-settings");
|
||
function toggleSettingsOverlay(show) {
|
||
if (!settingsOverlay) return;
|
||
settingsOverlay.setAttribute("aria-hidden", String(!show));
|
||
if (show) {
|
||
// show first settings tab by default
|
||
showSettingsTab("settings-tab-1");
|
||
}
|
||
}
|
||
if (settingsBtn) settingsBtn.addEventListener("click", () => toggleSettingsOverlay(true));
|
||
if (closeSettings) closeSettings.addEventListener("click", () => toggleSettingsOverlay(false));
|
||
const settingsTabButtons = document.querySelectorAll(".settings-tab-button");
|
||
const settingsTabContents = document.querySelectorAll(".settings-tab-content");
|
||
function showSettingsTab(targetId) {
|
||
settingsTabContents.forEach(p => p.classList.toggle("active", p.id === targetId));
|
||
settingsTabButtons.forEach(b => b.classList.toggle("active", b.getAttribute("data-target") === targetId));
|
||
}
|
||
settingsTabButtons.forEach(btn => btn.addEventListener("click", () => showSettingsTab(btn.getAttribute("data-target"))));
|
||
toggleSettingsOverlay(false);
|
||
|
||
// --- Potentiometer visual (unchanged) ---
|
||
let potValue = 50;
|
||
const potEl = document.getElementById("pot-0");
|
||
const potValueEl = potEl ? potEl.querySelector(".pot-value") : null;
|
||
const potRingEl = potEl ? potEl.querySelector(".pot-ring") : null;
|
||
function updatePot(value) {
|
||
if (!potEl) return;
|
||
potValue = Math.max(0, Math.min(100, Number(value) || 0));
|
||
if (potValueEl) potValueEl.textContent = String(potValue);
|
||
if (potRingEl) potRingEl.style.setProperty("--deg", `${potValue * 3.6}deg`);
|
||
console.log(`pot updated -> ${potValue}`);
|
||
}
|
||
window.setPotValue = function(v) { updatePot(v); };
|
||
window.getPotValue = function() { return potValue; };
|
||
updatePot(potValue);
|
||
|
||
// --- Connect / Disconnect wiring ---
|
||
const connectBtn = document.getElementById("connect-com-btn");
|
||
let unregisterSerial = null;
|
||
let isConnected = false;
|
||
|
||
function setConnectedUI(connected) {
|
||
isConnected = !!connected;
|
||
if (!connectBtn) return;
|
||
if (connected) {
|
||
connectBtn.textContent = "Disconnect";
|
||
connectBtn.classList.remove("connect-state");
|
||
connectBtn.classList.add("disconnect-state");
|
||
} else {
|
||
connectBtn.textContent = "Connect";
|
||
connectBtn.classList.remove("disconnect-state");
|
||
connectBtn.classList.add("connect-state");
|
||
// ensure UI sliders reset to 0 on disconnect / initial state
|
||
sliderIds.forEach((sid, idx) => {
|
||
const sliderEl = document.getElementById(sid);
|
||
if (!sliderEl) return;
|
||
sliderEl.value = "0";
|
||
// update bubble/position via input event
|
||
sliderEl.dispatchEvent(new Event('input', { bubbles: true }));
|
||
// update internal state array
|
||
sliderValues[idx] = 0;
|
||
});
|
||
}
|
||
}
|
||
|
||
if (connectBtn) {
|
||
connectBtn.addEventListener("click", async () => {
|
||
const comSelect = document.getElementById("com-port-select");
|
||
const port = comSelect ? comSelect.value : "";
|
||
if (!port) {
|
||
console.warn("No COM port selected");
|
||
return;
|
||
}
|
||
if (!isConnected) {
|
||
await doConnect(port);
|
||
} else {
|
||
await doDisconnect();
|
||
}
|
||
}, { passive: true });
|
||
// initialize UI state
|
||
setConnectedUI(false);
|
||
}
|
||
|
||
// --- AUTOCONNECT: after COM list population, try to connect when configured ---
|
||
(async function maybeAutoConnect() {
|
||
try {
|
||
const cfg = await window.api.loadTags();
|
||
if (cfg && cfg.autoconnect && cfg.comPort) {
|
||
setTimeout(() => {
|
||
const comSelect = document.getElementById("com-port-select");
|
||
if (comSelect) {
|
||
try { comSelect.value = cfg.comPort; } catch (e) { /* ignore */ }
|
||
}
|
||
doConnect(cfg.comPort);
|
||
}, 120);
|
||
}
|
||
} catch (e) { /* ignore */ }
|
||
})();
|
||
|
||
// --- NEW: shared connect/disconnect helpers for autoconnect and button click ---
|
||
async function doConnect(port) {
|
||
if (!port) {
|
||
console.warn("doConnect: no port specified");
|
||
return { ok: false, message: "No port" };
|
||
}
|
||
if (isConnected) {
|
||
return { ok: false, message: "Already connected" };
|
||
}
|
||
try {
|
||
const result = await window.api.connectCom(port, 9600);
|
||
if (result && result.ok) {
|
||
// subscribe to serial updates exactly as the original click handler did
|
||
unregisterSerial = window.api.onSerialUpdate((values) => {
|
||
if (!Array.isArray(values)) return;
|
||
sliderIds.forEach((sid, idx) => {
|
||
const sliderEl = document.getElementById(sid);
|
||
if (!sliderEl) return;
|
||
const raw = values[idx];
|
||
if (typeof raw !== "number") return;
|
||
const pct = Math.round((raw * 100) / 1023);
|
||
const clamped = Math.max(0, Math.min(100, pct));
|
||
sliderEl.value = String(clamped);
|
||
sliderEl.dispatchEvent(new Event('input', { bubbles: true }));
|
||
sliderValues[idx] = clamped;
|
||
});
|
||
});
|
||
setConnectedUI(true);
|
||
return { ok: true };
|
||
} else {
|
||
console.warn("Connect failed:", result && result.message);
|
||
return { ok: false, message: result && result.message };
|
||
}
|
||
} catch (err) {
|
||
console.warn("doConnect error:", err);
|
||
return { ok: false, message: String(err) };
|
||
}
|
||
}
|
||
|
||
async function doDisconnect() {
|
||
if (!isConnected) {
|
||
return { ok: false, message: "Not connected" };
|
||
}
|
||
try {
|
||
const res = await window.api.disconnectCom();
|
||
if (res && res.ok) {
|
||
if (typeof unregisterSerial === "function") unregisterSerial();
|
||
unregisterSerial = null;
|
||
setConnectedUI(false);
|
||
return { ok: true };
|
||
} else {
|
||
console.warn("Disconnect returned:", res && res.message);
|
||
return { ok: false, message: res && res.message };
|
||
}
|
||
} catch (err) {
|
||
console.warn("doDisconnect error:", err);
|
||
return { ok: false, message: String(err) };
|
||
}
|
||
}
|
||
|
||
// --- NEW: utility to rebuild the layered background using current CSS vars and set inline style ---
|
||
function setBodyBackgroundFromVars() {
|
||
const root = document.documentElement;
|
||
const body = document.body;
|
||
if (!root || !body) return;
|
||
|
||
// read variables (fall back to existing defaults)
|
||
const bg1 = (getComputedStyle(root).getPropertyValue('--bg-base-1') || '#1a1a1a').trim();
|
||
const bg2 = (getComputedStyle(root).getPropertyValue('--bg-base-2') || '#0d0d0f').trim();
|
||
const stripe = (getComputedStyle(root).getPropertyValue('--stripe') || 'rgba(255,255,255,0.012)').trim();
|
||
|
||
// Recreate the layered background used in CSS, but inline so changes are immediate.
|
||
const layers = [
|
||
`radial-gradient(600px 360px at 10% 10%, rgba(255,255,255,0.03), transparent 18%)`,
|
||
`radial-gradient(700px 420px at 92% 86%, rgba(0,0,0,0.12), transparent 45%)`,
|
||
`linear-gradient(180deg, ${bg1}, ${bg2})`,
|
||
`repeating-linear-gradient(135deg, ${stripe} 0 2px, transparent 2px 8px)`
|
||
];
|
||
|
||
body.style.backgroundImage = layers.join(',\n ');
|
||
// keep background-color in sync as a fallback
|
||
body.style.backgroundColor = bg2;
|
||
body.style.backgroundRepeat = 'no-repeat';
|
||
body.style.backgroundSize = 'cover';
|
||
}
|
||
|
||
// --- NEW: apply a customisation payload (keys: background, sidebar, button, potentiometer, accent) ---
|
||
function applyCustomisation(payload) {
|
||
try {
|
||
const root = document.documentElement;
|
||
if (!root) return;
|
||
|
||
// helper: simple hex lighten for bg1 derivation
|
||
function lightenHex(hex, pct) {
|
||
try {
|
||
const h = hex.replace('#','');
|
||
const num = parseInt(h,16);
|
||
let r = (num >> 16) & 0xFF;
|
||
let g = (num >> 8) & 0xFF;
|
||
let b = num & 0xFF;
|
||
r = Math.min(255, Math.round(r + (255 - r) * pct));
|
||
g = Math.min(255, Math.round(g + (255 - g) * pct));
|
||
b = Math.min(255, Math.round(b + (255 - b) * pct));
|
||
const res = '#' + ((1<<24) + (r<<16) + (g<<8) + b).toString(16).slice(1);
|
||
return res;
|
||
} catch (e) { return hex; }
|
||
}
|
||
|
||
if (payload.background) {
|
||
root.style.setProperty('--bg-base-2', payload.background);
|
||
// derive a slightly lighter variant for the top of the gradient
|
||
root.style.setProperty('--bg-base-1', lightenHex(payload.background, 0.08));
|
||
}
|
||
if (payload.sidebar) root.style.setProperty('--sidebar-bg', payload.sidebar);
|
||
if (payload.button) root.style.setProperty('--button-bg', payload.button);
|
||
if (payload.potentiometer) root.style.setProperty('--pot-color', payload.potentiometer);
|
||
if (payload.accent) {
|
||
root.style.setProperty('--accent-color', payload.accent);
|
||
// create a subtle stripe using accent color with low alpha
|
||
try {
|
||
const rgb = payload.accent.replace('#','');
|
||
const r = parseInt(rgb.slice(0,2),16);
|
||
const g = parseInt(rgb.slice(2,4),16);
|
||
const b = parseInt(rgb.slice(4,6),16);
|
||
root.style.setProperty('--stripe', `rgba(${r},${g},${b},0.012)`);
|
||
} catch (e) {
|
||
// fallback stripe
|
||
root.style.setProperty('--stripe', 'rgba(255,255,255,0.012)');
|
||
}
|
||
}
|
||
|
||
// rebuild inline layered body background for immediate result
|
||
setBodyBackgroundFromVars();
|
||
} catch (e) { /* ignore */ }
|
||
}
|
||
|
||
// --- NEW: re-sync all customisation inputs (applies their values to CSS variables) ---
|
||
function syncCustomInputs() {
|
||
const idToKey = {
|
||
'cust-bg': 'background',
|
||
'cust-sidebar': 'sidebar',
|
||
'cust-button': 'button',
|
||
'cust-pot': 'potentiometer',
|
||
'cust-accent': 'accent'
|
||
};
|
||
const inputs = Array.from(document.querySelectorAll('#settings-tab-2 input[type="color"][data-default]'));
|
||
const payload = {};
|
||
inputs.forEach(inp => {
|
||
const key = idToKey[inp.id];
|
||
if (key && inp.value) payload[key] = inp.value;
|
||
});
|
||
if (Object.keys(payload).length) applyCustomisation(payload);
|
||
}
|
||
|
||
// --- NEW: wire reset-default buttons and live update on color inputs ---
|
||
(function wireCustomisationInputs() {
|
||
try {
|
||
// helper: update visibility of reset button for a given input
|
||
function updateResetVisibilityForInput(inp) {
|
||
if (!inp) return;
|
||
const btn = document.querySelector(`#settings-tab-2 .reset-default[data-target="${inp.id}"]`);
|
||
if (!btn) return;
|
||
const def = (inp.getAttribute('data-default') || '').toLowerCase();
|
||
const cur = (inp.value || '').toLowerCase();
|
||
if (cur !== def && cur !== '') {
|
||
btn.classList.add('dirty');
|
||
btn.setAttribute('aria-hidden', 'false');
|
||
} else {
|
||
btn.classList.remove('dirty');
|
||
btn.setAttribute('aria-hidden', 'true');
|
||
}
|
||
}
|
||
|
||
// color inputs change -> apply + save + toggle reset visibility
|
||
document.querySelectorAll('#settings-tab-2 input[type="color"][data-default]').forEach(inp => {
|
||
// set initial visibility on setup
|
||
try { updateResetVisibilityForInput(inp); } catch (e) { /* ignore */ }
|
||
|
||
inp.addEventListener('input', () => {
|
||
syncCustomInputs();
|
||
// update associated reset button visibility immediately
|
||
try { updateResetVisibilityForInput(inp); } catch (e) { /* ignore */ }
|
||
|
||
// debounce save to avoid excessive disk writes (coarse)
|
||
if (window._custSaveTimer) clearTimeout(window._custSaveTimer);
|
||
window._custSaveTimer = setTimeout(() => saveConfig(), 220);
|
||
}, { passive: true });
|
||
});
|
||
|
||
// reset buttons: restore default, hide arrow, apply + save
|
||
document.querySelectorAll('#settings-tab-2 .reset-default').forEach(btn => {
|
||
btn.classList.remove('dirty'); // ensure hidden by default
|
||
btn.setAttribute('aria-hidden', 'true');
|
||
|
||
btn.addEventListener('click', (e) => {
|
||
const target = btn.getAttribute('data-target');
|
||
if (!target) return;
|
||
const inp = document.getElementById(target);
|
||
if (!inp) return;
|
||
const def = inp.getAttribute('data-default') || inp.value || '';
|
||
try { inp.value = def; } catch (er) { /* ignore */ }
|
||
// hide the button immediately
|
||
try {
|
||
btn.classList.remove('dirty');
|
||
btn.setAttribute('aria-hidden', 'true');
|
||
} catch (er) { /* ignore */ }
|
||
|
||
// apply changes and persist
|
||
syncCustomInputs();
|
||
saveConfig();
|
||
}, { passive: true });
|
||
});
|
||
|
||
// ensure initial sync and reset-visibility for all inputs now
|
||
setTimeout(() => {
|
||
try {
|
||
document.querySelectorAll('#settings-tab-2 input[type="color"][data-default]').forEach(inp => updateResetVisibilityForInput(inp));
|
||
} catch (e) { /* ignore */ }
|
||
syncCustomInputs();
|
||
}, 40);
|
||
} catch (e) { /* ignore */ }
|
||
})();
|
||
});
|
||
|
||
|