Adding Project Files
This commit is contained in:
814
renderer.js
Normal file
814
renderer.js
Normal file
@@ -0,0 +1,814 @@
|
||||
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 */ }
|
||||
})();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user