The custom sidebar read as an ad-hoc widget: collapsed rows were the
expanded layout clipped to the strip, the libraries tree used a
staircase of indents and mixed row heights, and hover expansion pushed
the whole content area sideways.
Rebuild the rail on M3 Expressive geometry: collapsed destinations are
icon-only 56x32 pills centered in an 80px strip (48px icon-only on TV),
expanded rows are fixed-height destinations with a full-width stadium
indicator, and the two layouts crossfade/lerp during the width morph
instead of clipping. The libraries section is flattened into full-width
destination rows under a chevron header at standard destination
metrics, hover/touch expansion overlays content as a rounded, shadowed
panel over a dimmed scrim instead of pushing it, and rail width, item
morphs, content translate, and bleed all share one duration/curve.
Collapsing Libraries in the sidebar only lasted until the app closed. The
next launch always came back expanded, which buries the rest of the nav
menu for anyone running a lot of libraries.
The expansion flag was plain widget state on SideNavigationRailState. It
survived tab navigation only because MainScreen pins the rail with a
GlobalKey, and died on relaunch, profile switch or a layout change. It now
lives in the librariesSectionExpanded preference, so the rail reads and
writes the persisted value and follows a settings reset or import too.
close#1896
The Explore tab appears for every Plex-backed profile because the Plex
Discover catalog source connects implicitly, with no way to opt out
short of a Jellyfin-only profile. Add a Show Explore Tab switch to
Appearance > Navigation that hides the tab in the bottom navigation and
side rail. UI-only: catalog sources stay connected so watchlist
surfaces keep working while the tab is hidden.
close#1844
- PaginatedCardGridTabState: the collections and playlists tabs were 95%
identical; they now supply only pageSize/fetchPage/idOf instead of each
duplicating the grid, memo, inflation budget and focus wiring.
- EtagCachedRemoteStore: the anime-lists and fribb mapping stores now share
one download/cache/isolate-parse/conditional-GET lifecycle.
- FocusableTileStateMixin manages its own initState/didUpdateWidget/dispose
instead of requiring every caller to forward three lifecycle hooks.
Also drops unused ServerCapabilities entries and dead code in
focusable_list_tile and music/track_row.
Collapse duplicated setup across the suite into six shared helpers under
test/test_helpers/ and rewrite the 28 suites that were open-coding it:
http_fixtures.dart jsonResponse() for http.Response JSON stubs
library_tab_scaffold.dart pumps library tabs under their required ancestors
multi_server_fixtures.dart MultiServerProvider wiring for widget tests
playback_report_fakes.dart PlaybackReportCall + fake report sinks
profile_stack.dart production-shaped profile dependency graph
theme.dart testMonoTokens for fast-settling widget tests
Net -1245 lines with no change in coverage or assertions.