Make the media pool a list you can actually read

Every row is one line now — a thumbnail at the stage's own 16:10, the name,
and the one number you need before dragging it somewhere — at the timeline's
own row height. Two always-open folders cost eight headings before the first
row in a pane 210px wide; they are two tabs, and a search counts its hits in
the scope you are not looking at so nothing can hide behind the one you did
not pick.

Symbols draw their own first frame, through the resolver and rasteriser the
stage uses. A PLACED symbol is drawn where it is placed, with the rest of its
host isolated away: rooting at a symbol renders its DRAWING, and a rotoscoped
face is head-local in units of one image height, so the source-to-stage scale
that makes it pixels lives on the :face group of whatever places it. Rendered
rooted at itself a face is correct and under a pixel across. Thumbnails are
smoothly downscaled for the same kind of reason the preview is not: at a tenth
of the stage's size nearest neighbour samples one pixel in a hundred, and the
silhouette is the whole of what makes a thumbnail recognisable.

Names are editable, and that is two operations behind one pencil. A symbol's
name is a field of this document, so it is an undoable edit and blank gives it
back its id. Footage and sounds live beside projects rather than inside one,
so theirs is a server write shared by every project using the row — PATCH on
the existing Footage.label, and a new Sound.label kept separate from the
filename, which is a fact about the upload and not somebody's name for it.

No TIMELINES section beside a SYMBOLS one: every symbol here IS a timeline, so
that pair named one thing twice. What is true is that exactly one of them is
where the work happens, and clip/opens-on already answers which — it leads the
pane as PROJECT, drawn at a size you can read a pose off, with the library
under it. Still not :main being special; rename it or place it inside
something else and the pool follows.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-10-01 00:56:39 -04:00
parent 815ce449ea
commit 7e34d0c704
11 changed files with 924 additions and 160 deletions

View file

@ -226,10 +226,31 @@ def sources(request):
def _sound_json(row):
return {"id": str(row.id), "label": row.filename, "duration": row.duration,
return {"id": str(row.id), "label": row.label or row.filename,
"filename": row.filename, "duration": row.duration,
"audio": f"/blob/{row.blob_id}"}
def _relabel(request, row):
"""PATCH one asset's display name.
A LABEL IS THE ONLY FIELD EITHER ROW LETS A CLIENT WRITE, and the body is
read for that key alone. Footage is content-addressed and its frame count,
rate and digest are facts about the bytes; an endpoint that merged whatever
it was sent would let a rename quietly contradict them. Blank clears it,
which puts the row back to the name it was uploaded under rather than
leaving it nameless.
"""
data = _body(request)
if "label" not in data:
raise Bad("a rename needs a label")
label = str(data["label"] or "").strip()[:200]
if label != row.label:
row.label = label
row.save(update_fields=["label"])
return row
@require_http_methods(["GET", "POST"])
def sounds(request):
if request.method == "GET":
@ -252,12 +273,18 @@ def sounds(request):
return JsonResponse({"error": str(exc)}, status=400)
@require_http_methods(["GET"])
@require_http_methods(["GET", "PATCH"])
def sound_detail(request, sound_id):
try:
return JsonResponse(_sound_json(Sound.objects.get(id=sound_id)))
row = Sound.objects.get(id=sound_id)
except Sound.DoesNotExist:
return JsonResponse({"error": "no such sound"}, status=404)
try:
if request.method == "PATCH":
row = _relabel(request, row)
except Bad as exc:
return _error(exc)
return JsonResponse(_sound_json(row))
def _extraction_json(row):
@ -367,12 +394,17 @@ def symbols(request):
return JsonResponse({"symbols": rows})
@require_http_methods(["GET"])
@require_http_methods(["GET", "PATCH"])
def footage_detail(request, footage_id):
try:
footage = Footage.objects.select_related("audio", "video", "stream").get(id=footage_id)
except Footage.DoesNotExist:
return JsonResponse({"error": "no such footage"}, status=404)
try:
if request.method == "PATCH":
footage = _relabel(request, footage)
except Bad as exc:
return _error(exc)
return JsonResponse(_footage_json(footage))