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 = ''; 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 = ''; 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 = ''; } } 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 */ } })(); });