- 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
87 lines
3.1 KiB
Python
87 lines
3.1 KiB
Python
from uuid import UUID
|
|
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
from typing import Any
|
|
import lightsync.main as state
|
|
|
|
router = APIRouter(tags=["timeline"])
|
|
|
|
|
|
class BlockCreate(BaseModel):
|
|
id: str # UUID as string from frontend crypto.randomUUID()
|
|
timestamp: float
|
|
duration: float = 4.0
|
|
animation: str | None = None
|
|
params: dict[str, Any] = {}
|
|
|
|
|
|
class BlockUpdate(BaseModel):
|
|
timestamp: float | None = None
|
|
duration: float | None = None
|
|
animation: str | None = None
|
|
params: dict[str, Any] | None = None
|
|
|
|
|
|
@router.post("/shows/{show_id}/tracks/{device_id}/blocks", status_code=201)
|
|
async def create_block(show_id: str, device_id: str, block: BlockCreate):
|
|
show = await state.show_store.load(show_id)
|
|
if show is None:
|
|
raise HTTPException(404, "Show not found")
|
|
track = next((t for t in show.tracks if str(t.device_id) == device_id), None)
|
|
if track is None:
|
|
# Auto-create track for this device
|
|
from lightsync.models.show import TrackModel, CueModel
|
|
cue = CueModel(
|
|
id=UUID(block.id), timestamp=block.timestamp,
|
|
duration=block.duration, animation=block.animation,
|
|
params=block.params
|
|
)
|
|
track = TrackModel(device_id=UUID(device_id), cues=[cue])
|
|
show.tracks.append(track)
|
|
else:
|
|
from lightsync.models.show import CueModel
|
|
cue = CueModel(
|
|
id=UUID(block.id), timestamp=block.timestamp,
|
|
duration=block.duration, animation=block.animation,
|
|
params=block.params
|
|
)
|
|
track.cues.append(cue)
|
|
await state.show_store.save(show)
|
|
return {"status": "ok"}
|
|
|
|
|
|
@router.patch("/shows/{show_id}/tracks/{device_id}/blocks/{block_id}")
|
|
async def update_block(show_id: str, device_id: str, block_id: str, update: BlockUpdate):
|
|
show = await state.show_store.load(show_id)
|
|
if show is None:
|
|
raise HTTPException(404, "Show not found")
|
|
track = next((t for t in show.tracks if str(t.device_id) == device_id), None)
|
|
if track is None:
|
|
raise HTTPException(404, "Track not found")
|
|
cue = next((c for c in track.cues if str(c.id) == block_id), None)
|
|
if cue is None:
|
|
raise HTTPException(404, "Block not found")
|
|
if update.timestamp is not None:
|
|
cue.timestamp = update.timestamp
|
|
if update.duration is not None:
|
|
cue.duration = update.duration
|
|
if update.animation is not None:
|
|
cue.animation = update.animation
|
|
if update.params is not None:
|
|
cue.params = update.params
|
|
await state.show_store.save(show)
|
|
return {"status": "ok"}
|
|
|
|
|
|
@router.delete("/shows/{show_id}/tracks/{device_id}/blocks/{block_id}")
|
|
async def delete_block(show_id: str, device_id: str, block_id: str):
|
|
show = await state.show_store.load(show_id)
|
|
if show is None:
|
|
raise HTTPException(404, "Show not found")
|
|
track = next((t for t in show.tracks if str(t.device_id) == device_id), None)
|
|
if track is None:
|
|
raise HTTPException(404, "Track not found")
|
|
track.cues = [c for c in track.cues if str(c.id) != block_id]
|
|
await state.show_store.save(show)
|
|
return {"status": "ok"}
|