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 { 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';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user