The Library and browse screens
The Library tab and every browse screen (Home's shelves, the series and person
pages, collections) are built from the pieces in src/components/library/ and
src/components/series/. The routes that show them are in the
route map.
The Library modes
LibraryScreen (src/components/library/library-screen.tsx) is the Library tab
root. Its sections are the modes of library-modes.ts: books, authors,
series, narrators, collections and folders. parseLibraryMode reads the
mode from ?mode=, and an unknown or absent mode means books. The screen
writes the mode with router.setParams. The default carries no param, so the
plain link stays /library.
- Capability-aware.
authors,seriesandnarratorsneedbrowse_people, andcollectionsneedscollections. Both flags are read withuseCapabilityfor the selected library's connection.availableLibraryModesoffers only modes whose flag is known to be on. A mode waiting on its flag is left out, rather than shown and taken away.resolveLibraryModekeeps a deep-linked mode while its flag is still unknown, and falls back to Books once the flag is known to be off.- The segment counts come from the gated
useAuthors,useNarrators,useSeriesListanduseCollections. Books and Folders have no count.
- One selected library. Every mode but Folders shows one library.
useSelectedLibrary()reconciles the device-localuseLibrarySelectionpick with every connection's library list. It never puts another server's library in place of one whose server has not answered. The screen shows that server's state instead. The order of the pick, those states and the picker's labels are under Library selection.LibraryPicker(published withSubNavActions, order -100) chooses the library, grouped by server.- In Collections mode the picker is
by="server", because a collection belongs to a server. - Each mode body is keyed on
connection:library, so switching remounts it cleanly.
- Mode bodies live in
src/components/library/modes/:BooksMode(see Books mode);AuthorsModeandNarratorsMode, on the sharedPeopleModeinsrc/components/series/people-mode.tsx: portrait cards, letter heads, and the "no author listed" count;SeriesMode: series cards with a mini shelf of owned spines and dashed gaps. A card fetches its books once it shows. On aseries_booksserver, the cards that show together share one request (see Series cards share one request);CollectionsMode: Favourites, own and shared collections, and New collection;FoldersMode: the old libraries-then-folders flow and its Favourites row, unchanged.
Books mode
Books mode is the one place that loads a whole library.
useWholeLibrary(connectionId, libraryId) (over useAllLibraryBooks) pages
GET /libraries/{id}/books 200 at a time, always newest first, until
next_cursor runs out. It has its own key under the libraryBooks prefix, so
its pages never mix with the 100-book pages of useLibraryBooks. There is one
cache entry per library whatever the screen's sort, because every sort happens
on the device. The pages loaded so far are usable at once. A failed page keeps
what loaded, and Retry carries on. While a page waits for the network
(offline), the books loaded stay and "Loading more" hides. With nothing loaded
the grid is the offline state, never a skeleton
(see Offline states).
The view is books-view.ts, which is pure:
parseBooksViewandbooksViewParamsare the URL contract:sort,status,dl=1,len,genreandtag. Defaults are omitted.filterBooksfilters on the device. It matches a genre or tag bylabelKey: lowercased, with spacing collapsed.statusCountsgives each chip's count among the books the other filters keep.labelOptionsgives the genre or tag choices, each counted the same way. The chosen one is kept even at 0.sortBookssorts. Title order folds accents and skips a leading article. An untitled book sorts, and shows, by its folder name throughbookTitle.letterGridbuilds the title sort's letter heads and A-Z rail.lengthBucketplaces a book in one of thelenchips. Home's Short listens shelf reuses it.
books/label-filter.tsx shows the genre and tag choices as a picker chip that
opens a dialog. It shows only when a loaded book has a label or the URL names
one, so an older server shows no new control. With any filter on, BooksMode
leads the chip row with Clear and then the chosen genre or tag chip, and the
unchosen pickers stay at the end. It re-keys the row by the chosen kinds, so the
row starts scrolled to its start.
The grid or list choice is useBooksLayout (audiosilo.booksLayout), which a
collection and Favourites use too.
The book menu
The book menu is useBookActions, presented by BookActionsMenu
(books/book-actions.tsx). The list view's BookActionsButton opens it, and so
does every CoverTile. Its items:
- Play or Resume;
- Up next (
queue); - Add to collection (
collections); - download;
- Mark as finished. Undo is offered only with
progress_edit. The mark is sent as afinishevent where the server takes one, else as a progress edit withprogress_edit, else asuseMarkFinished's save (see State & data); - Mark as not finished (
progress_editonly); - More in this series.
A capability that is still unknown hides its item. Remove download asks first
through the shared RemoveDownloadConfirm (see
Offline).
Find on LibriVox (admin)
src/components/librivox/ is the player's side of
Add from LibriVox: an admin searches the community
metadata's LibriVox works and adds a recording to a books library, which the
server's Bridge downloads. The console's Library > LibriVox is the same page, so
the two keep the same rules.
The gate. librivoxAccess(user, server) (librivox-model.ts) needs an admin
(isBridgeAdmin), the bridge flag with librivox in bridge.features, and
metadata. mayAddLibrivox is its allowed. Unlike the console, the player
offers nothing while the Bridge is offline (see
The gate). Without allowed the page shows only
a sentence (notAdmin, unavailable, metadataOff): the search and its hooks
never mount.
Once the page was allowed for a connection, it stays when a later /server read
finds the Bridge away (unavailable: its heartbeat lapsed, so /server dropped
its features). The search and the followed fetches are kept, a caption says so
(librivox.access.away), and Add and Cancel are held back (bridgeAway) until it
is back. While away, the page asks /server again every BRIDGE_IDLE_POLL_MS
with the app in the foreground, since nothing else reads it on an open page.
notAdmin and metadataOff still close the page. A /server read that failed
with nothing kept shows librivox.access.readFailed with Retry.
A link without ?connection= is the default connection's (useCid, as Find
shows). A link whose library the server doesn't have shows
library.detail.badLink, once the library list has settled (not while it loads
or refetches).
The way in. LibrivoxMenu is a ... menu in the Books mode's controls
(books-controls.tsx's trailing slot: the sub-nav row on a tablet or desktop,
the count line on a phone) with one item, Find on LibriVox. It renders
nothing for anyone else. It opens /find-librivox?connection=&library=
(findLibrivoxHref, openFindLibrivox), the Library tab's own page like Add
shows. It is a browse page (BROWSE_PATHS), so opening it keeps the Library's
scroll memory.
The wire (client.ts, hand-mirrored from handlers_librivox.go):
| Method | Route |
|---|---|
librivoxSearch(q) | GET /admin/librivox/search (the server's lang and limit are never sent) |
librivoxWork(id) | GET /admin/librivox/work |
librivoxFetch(lib, id) | POST /admin/librivox/fetch, 202 {job} |
bridgeJob(id) | GET /admin/bridge/jobs/{id} |
addRefusalKey and readErrorKey word the refusals: in_library (the work read
again shows where; the body's library_id and path are not read),
not_book_library, kind_unavailable, library_not_writable, metadata_off,
not_found and, for a read, invalid.
The hooks (hooks.ts):
useLibrivoxSearchanduseLibrivoxWorkaremetadatareads withretry: false.useAddLibrivoxis ametadatawrite. It seeds the job's own read and asks the connection's works again whatever the answer.useLibrivoxJobis abridgeread, and Cancel isuseCancelBridgeJob.storeJobputs a job about no book at its own read (qk.bridgeJob).
A librivox.fetch has no book path, so a row follows it by its id, not through
useBridgeJobs or useBridgeJobSurface, which are keyed on a book. It is polled
while open and the app is in the foreground (librivoxJobPollInterval: 5 s, 60 s
while parked) and never after a 404. A first read that failed with no answer or a
server error is asked again every 60 s; a refusal (400, 401, 403) is not.
The page. One scroller holds the search (asked on submit only), the works found, and the opened work's recordings under it.
- Add goes to the library the page was opened from, or the one picked when the server has several books libraries. Podcast libraries are never offered.
- A row follows the last job it queued or saw open on the work
(
useFollowedFetch) and switches only when the work names a different job, so a failure still says why and an older answer never takes the row back from the job it just queued. fetchViewwords the job: its step (manifest,download,cover,place,rescan) or its code (librivox.fetch.code.*). The Bridge's English is never shown.- A job's end is handled by the fetch watcher (below), whether the page is open or not.
- The status is
JobStatusLayout(src/components/bridge/job-status-layout.tsx, shared with Generate'sJobStatus), with Cancel under it where Add sits. - A read with nothing to show and no network is
OfflineState(useUnreadQuery). A search or a work read again that failed keeps what it shows. - The recording's
cover_urlis never loaded: it is a third-party host, with no server proxy.
The fetch watcher (fetch-watcher.tsx) follows a fetch past the page. Every
open job a row follows, and an Add's job as it answers, goes to
watchLibrivoxFetch(cid, id), an in-memory list (nothing is persisted, so after a
restart the Library's stale time covers it). LibrivoxFetchWatcher, a root
listener, is the one poller: one useLibrivoxJob(id, true, cid) per watch,
whatever page is open, while the app is in the foreground. A row reads the same
query without polling. When a job ends, the watcher asks the works again, and for
a done one reads the library's lists and the connection's recently added again
(invalidateLibraryLists) and asks the works once more after DONE_RECHECK_MS
(the server records the book just after the job ends). It then drops the watch.
It also drops one that is gone (404), refused (400, 401, 403) or whose
connection signed out. The watcher keeps the hooks' bridge gate, so a lapsed
heartbeat pauses its reads and keeps the watch.
Strings are under librivox.* in all six locales, and fixtures are in
src/testing/librivox-fixtures.ts.
Browse building blocks
The covers and shelves every browse screen is made of (STYLEGUIDE section 8)
live in src/components/library/:
| Piece | What it is |
|---|---|
BookCover | A book's art. See BookCover. |
CoverTile | A cover as one button, which owns the book menu. See CoverTile. |
GhostCover | A book the listener doesn't have: hatched and dashed, with the real title and position from the community data and "Not in your library". When another connected server has it, the label is an info-tinted "On <server>" instead. Below 100 wide it draws the title's initials, with the full title kept in its accessible name. It never shows fake art, and it has no book menu. |
ShelfRow | A horizontal, snap-scrolling row of tiles on a FlashList, standing on a decorative Ledge. It bleeds to the window edge past the page gutter. |
CoverGrid, CoverGridSkeleton, CoverListRow | The page's grid on FlashList, its same-size skeleton, and the list variant of a book. The grid is the scroller. Full-width rows such as letter heads span every column through overrideItemLayout. |
cover-layout.ts | pageGutter, shelfMetrics (164 tiles, 132 on a phone) and gridMetrics (columns of at least 158, two on a phone; cardGrid for card grids). |
CoverWash (cover-wash{,.web}.tsx) + src/lib/cover-tint.ts | The cover-colour wash behind the Now card and the series hero. coverTint(book.cover_color) returns null, and so no wash at all, when the server sent no colour. The wash is drawn at WASH_STRENGTH (the --wash token), as SVG radial gradients on native and CSS on the web. |
FilterChip / ChipRow (src/components/ui/filter-chip.tsx) | The filter chips. |
useQueueActions / QueueButton | "Queue it" for one connection's server. See Queue actions. |
BookCover
BookCover picks its source from coverCandidates, best first:
- the downloaded copy;
- the smallest server thumbnail that covers the drawn pixels (
coverSizeFor: 160, 320 or 640), only when the server advertisescover_sizes; - the full art, which is also the fallback for a thumbnail that fails.
cover_version rides along as the cache buster. While the server's flags are
unknown, the cover shows an empty frame rather than fetching the full art first.
CoverTile
CoverTile is a cover as one button. It shows a progress bar while the book is
in progress, flags for a friend's (non-default) server, downloaded and finished,
the title and one caption line. A small cover with no art shows a two-letter
monogram (src/lib/monogram.ts). On the web desktop the tile is also the Up next
drag source.
The tile owns the book menu (TileActions, tile-actions.tsx). These open
BookActionsMenu:
- a long-press;
- a right-click. On macOS and Linux the menu opens on the button's release, so the release can't pick an item;
- the Menu key or Shift+F10;
- the screen reader's More actions.
useContextMenuRequest in src/lib/context-menu{,.web}.ts handles the
right-click and the keys. The menu is a sheet on a phone, and a dropdown anchored
to the tile's corner on tablet and desktop. It uses the screen's book row when
the screen passes one, else one item fetch on first use.
Queue actions
useQueueActions is "Queue it" for one connection's server. It offers
isQueued, queue(lib, path, position?) and unqueue, with Undo toasts. It is
hidden until queue is known to be on. It swallows a CapabilityError, and a
409 says the queue is full. QueueButton ("Queue it" or "Queued") is the
series page's action for an unread owned book.
Starting a book from a browse surface
Every surface outside the player starts a book, or jumps into one, through
usePlayBook (src/components/player/use-play-book.ts). That covers Home,
the Library, the series page, Up next, the book page, the bookmark, note and
history rows, and the Journal.
- A browse surface passes the book, with
viaBookPagefrom a list or Home. - A play or pause button passes
toggle. - A jump passes
at.
Where each one goes is the one play path.
Series and person pages
src/components/series/ holds the series and person pages:
series-model.ts: the entries, gaps, reading orders, progress track, and the one action per entry. A ghost that is not released yet carriesoutDatefrom the rails'release_date, and reads "Out Nov 12, 2026" in English.spine-fit.ts: spine titles always fit. It tightens, then shrinks, then wraps, then ellipsizes.bookcase.tsxandspine.tsx, andperson-page.tsx.use-series-data.ts: the page's reads beyond the sharedsrc/apihooksuseAllLibraryBooksanduseProgressLookup.useElsewhereBooksruns one search per other server for copies of the series elsewhere.usePlacedBooksfetches an/itemfor each book a rail places in another library.
The series page reads community rails only on a metadata server, and shares
reading-order picks with the book page (useSeriesOrderings). Under its list,
KeepAheadCard (keep-ahead-card.tsx) binds the device-wide keepAhead
setting through the Downloads page's exports. It shows only once the downloads
store knows the device can download.
Search's series results reuse the Library's SeriesCard. Each card asks for its
series' books only once it shows, batched with the other cards on a
series_books server.