Files
led2/lightsync/frontend/timeline/commands.js
Claude e82fc4a775 feat(04-04): add UpdateParamsCommand + BlockInspector module
- UpdateParamsCommand with execute/undo for color/speed/direction/length
- Uses module-level currentShowId (not this._showId) matching existing pattern
- BlockInspector renders compact strip with animation label, color swatch,
  speed input, direction select, length input, and remove button
- Color change committed on 'change' event (not 'input') to avoid command flood
- _onDelete callback wired to remove button for app.js to handle
2026-04-06 23:51:48 +00:00

132 lines
5.0 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');
}
}
// ── UpdateParamsCommand ──────────────────────────────────────────────────────
export class UpdateParamsCommand {
constructor(tracks, block, oldParams, newParams) {
this.tracks = tracks;
this.block = block;
this.oldParams = { ...oldParams }; // shallow copy
this.newParams = { ...newParams }; // shallow copy
}
execute() {
Object.assign(this.block.params, this.newParams);
syncBlock(currentShowId, this.block.device_id, this.block, 'PATCH');
}
undo() {
Object.assign(this.block.params, this.oldParams);
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');
}
}