* fix(player): preserve volume when toggling mute
Muting now keeps the last volume level memorized. Unmuting restores that level, including amplified values allowed by the configured maximum.
* fix(player): preserve volume for companion remote mute
Route the companion remote through the shared mute transition so it no longer overwrites the persisted volume or restores to 100%. Also correct the Dartdoc formatting for the transition record fields.
TrackRow compared bare ids (server-scoped rating keys), so a track on
another server with the same id lit up as currently playing. Compare
globalKey (serverId:id) like the rest of the queue engine.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Shared TabChipStrip replaces the plain Rows in libraries/downloads/live
TV app bars (and live TV's hand-rolled scroll wrapper); chips center
themselves on focus so d-pad reaches off-screen tabs.
Queue sheet/panel now renders the whole playback order — dimmed played
tracks above the current one — instead of only up-next. Opens scrolled
to the current track and follows advancement while parked on it.
Album/artist downloads expand to tracks with pinned parent metadata,
aggregate progress, container deletes with reference-counted album
covers, and a Music tab on the downloads screen with fully offline
album playback. Home rows include music libraries (fixes plex hub
items being filtered to video types) and audio playlists join
download/sync rules.
Un-gates music libraries; adds CardShape.square plumbing (circular
artists), artists/albums/tracks browse groupings with per-grouping
sorts, audio playlists tab, artist + album detail screens with dpad
track rows, music context-menu actions, and a stub MusicPlaybackService
seam for the upcoming engine.
The decode clamp was height-only, so ultra-wide originals could decode
far past the display budget, and two small-slot fallbacks skipped
server-side resizing entirely — handing multi-megapixel originals to
the decoder behind tiny slots. The TV spotlight background's offline
branch had no decode bound at all. All artwork now flows through a
shared both-axes bound (ResizeImagePolicy.fit: aspect-preserving,
scale-down only), and the reduced tier gets matching thumb/poster
decode caps.
Ref #1349
Extract the manage/reorder-libraries sheet from the libraries screen into
a shared widget with a public launcher, and add a settings tile (top
group, gated on having libraries) that opens it. The libraries screen
keeps its side-nav refresh and re-select-on-hide behavior via launcher
hooks; the TV dialog path lives in the launcher so the entry works there
too.
Desktop overlay sheets defaulted to a fixed 400px max height, which left
the reorder sheet ~280px tall. Both default-constraint sites now use 75%
of window height, matching mobile.
close#1468
Initialize TvDetectionService on every platform so the existing
force_tv_mode setting drives the 10-foot TV interface on Windows,
macOS, and Linux, and surface the toggle in appearance settings there.
Desktop keeps real-mouse behavior in TV mode: InputModeTracker still
flips between pointer and keyboard modes (cursor hidden while
keyboard-driven), segmented controls keep their hover affordance, and
the settings backup section stays available (only Android TV lacks a
document picker).
Adds PlatformDetector.debugSetIsDesktopOSOverride so TV-device
simulations in widget tests don't inherit the desktop test host's
platform.
close#1409
Profile traces showed 100-370ms UI-thread frames while scrolling the
library screen, dominated by rebuilding and inflating media cards.
- Gate per-card focus/pointer chrome on input mode: FocusableWrapper
skips the scale/border wrappers and creates its AnimationController
lazily outside keyboard mode, and ClickableCursor plus the card tap
region only build MouseRegion/InkWell machinery on desktop - TV and
touch use a bare GestureDetector. Hub cards also drop their outer
gesture wrapper outside keyboard mode; the card's own tap region
always won the gesture arena anyway.
- Memoize sliver children (SliverChildMemo): browse/collections grids
and hub rows return identical widget instances for unchanged items,
so delegate swaps from pagination, watch-state, and deletion
setStates no longer rebuild every realized card inside layout. The
browse tab prunes the memo in lockstep with focus-node eviction so a
cached card can never resurrect a disposed FocusNode.
- Budget fresh inflation (CardInflationBudget): while a scrollable is
moving in pointer/touch mode at most one new card inflates per frame,
the rest render as SkeletonMediaCard and upgrade on following frames.
Hub rows also stop pre-inflating 250px of off-screen cards on entry.
Device traces: worst frame 373ms -> 103ms, per-card build 3.6ms ->
2.4ms median; remaining row-entry work is spread across frames.
Anchor the overlay to the screen and seekbar clearance, widen the card
on height-limited screens so sections pack side by side, and scale down
as a last resort instead of clipping the bottom sections.
close#1469
The preview cleared unconditionally 2s after a committed seek; on a device
still buffering, the live position hasn't caught up yet, so the next key
press re-based off the stale position and re-targeted the same spot -
discarding the seek exactly on the slow hardware the coalescing targets.
Re-arm the clear until the live position is near the target, with a 10s
ceiling backstop (same settle pattern as LiveSeekAccumulator).
350ms sits below the typical KeyDown-to-first-repeat gap (~400-500ms on
Android/TV), so every hold committed an intermediate seek before repeats
even began. 800ms matches Jellyfin ATV (media3's DefaultTimeBar uses 1s);
key release still flushes synchronously, so taps are unaffected.
_lastDispatchedTimelineSeek was set by the drag throttle and never reset, so
with key-seeks now routed through onSeekEnd a coalesced flush targeting the
same position as a stale drag (realistic at the 0/duration clamp boundaries)
skipped the real seek while still reporting completion.
A held fast-forward fired a real seek on every key-repeat for direct/
progressive streams, flooding the source with overlapping range requests and
wedging low-power devices (e.g. Fire TV Stick) in BUFFERING until the player
was restarted. Route direct play through the same preview/debounce path the
transcode case already uses: move only the timeline preview while the key is
held and commit a single seek on release or pause.
close#1424
Backing out to home selected the home tab but let focusActiveTabIfReady
steal real focus into the content while _isSidebarFocused stayed true,
leaving the rail stuck expanded over a shifted content box that clipped
the end of every hub row. Skip the content-focus grab while the sidebar
owns focus, move the sidebar focus ring to the Home item on back-to-home,
and self-heal the flag whenever the content scope actually gains focus.
close#1411