The Download/Or/Upload panel. Semantic drew it as a `Segment placeholder` with
a vertical `Divider`: two equal fields, a rule down the true middle, a button
centred in each. The migrated version was a `Group` with a divider between two
buttons, so the halves were sized to their labels -- the pair huddled in the
middle with the rule wherever they left it. `.wrolpi-or-split` is a grid, so
the halves stay equal whatever the labels say, and the rule is the second
column's border rather than an element between them (an element would be a third
grid item and push the middle off centre by its own width). Below the tablet
breakpoint the halves stack and the rule turns horizontal, as Semantic's
`stackable` grid did.
The video page's panels touched the left edge. Its route is deliberately
outside `VideosTabLayout`'s `PageContainer` so the player can run to both edges
-- which is the most viewing area a phone can give it, and worth keeping -- but
everything below the player inherited that too. `.wrolpi-page-inset` gives the
stack under the player the same inset `PageContainer` gives a page, at the same
breakpoint, so a phone still has no side padding anywhere.
A video's duration chip was a literal `#ffffff` with black text. The poster
under it IS filtered by night mode, being a `.media` leaf, but the chip is drawn
beside the image rather than inside it, so the filter never reached it: a white
tile on a card whose picture had gone dim red, and the brightest thing on the
videos page in the theme whose whole point is that nothing is bright. Tokens
now, opaque because it sits on an arbitrary poster.
Nine guards, all verified to fail against the old code. One thing they turned
up: cypress component tests default to a 500x500 viewport, which is BELOW the
699px breakpoint, so viewport-keyed CSS is exercised in its phone branch unless
a test says otherwise. The desktop assertions set the viewport explicitly and
say why; the stacked branch has a test of its own.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Five findings, all valid. The substantive one: the video page's Download /
Delete / Refresh / AddToPlaylist strip was still four bare buttons in a `<p>` --
the direct analogue of the archive row this PR started with -- and the scan that
was supposed to catch exactly that reported the app clean.
It only ever saw `{fooButton}` interpolations, and most of the app writes its
buttons out in place. Scanning for adjacent button ELEMENTS finds twelve more
rows in the same state: reorder icons in the playlist and field-schema tables,
both service tables on the controller page, its hotspot Save/QR pair and its
Restart/Shutdown pair, the downloads footer, the one-time pad's Generate/Cheat
Sheet, and the ration calculator's CSV/Print. All rowed.
Two holes had to be closed before that scan could be trusted, because a scan
that reports noise is worse than one with a stated limit:
- `tagsIn` never captured the tag NAME, so `ROW_COMPONENTS.has(undefined)` was
false for every tag and the list of components that lay out their own
children -- Group, ButtonGroup, Modal.Actions -- silently accepted none of
them. 56 false positives, every modal footer in the app among them.
- A row whose style is a const (`style={seasonRowStyle}`) says nothing in its
own attributes. Resolved now; the dates selector's seasons row is a
four-column grid with a gap and was reported three times.
Also from review: the row body is found by walking to the MATCHING close rather
than the first `</div>`, which one nested div would have truncated -- and the
collection form now nests one, so this was about to matter. Cancel and Save
travel together in a row of their own pushed to the end; `margin-left: auto` on
Save alone was right for a row that could not wrap, and would now drop Save onto
a second line by itself with Cancel left among the action buttons. The doc
page's comment no longer implies the picker's 1em separation was not a change.
One more found on the way: the file browser's filter row is flex with no gap, and
its collapse-all button carried `margin-left: 0.5em` for it. The gap is the
container's.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reported: the Back button fills the page width on the domain and channel edit
pages. It fills it on all nine pages that have one, and this is mine from the
last PR.
`.wrolpi-stack` is a flex column at `align-items: stretch`, deliberately -- a
panel was full-width as a block element and has to stay that way. A BARE button
as a direct child of that stack is stretched too, and every page's Back button
was one. On the domain, channel and playlist edit pages it also pushed the
button beside it onto its own line, where the two had been inline.
Wrapping each in a `.wrolpi-button-row` fixes both without touching the stack:
the row is the stretched child, and it lays its buttons out at their own width.
The three pages that have a second button get one row holding both, which is how
they read before.
Also the Videos, Archives and Documents select bars, whose Tag/Delete/Invert/
Clear were four touching buttons in a bare div. They are inline elements rather
than interpolations, which is the shape the bare-siblings scan does not see.
Guarded twice: the row keeps its buttons their own width inside a stack while
itself stretching (both halves, since a row that refused to stretch would pass a
test that only looked at the button), and every `<BackButton/>` in the app is
inside a row.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Panels shared edges: two 1px borders meeting, which reads as one tall
surface with a rule across it rather than a seam between two.
Done as `margin-bottom` on the panel first, which is the anti-pattern, and
it failed in both of the ways that rule is known to fail before the reading
was done. It compounded with the gap of every Stack that already held
panels -- four of them, spacing those lists at 35.2px against the app's
17.6px -- and the `:last-child` reset meant to keep trailing space off the
bottom of a container silently did nothing, because Media wrapping makes a
panel the only child of its own wrapper. On Status that separated four
pairs out of five and left the first touching.
So the space belongs to whatever stacks the blocks. `.wrolpi-stack` is on
PageContainer, which every page uses, and on the three wrappers that stack
blocks somewhere the page's own stack cannot reach: Settings' panel wrapper,
the block below the video player, and the Zim and Map route wrappers, which
hold their tab bar outside PageContainer.
A gap rather than `> * + * { margin-top }`, which was tried and is quietly
broken here: Media injects `.fresnel-container { margin: 0; padding: 0 }` at
runtime, same specificity, later source order, so every wrapped block threw
the margin away. A child cannot zero its parent's gap. It also means
Media's hidden half contributes nothing, where the owl selector spaced
around something invisible.
With the page owning the spacing, the blocks that owned some of it had to
give it up, or their margin was added to the gap: the tab bar (33px against
17.6 everywhere else), the shared search control bar and paginator on
Videos/Archives/Docs/Files, the card grid, the file browser's filter row and
the inventory search. Two margins deliberately stay -- the file browser's
4em, which reserves room for the sticky buttons (62645831), and heading
margins, which bind a heading to the content below it.
Tests plant each of the three failures: the wrapped block, a child that
zeroes its own margin, and a hidden Media half. Verified in the browser on
sixteen routes and in all four themes -- every stacked pair 17.6px, the
map's 85vh box and the video player unchanged, no horizontal overflow.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces semantic-ui-react and ./Theme imports with the token-driven ./ui
library: Segment->Panel, Tab->Tabs (Tabs.List/Tab/Panel, keepMounted=false
to preserve per-tab lazy render/fetch), Loader active->Loading, Input->
TextInput, Grid/Grid.Column->Mantine Grid/Grid.Col, Container->plain div,
color=/APIButton->role=. Comment/CommentGroup and Transition have no ui
equivalent, so the comment thread and audio-poster fade are rebuilt from
plain elements/tokens. The audio poster wrapper (custom play/pause/volume
glyphs drawn over the poster image) gets .media so night mode tints the
composite as one unit; the video/audio elements and poster img stay
untagged since they are already filtered leaves. Added ARIA button
semantics to the poster click target since it has no native href/button.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Require `line === 'auto'` in the auto-caption signature so a cue that
is deliberately placed (e.g. `line:0` for top-edge) is left alone even
if it is also `align:start position:0%`. YouTube auto-captions never
set `line`.
- For lazily-loaded language tracks, only detach the cuechange listener
after the cue list is actually populated, in case a cuechange fires
before cues load.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
YouTube auto-generated captions (downloaded via yt-dlp) stamp every cue
with `align:start position:0%`, which pins the caption text to the
bottom-left of the player and makes it hard to read. Properly-authored
VTT files have no cue settings and already render centered.
Detect that auto-caption signature and reset only those cues to the
browser's centered default (align:center / position:auto / line:auto).
This mutates the in-memory VTTCue objects the browser parsed; the .vtt
file on disk is never modified, and deliberately-positioned cues are
left untouched.
Wired via each <track>'s onLoad. Non-default language tracks are
`disabled` at load (no cues yet), so they are normalized lazily on the
first cuechange when the user selects them.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add Playlists: ordered, mixed-type collections
A playlist (Collection kind='playlist') is a manually-curated, ordered list that
mixes item types. Squash of the playlists feature branch (24 commits).
Data model & API:
- CollectionItem is polymorphic via item_kind: 'file' (FileGroup), 'zim'
(zim_id + zim_entry), or 'url' (link to anything the WROLPi browser opens,
e.g. /map?lat=&lon=&z=). CHECK constraint enforces exactly one kind; per-kind
unique constraints; migration a7b1c2d3e4f5.
- Endpoints: create playlist; add/remove/reorder items (idempotent adds: 201 new,
200 existing); GET returns the ordered items. All mutations wrol_mode_check'd.
Config (source of truth):
- playlists.yaml (PlaylistsConfig) stores each playlist's metadata AND ordered
items (file by relative path, zim by path+entry, url inline); round-trips a
DB wipe. Supports backup import (DB_CONFIG_FILE_NAMES).
- The redundant generic CollectionsConfig (collections.yaml) is deleted:
channels/domains own their configs, authors/subjects are re-derived.
On-disk sync (sync_playlists_directory switch):
- Each playlist materializes into the configurable Playlists Directory
(playlists_destination, Settings UI input, excluded from indexing) with
position-prefixed entries: NNNN_<file> hard links (copy fallback across
filesystems) and NNNN_<title>.html stubs for zim/url items.
- Tagged playlists are namespaced like channels: <playlists>/<tag>/<name>;
a tag change moves the directory and prunes the empty old tag dir.
- Deletion safety: managed files are only removed when they are our marked
stubs or still have another hard link (st_nlink > 1) -- never a last copy.
Stray loose files at the playlists root are removed; stub URLs are
scheme-allowlisted (no javascript:/data:).
Frontend:
- /playlists list page (CollectionTable, name filter, New Playlist modal, Tag
column) between Files and Zim in the nav.
- View-only detail page (Name link) and edit page (Edit button): themed table
with model icons/colors + poster thumbnails, items link to their model page
(video/archive/doc) or preview, up/down reorder, remove, add-URL form,
single-tag selector; all editing disabled in WROL Mode.
- Reusable green "Add to Playlist" button on Video/Archive/Doc detail pages,
the FilePreview toolbar, Zim search results, and Map pins (added as a /map
URL item with a map-marker icon).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Playlist edit page parity: rename, custom directory, tag modal move suggestion
Match the ChannelEditPage/DomainEditPage UX on the playlist edit page:
Back/View buttons, an edit form (name, description, directory), and the
shared CollectionTagModal with the violet "Tag" button. The tag modal now
suggests moving the playlist to its managed tag location
(playlists/<tag>/<name>); accepting the suggestion stores directory=None so
the sync keeps auto-managing the location. Tag buttons on the Channel and
Domain edit pages are now violet to match.
Backend: update_collection learns rename (with per-kind validation and
duplicate checks) and playlist custom directories (cleanup of the abandoned
directory, config round-trip, commit-before-switch to avoid the background
sync racing pre-commit state). tag_collection's duplicated untag/tag
branches are merged, and the per-kind config-save dispatch is consolidated
into _activate_config_save().
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Deduplicate Collection error handling and Tag creation
Delete the duplicate UnknownCollection in wrolpi/collections/errors.py and
re-export the canonical one from wrolpi/errors.py, which carries
status_code=NOT_FOUND (the local copy inherited 500). Replace the repeated
query/raise pattern in reorganize.py with Collection.find_by_id.
Add Tag.get_or_create_tag and use it for collection tagging; flush the new
Tag before returning so callers can read .id immediately -- previously
tagging a collection with a brand-new tag name set tag_id=None. Regression
test included.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Storing video settings in session storage to help user start downloads consistently.
Applying channel tag name to new channels when download videos or channels.
Initializing map tiles when importing and resetting map.
Displaying star for Uploader's comments, heart for favorited comments.
Adding wrolpi help files desktop shortcut to rpi image.
Using zim file names as FileGroup.title.
Removing accidental output commit.
* A new zim module has been added, this module allows several things to interact with Zim files.
* Allow searching of Zim files using libzim.
* Allow downloading of Kiwix projects (wikipedia, wikibooks, etc) on a schedule which replaces the outdated files.
* Creation of a new zim docker container which allows viewing of the Zim file contents.
* This commit also adds move/rename/mkdir features in the File Browser.
* File tags are preserved when moving/renaming.