// Timeline block commands — command pattern for undo/redo support // All mutations go through these commands, which also sync to the backend. // Backend sync is fire-and-forget (no await during rapid undo/redo). let currentShowId = null; export function setCurrentShowId(id) { currentShowId = id; } function syncBlock(showId, deviceId, block, method) { if (!showId) return; // no show loaded yet — skip sync const base = `/api/shows/${showId}/tracks/${deviceId}/blocks`; const opts = { method, headers: { 'Content-Type': 'application/json' } }; if (method === 'POST' || method === 'PATCH') { opts.body = JSON.stringify(block); } const url = method === 'POST' ? base : `${base}/${block.id}`; fetch(url, opts).catch(e => console.warn('[sync]', e)); } // ── PlaceBlockCommand ──────────────────────────────────────────────────────── export class PlaceBlockCommand { constructor(tracks, block) { this.tracks = tracks; this.block = block; // {id, timestamp, duration, animation, params, device_id} } execute() { const track = this.tracks.find(t => t.device_id === this.block.device_id); if (track) track.cues.push(this.block); syncBlock(currentShowId, this.block.device_id, this.block, 'POST'); } undo() { const track = this.tracks.find(t => t.device_id === this.block.device_id); if (track) track.cues = track.cues.filter(c => c.id !== this.block.id); syncBlock(currentShowId, this.block.device_id, this.block, 'DELETE'); } } // ── MoveBlockCommand ───────────────────────────────────────────────────────── export class MoveBlockCommand { constructor(tracks, block, oldTimestamp, newTimestamp) { this.tracks = tracks; this.block = block; this.oldTimestamp = oldTimestamp; this.newTimestamp = newTimestamp; } execute() { this.block.timestamp = this.newTimestamp; syncBlock(currentShowId, this.block.device_id, this.block, 'PATCH'); } undo() { this.block.timestamp = this.oldTimestamp; syncBlock(currentShowId, this.block.device_id, this.block, 'PATCH'); } } // ── ResizeBlockCommand ─────────────────────────────────────────────────────── // Left-edge resize changes both timestamp and duration (right edge stays fixed). // Right-edge resize changes only duration. export class ResizeBlockCommand { constructor(tracks, block, oldTimestamp, oldDuration, newTimestamp, newDuration) { this.tracks = tracks; this.block = block; this.oldTimestamp = oldTimestamp; this.oldDuration = oldDuration; this.newTimestamp = newTimestamp; this.newDuration = newDuration; } execute() { this.block.timestamp = this.newTimestamp; this.block.duration = this.newDuration; syncBlock(currentShowId, this.block.device_id, this.block, 'PATCH'); } undo() { this.block.timestamp = this.oldTimestamp; this.block.duration = this.oldDuration; syncBlock(currentShowId, this.block.device_id, this.block, 'PATCH'); } } // ── DeleteBlockCommand ─────────────────────────────────────────────────────── export class DeleteBlockCommand { constructor(tracks, block) { this.tracks = tracks; this.block = block; } execute() { const track = this.tracks.find(t => t.device_id === this.block.device_id); if (track) track.cues = track.cues.filter(c => c.id !== this.block.id); syncBlock(currentShowId, this.block.device_id, this.block, 'DELETE'); } undo() { const track = this.tracks.find(t => t.device_id === this.block.device_id); if (track) track.cues.push(this.block); syncBlock(currentShowId, this.block.device_id, this.block, 'POST'); } }