feat(ui): numpad, audio readout, and sequence beat controls
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,14 +1,98 @@
|
||||
// Sequences: lanes (parallel preset chains); advance is always by audio beats or simulated BPM.
|
||||
// Debug: in the browser console run setSequenceDebug(true) — toggling logs 1 (on) or 0 (off).
|
||||
// Debug: in the browser console run setSequenceDebug(true) — session only, not persisted.
|
||||
|
||||
const SEQ_DEBUG_STORAGE_KEY = 'led-controller-sequence-debug';
|
||||
/** @type {'beat'|'downbeat'} */
|
||||
let sequenceSwitchWaitFor = 'beat';
|
||||
|
||||
let sequenceDebugEnabled = false;
|
||||
let sequenceSwitchSaveInFlight = false;
|
||||
|
||||
async function loadSequenceSwitchWaitForFromServer() {
|
||||
try {
|
||||
const res = await fetch('/settings', {
|
||||
cache: 'no-store',
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!res.ok) return;
|
||||
const data = await res.json();
|
||||
const raw = data && data.sequence_switch_wait;
|
||||
if (raw === 'downbeat' || raw === 'beat') {
|
||||
sequenceSwitchWaitFor = raw;
|
||||
} else if (raw === 'phrase') {
|
||||
sequenceSwitchWaitFor = 'beat';
|
||||
}
|
||||
} catch {
|
||||
/* keep default */
|
||||
}
|
||||
}
|
||||
|
||||
async function persistSequenceSwitchWaitFor() {
|
||||
sequenceSwitchSaveInFlight = true;
|
||||
try {
|
||||
const res = await fetch('/settings', {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
||||
body: JSON.stringify({ sequence_switch_wait: sequenceSwitchWaitFor }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.warn('[sequence] could not save switch wait to server', res.status);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('[sequence] could not save switch wait to server', e);
|
||||
} finally {
|
||||
sequenceSwitchSaveInFlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
function getSequenceSwitchWaitFor() {
|
||||
return sequenceSwitchWaitFor === 'downbeat' ? 'downbeat' : 'beat';
|
||||
}
|
||||
|
||||
async function setSequenceSwitchWaitFor(waitFor) {
|
||||
sequenceSwitchWaitFor = waitFor === 'downbeat' ? 'downbeat' : 'beat';
|
||||
updateSequenceSwitchToggleUI();
|
||||
await persistSequenceSwitchWaitFor();
|
||||
}
|
||||
|
||||
function updateSequenceSwitchToggleUI() {
|
||||
const mode = getSequenceSwitchWaitFor();
|
||||
const ariaLabels = {
|
||||
beat: 'Switch sequence on beat',
|
||||
downbeat: 'Switch sequence on downbeat',
|
||||
};
|
||||
document.querySelectorAll('.seq-switch-toggle').forEach((btn) => {
|
||||
btn.setAttribute('aria-pressed', mode === 'beat' ? 'false' : 'true');
|
||||
btn.setAttribute('aria-label', ariaLabels[mode] || ariaLabels.beat);
|
||||
btn.classList.toggle('seq-switch-toggle--downbeat', mode === 'downbeat');
|
||||
});
|
||||
document.querySelectorAll('.seq-switch-toggle-wrap').forEach((wrap) => {
|
||||
wrap.classList.toggle('nav-slide-toggle-wrap--downbeat', mode === 'downbeat');
|
||||
});
|
||||
}
|
||||
|
||||
async function initSequenceSwitchToggle() {
|
||||
await loadSequenceSwitchWaitForFromServer();
|
||||
updateSequenceSwitchToggleUI();
|
||||
document.querySelectorAll('.seq-switch-toggle').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
void setSequenceSwitchWaitFor(getSequenceSwitchWaitFor() === 'beat' ? 'downbeat' : 'beat');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/** Sync toggle when settings changed elsewhere (e.g. another tab via audio status poll). */
|
||||
function applySequenceSwitchWaitFromServer(raw) {
|
||||
if (sequenceSwitchSaveInFlight) return;
|
||||
let mode = 'beat';
|
||||
if (raw === 'downbeat') mode = 'downbeat';
|
||||
else if (raw !== 'beat' && raw !== 'phrase') return;
|
||||
if (mode === getSequenceSwitchWaitFor()) return;
|
||||
sequenceSwitchWaitFor = mode;
|
||||
updateSequenceSwitchToggleUI();
|
||||
}
|
||||
|
||||
function seqDebugEnabled() {
|
||||
try {
|
||||
return localStorage.getItem(SEQ_DEBUG_STORAGE_KEY) === '1';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return sequenceDebugEnabled;
|
||||
}
|
||||
|
||||
/** @type {ReturnType<typeof setInterval> | null} */
|
||||
@@ -1120,15 +1204,11 @@ async function loadSequencesModalList() {
|
||||
});
|
||||
}
|
||||
|
||||
window.applySequenceSwitchWaitFromServer = applySequenceSwitchWaitFromServer;
|
||||
window.stopZoneSequencePlayback = stopZoneSequencePlayback;
|
||||
/** @param {boolean} on */
|
||||
window.setSequenceDebug = function setSequenceDebug(on) {
|
||||
try {
|
||||
if (on) localStorage.setItem(SEQ_DEBUG_STORAGE_KEY, '1');
|
||||
else localStorage.removeItem(SEQ_DEBUG_STORAGE_KEY);
|
||||
} catch (e) {
|
||||
console.warn('[sequence] could not persist debug flag', e);
|
||||
}
|
||||
sequenceDebugEnabled = !!on;
|
||||
console.log(seqDebugEnabled() ? 1 : 0);
|
||||
};
|
||||
window.appendZoneSequenceTiles = appendZoneSequenceTiles;
|
||||
@@ -1137,6 +1217,7 @@ window.addSequenceToTab = addSequenceToTab;
|
||||
window.removeSequenceFromTab = removeSequenceFromTab;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
void initSequenceSwitchToggle();
|
||||
const btn = document.getElementById('sequences-btn');
|
||||
const modal = document.getElementById('sequences-modal');
|
||||
const closeBtn = document.getElementById('sequences-close-btn');
|
||||
|
||||
Reference in New Issue
Block a user