Settings → Offline cache lists every saved video; it now sits in a collapsible
'Saved videos' section showing the count and total size, closed by default,
remembering how it was left.
Queue reviewable Whisper drafts from the song list and lyrics editor. Preserve line breaks within one timed cue across editing, saving, reporting, and service views.
Add storage and listening analytics with a durable metadata collector, related-search depth, video limits, thumbnail storage, and a browsable metadata library.
The CH badge by the scrubber is now a button opening a list of every chapter
(number, title, start time); the playing one is highlighted and scrolled into
view, a tap seeks there (and resumes if paused). Works with shared chapters and
YouTube's.
They relied on the hidden attribute, which browsers do not reliably honour on
SVG elements, so in Glass Stage each nav item drew a full-size black icon.
- Channel pages join the select-mode views: tap Select in the filter row, pick
videos (or Select all — respects the filter), then + Queue / + Playlist.
Delete is hidden there (nothing to delete on a channel).
- --bottom-chrome (the fixed bottom nav + mini player height) is now measured
whenever the mini player shows/hides and on resize/rotation, not only while
the section rail scrolls, and the list pane pads its end by it, so the last
card scrolls fully clear with or without the mini player.
Stray pinches and double-taps kept zooming the installed app. On iOS only the
viewport gets maximum-scale=1/user-scalable=no, Safari's gesture events are
cancelled and the page uses touch-action: manipulation. The player's own pinch
zoom (pointer events) still works.
Shown as soon as an update is offered, so Later, a backdrop tap or a failed
download all leave a one-tap way to update; it targets the newest build and
disappears while updating. The search form and box now shrink so the icon
never pushes Search off a phone screen.
In service mode's Video view the overlay lives inside the player pane, which
the home-over rule hid, while service mode kept the bottom bar and mini bar
hidden. Going home now leaves service mode first, and the rule spares the
service overlay.
- fitLyricLines measures text on a canvas with the line's own font and writes
every --fit after measuring (it used an in-list DOM probe and wrote inside
the loop, re-laying-out the whole list per line). 1200 lines: 600 ms -> 28 ms;
sizes match the old method within 1 px. Service mode on long sermons with
hundreds of transcribed lines opened slowly enough on phones to look broken.
- Bottom-nav Search while something plays shows the home screen (playlist
board / hero) over the player pane; the media keeps playing unseen and the
mini bar brings the player back (its close button too, so there is always a
way back). Starting another video restores the player.
Server
- /api/download/:id answers Range with a strong ETag ("<id>.<gen>") and honours
If-Range; a stale partial gets the whole current file (streamed, so Bun does
not re-apply the Range itself). Uploads get the same treatment.
- GET /api/download/:id/prepare never blocks: ready {gen,size,sha256,etag,ext},
working (server still fetching), legacy (HEVC / too long / cache offline),
failed. Recently refused prepares are remembered for 10 minutes.
Browser
- The OPFS worker saves in 8 MiB ranges, writes at the byte offset, flushes
each chunk, retries each chunk 6 times with backoff (30 s idle timeout) and
keeps the .part plus a .part.json sidecar naming the server copy it belongs
to. A changed copy restarts cleanly; the finished file is hashed once and
checked against the server's SHA-256.
- SaveQueue remembers unfinished saves and resumes them on start, online,
return to the foreground and every 2 minutes while visible; one at a time.
- Downloads shows live MB progress, 'Preparing on server', 'Verifying', and
paused saves with Resume and Cancel (confirmed).
- listVideos ignores the sidecars; new listPartials/discardPartial helpers.
Verified in Chromium through a connection-dropping proxy: paused at 8 MiB,
auto-resumed after a reload from byte 8388608, final SHA-256 matched.