feat(05-02): wire preview_update handler, show selector, and preview strip builder

- Import setCurrentShowId from commands.js
- Add preview_update WebSocket message handler that calls updatePreviewStrip
- Add updatePreviewStrip() function to update device strip color and animation
- Add buildPreviewStrips() function to populate #live-preview from device list
- Call buildPreviewStrips() inside device fetch callback in initTimeline
- Add async refreshShows() to populate show-select dropdown from /api/shows
- Add btn-load-show click handler: loads show, populates tracks, sends WebSocket load msg
- Clear preview strips on seek (remove active class, reset color and anim label)
This commit is contained in:
Claude
2026-04-07 10:15:34 +00:00
parent e0e412fcd1
commit e064954b28

View File

@@ -3,7 +3,7 @@
import { TimelineCanvas } from './timeline/timeline.js'; import { TimelineCanvas } from './timeline/timeline.js';
import { BlockInspector } from './timeline/inspector.js'; import { BlockInspector } from './timeline/inspector.js';
import { DeleteBlockCommand } from './timeline/commands.js'; import { DeleteBlockCommand, setCurrentShowId } from './timeline/commands.js';
const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`; const WS_URL = `${location.protocol === 'https:' ? 'wss' : 'ws'}://${location.host}/ws`;
@@ -50,6 +50,7 @@ class LightSyncClient {
this.ws.onmessage = (e) => { this.ws.onmessage = (e) => {
const msg = JSON.parse(e.data); const msg = JSON.parse(e.data);
if (msg.type === 'beat') flashBeat(); if (msg.type === 'beat') flashBeat();
else if (msg.type === 'preview_update') updatePreviewStrip(msg.device_id, msg.animation, msg.color);
}; };
} }
@@ -71,6 +72,25 @@ function flashBeat() {
setTimeout(() => dot.classList.remove('beat-flash'), 180); setTimeout(() => dot.classList.remove('beat-flash'), 180);
} }
// ── Preview strip update ─────────────────────────────────────────────────────
function updatePreviewStrip(deviceId, animation, color) {
const strip = document.querySelector(`.device-strip[data-device-id="${deviceId}"]`);
if (!strip) return;
const colorEl = strip.querySelector('.strip-color');
const animEl = strip.querySelector('.strip-anim');
if (animation && color) {
const [r, g, b] = color;
colorEl.style.backgroundColor = `rgb(${r},${g},${b})`;
animEl.textContent = animation.toUpperCase().replace('_', ' ');
strip.classList.add('active');
} else {
colorEl.style.backgroundColor = ''; // revert to CSS default (--text-dim)
animEl.textContent = '\u2014'; // em dash
strip.classList.remove('active');
}
}
// ── 10Hz position tick loop ────────────────────────────────────────────────── // ── 10Hz position tick loop ──────────────────────────────────────────────────
let _tickInterval = null; let _tickInterval = null;
@@ -143,6 +163,12 @@ function wireAudio() {
if (audio.duration > 0) { if (audio.duration > 0) {
audio.currentTime = (seek.value / 100) * audio.duration; audio.currentTime = (seek.value / 100) * audio.duration;
client.send({ type: 'seek', position: audio.currentTime }); client.send({ type: 'seek', position: audio.currentTime });
// Clear preview strips on seek (strips re-light when next cue fires)
document.querySelectorAll('.device-strip').forEach(s => {
s.classList.remove('active');
s.querySelector('.strip-color').style.backgroundColor = '';
s.querySelector('.strip-anim').textContent = '\u2014';
});
} }
}); });
} }
@@ -252,6 +278,20 @@ document.getElementById('add-device-form')?.addEventListener('submit', async (e)
wireAudio(); wireAudio();
// ── Preview strip builder ────────────────────────────────────────────────────
function buildPreviewStrips(devices) {
const panel = document.getElementById('live-preview');
if (!panel) return;
panel.innerHTML = devices.map(d => `
<div class="device-strip" data-device-id="${d.id}">
<span class="strip-label">${d.name}</span>
<div class="strip-color"></div>
<span class="strip-anim">\u2014</span>
</div>
`).join('');
}
// ── Timeline ───────────────────────────────────────────────────────────────── // ── Timeline ─────────────────────────────────────────────────────────────────
let timeline = null; let timeline = null;
@@ -285,6 +325,7 @@ function initTimeline() {
fetch('/api/devices').then(r => r.json()).then(devices => { fetch('/api/devices').then(r => r.json()).then(devices => {
timeline.loadTracks(devices); timeline.loadTracks(devices);
inspector.tracks = timeline.tracks; // keep reference in sync inspector.tracks = timeline.tracks; // keep reference in sync
buildPreviewStrips(devices); // populate preview strips
}).catch(e => console.warn('[timeline]', e)); }).catch(e => console.warn('[timeline]', e));
// When audio loads, fetch waveform + beats // When audio loads, fetch waveform + beats
@@ -377,3 +418,78 @@ function initTimeline() {
} }
initTimeline(); initTimeline();
// ── Show Selector ───────────────────────────────────────────────────────────
async function refreshShows() {
const sel = document.getElementById('show-select');
if (!sel) return;
try {
const shows = await (await fetch('/api/shows')).json();
sel.innerHTML = shows.length
? '<option value="">\u2014 select show \u2014</option>'
: '<option value="">\u2014 no shows \u2014</option>';
shows.forEach(s => {
const opt = document.createElement('option');
opt.value = s.id;
opt.textContent = s.name;
sel.appendChild(opt);
});
} catch (e) { console.error('[shows]', e); }
}
refreshShows();
document.getElementById('btn-load-show')?.addEventListener('click', async () => {
const sel = document.getElementById('show-select');
const showId = sel?.value;
if (!showId) return;
const btn = document.getElementById('btn-load-show');
btn.textContent = '...';
try {
const show = await (await fetch(`/api/shows/${showId}`)).json();
// Populate timeline tracks with saved cues
if (timeline && show.tracks) {
for (const showTrack of show.tracks) {
const existing = timeline.tracks.find(
t => String(t.device_id) === String(showTrack.device_id)
);
if (existing) {
existing.cues = (showTrack.cues || []).map(c => ({...c}));
} else {
console.warn('[show] track for unknown device', showTrack.device_id);
}
}
}
// Set current show ID for auto-save
if (timeline) timeline.showId = showId;
setCurrentShowId(showId);
// Load audio if show has audio path
if (show.audio?.path) {
const filename = show.audio.path.split('/').pop();
const audio = document.getElementById('player');
if (audio && filename) {
audio.src = `/shows/${filename}`;
audio.load();
}
}
// Tell server to load show cue list for scheduling
client.send({ type: 'load', show_id: showId, path: show.audio?.path || '' });
// Show preview panel
const preview = document.getElementById('live-preview');
if (preview) preview.style.display = 'flex';
console.log('[show] loaded', show.name, 'with', show.tracks?.length || 0, 'tracks');
} catch (e) {
console.error('[show] failed to load', showId, e);
} finally {
btn.textContent = 'LOAD';
}
});