chore(release): beta-1.03

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-10 16:55:07 +12:00
parent 822d9d8e01
commit 0ae39ab94b
22 changed files with 1816 additions and 184 deletions

View File

@@ -2,6 +2,48 @@
let pollTimer = null;
let lastBeatSeq = 0;
const STORAGE_KEY = "led-controller-audio-restore";
const STORAGE_VERSION = 1;
function readRestorePrefs() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return null;
const o = JSON.parse(raw);
if (!o || o.v !== STORAGE_VERSION || !o.restore) return null;
return {
override: typeof o.override === "string" ? o.override : "",
select: typeof o.select === "string" ? o.select : "",
};
} catch {
return null;
}
}
function writeRestorePrefs(override, select) {
try {
localStorage.setItem(
STORAGE_KEY,
JSON.stringify({
v: STORAGE_VERSION,
restore: true,
override: override || "",
select: select || "",
}),
);
} catch (e) {
console.warn("audio restore prefs save failed", e);
}
}
function clearRestorePrefs() {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (e) {
console.warn("audio restore prefs clear failed", e);
}
}
function el(id) {
return document.getElementById(id);
}
@@ -31,19 +73,27 @@
node.textContent = `${label}${conf}`;
}
function setTopBpmVisible(on) {
const top = el("audio-top-indicator");
if (!top) return;
top.classList.toggle("audio-running", !!on);
}
function flashBeat() {
const node = el("audio-beat-flash");
if (!node) return;
node.classList.add("active");
setTimeout(() => node.classList.remove("active"), 80);
const top = el("audio-top-indicator");
if (top) {
if (top && top.classList.contains("audio-running")) {
top.classList.add("flash");
setTimeout(() => top.classList.remove("flash"), 90);
}
}
async function stopAudio() {
/** Stop detector and polling; does not clear “resume on load” prefs (used before restart). */
async function stopAudioOnly() {
setTopBpmVisible(false);
if (pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
@@ -57,6 +107,12 @@
}
}
/** User-initiated stop: also forget auto-restart on next page load. */
async function stopAudio() {
await stopAudioOnly();
clearRestorePrefs();
}
async function pollStatus() {
try {
const res = await fetch("/api/audio/status");
@@ -68,12 +124,14 @@
node.textContent = String(status.error).trim().slice(0, 120);
}
updateBpmDisplay(null);
setTopBpmVisible(!!status.running);
if (!status.running && pollTimer) {
clearInterval(pollTimer);
pollTimer = null;
}
return;
}
setTopBpmVisible(!!status.running);
updateBpmDisplay(status.bpm);
updateHitTypeDisplay(status.beat_type, Number(status.beat_type_confidence));
const seq = Number(status.beat_seq || 0);
@@ -88,7 +146,7 @@
}
async function startAudio() {
await stopAudio();
await stopAudioOnly();
const override = (el("audio-device-override")?.value || "").trim();
const selected = el("audio-device-select")?.value || "";
const rawDevice = override !== "" ? override : selected;
@@ -103,6 +161,7 @@
const data = await res.json().catch(() => ({}));
throw new Error(data.error || "Failed to start audio detector");
}
writeRestorePrefs(override, selected);
updateBpmDisplay(null);
updateHitTypeDisplay("unknown", NaN);
updateBeatCounter(0);
@@ -211,8 +270,30 @@
}
}
document.addEventListener("DOMContentLoaded", () => {
async function restoreAudioIfNeeded() {
if (pollTimer) return;
const prefs = readRestorePrefs();
if (!prefs) return;
const ov = el("audio-device-override");
const sel = el("audio-device-select");
if (ov) ov.value = prefs.override || "";
try {
await refreshDevices();
} catch (e) {
console.warn("audio restore refresh devices failed", e);
}
if (sel && prefs.select) sel.value = prefs.select;
try {
await startAudio();
} catch (e) {
console.warn("audio auto-restart failed", e);
clearRestorePrefs();
}
}
document.addEventListener("DOMContentLoaded", async () => {
bind();
resumePollingIfDetectorRunning();
await resumePollingIfDetectorRunning();
await restoreAudioIfNeeded();
});
})();