- CommandHistory class with execute/undo/redo, 50-step max, redo-clear on new action
- PlaceBlockCommand, MoveBlockCommand, ResizeBlockCommand, DeleteBlockCommand
- setCurrentShowId() module-level function for backend sync routing
- syncBlock() fire-and-forget fetch helper for backend sync
- POST/PATCH/DELETE /api/shows/{show_id}/tracks/{device_id}/blocks endpoints
- timeline router registered in main.py with prefix=/api
111 lines
4.2 KiB
JavaScript
111 lines
4.2 KiB
JavaScript
// 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');
|
|
}
|
|
}
|