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