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:
@@ -3,7 +3,7 @@
|
||||
|
||||
import { TimelineCanvas } from './timeline/timeline.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`;
|
||||
|
||||
@@ -50,6 +50,7 @@ class LightSyncClient {
|
||||
this.ws.onmessage = (e) => {
|
||||
const msg = JSON.parse(e.data);
|
||||
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);
|
||||
}
|
||||
|
||||
// ── 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 ──────────────────────────────────────────────────
|
||||
|
||||
let _tickInterval = null;
|
||||
@@ -143,6 +163,12 @@ function wireAudio() {
|
||||
if (audio.duration > 0) {
|
||||
audio.currentTime = (seek.value / 100) * audio.duration;
|
||||
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();
|
||||
|
||||
// ── 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 ─────────────────────────────────────────────────────────────────
|
||||
|
||||
let timeline = null;
|
||||
@@ -285,6 +325,7 @@ function initTimeline() {
|
||||
fetch('/api/devices').then(r => r.json()).then(devices => {
|
||||
timeline.loadTracks(devices);
|
||||
inspector.tracks = timeline.tracks; // keep reference in sync
|
||||
buildPreviewStrips(devices); // populate preview strips
|
||||
}).catch(e => console.warn('[timeline]', e));
|
||||
|
||||
// When audio loads, fetch waveform + beats
|
||||
@@ -377,3 +418,78 @@ function 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';
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user