Files
Dreckshub/renderer.js
2025-11-17 13:53:14 +01:00

815 lines
36 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 */ }
})();
});