Commit Graph
100 Commits
Author SHA1 Message Date
edde746 422db75b5b feat(music): mpv audio playback engine with gapless queue service
Audio-only mpv core on every platform (dedicated
com.plezy/mpv_audio_player channels): parameterized android/windows/
linux mpv plugins and a new apple MpvAudioPlayerCore, all skipping
video/window paths (vid=no, audio-display=no, gapless-audio=weak).
MusicPlaybackService drives an in-memory queue with shuffle/repeat,
file-loaded-event gapless arming (property edges coalesce and the
android bridge drops them), per-track progress reporting, OS media
controls, audio focus, sleep timer, and error auto-skip.
PlaybackCoordinator enforces one live native player: starting video
disposes the audio core first.
2026-07-05 19:26:28 +02:00
edde746 05a631415e feat(music): music browsing UI — square cards, library groupings, artist/album screens
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.
2026-07-05 17:20:28 +02:00
edde746 fff559da06 feat(music): add music data layer for plex and jellyfin
Backend-neutral fetchArtistAlbums/fetchAlbumTracks/fetchInstantMix/
fetchLyrics, lyrics model + LRC parser, audio quality presets, music
transcode (plex /music universal, jellyfin audio PlaybackInfo profile),
station/InstantMix radio, music search, playlist type derivation, and
audio download resolution.
2026-07-05 15:55:52 +02:00
edde746 c87a9060f9 chore: tidy formatting and local ignores 2026-07-05 14:32:21 +02:00
edde746 ee3c75ba9e fix(playback): remember last played media version for continue watching
close #1492
2026-07-05 14:29:57 +02:00
edde746 90c4fe5458 feat(audio): add Kodi-style stereo downmix with center channel boost 2026-07-05 14:29:57 +02:00
edde746 97901cba87 chore(deps): bump background_downloader 2026-07-05 14:29:57 +02:00
edde746 7f81a0d4fd fix(windows): retry connectivity listener registration outside input-sync calls
INetworkListManagerEvents Advise fails with
RPC_E_CANTCALLOUT_ININPUTSYNCCALL when the platform thread is inside an
input-synchronous call at subscribe time, and an OnListen error can
only surface in Dart as an uncatchable fatal FlutterError. The fork now
retries the registration via a message-loop timer and reports only
persistent failures through the event sink, where stream onError
handlers observe them.
2026-07-05 14:16:05 +02:00
edde746 173117602a fix(android): vendor saf_util with a sound pendingResult lifecycle
saf_util 2.0.0 leaves an already-answered MethodChannel.Result behind
in pendingResult: unrelated activity request codes consume (and answer)
the pending picker, and a failed picker launch replies without clearing
the pending state. The next delivery for request 1001 then replies a
second time — IllegalStateException "Reply already submitted" in
MainActivity.onActivityResult, crashing the process.

Vendored at packages/saf_util (BSD-3) with take-and-clear reply
ownership, request codes scoped to the plugin's own pickers, and a
guarded reply on the teardown race.
2026-07-05 14:16:05 +02:00
edde746 d9130cd932 fix(detail): guard on-deck fallback against a disposed screen
_ensureFallbackOnDeckEpisode is reached through unawaited fetch
continuations and reads providers through State.context — leaving the
screen before the season fetch settled crashed on the null-check in
the context getter.
2026-07-05 14:16:04 +02:00
edde746 be072365fc fix(player): scope event-channel cancel to the owning instance
Player backends share static per-backend EventChannel names, so two
overlapping instances (episode handoff, quick exit/reopen) collide:
the newer instance's listen displaces the older sink, the older
instance's late cancel then tears down the newer stream — silently
freezing its events — and the final cancel earns the engine's
"No active stream to cancel" reply, which the framework can only
report as an uncatchable fatal FlutterError (363 events).

Track the owning instance per channel: only the current owner sends
the native cancel; a displaced instance drops its dead subscription.
Residual benign teardown races are dropped in beforeSend.
2026-07-05 14:16:04 +02:00
edde746 61f7d33fdf fix(profiles): treat vault decrypt failures as lost credentials
A failed MAC check (key/ciphertext divergence: restored backup,
clobbered prefs, racing key generation across isolates) threw from
CredentialVault.reveal on the startup profile-settings path and
crash-looped the app until data was wiped — one device logged 31
fatals in 16 minutes on 2.8.0.

Decrypt failure now means the credential is lost, never a crash:
reveal() returns null, ProfileConnectionRegistry maps it onto the
existing empty-token lazy-fetch sentinel and heals the row so later
boots re-acquire the token instead of re-failing, and
revealConnectionConfig degrades tokens to empty strings without
marking them migrated. Key init also reloads prefs before deciding to
generate and re-reads after writing, adopting whatever landed so all
isolates converge on a single key instead of orphaning ciphertext.
2026-07-05 14:16:04 +02:00
edde746 3766b2c2d3 fix(remote): handle connect failures of losing race candidates
Race candidates created with IOWebSocketChannel.connect fail their
ready future on connect timeout or an unreachable address, and nothing
observed it — web_socket_channel reports connect errors there, so every
losing candidate surfaced as an unhandled fatal (the tracker's single
largest crash source, ~1300 events: bare 5s TimeoutException plus the
recurring no-route-to-host WebSocketChannelExceptions). The stream
listener's onError only covers post-connect errors. Swallow and log the
ready failure per candidate; the race outcome is unaffected.
2026-07-05 14:16:04 +02:00
edde746 0dfe013928 fix(home): include clip libraries in per-library home rows
close #1476
2026-07-05 09:43:34 +02:00
edde746 25c7893a8e fix(windows): report native crashes through the inproc sentry backend
Crashpad uploads minidumps straight to the DSN's /minidump endpoint,
which our tracker does not implement — every native Windows crash
(e.g. faults inside libmpv threads, #1490) was silently dropped.
The inproc handler stackwalks at crash time and reports through the
regular envelope endpoint, and crashpad_handler.exe/crashpad_wer.dll
leave the bundle.
2026-07-05 09:10:46 +02:00
edde746 28ec9cacb2 perf(tv): evict non-adjacent season episode pages on low-end hardware
Visited seasons' 200-item pages were retained for the screen's
lifetime — irrelevant for a 3-season show, tens of MB of Dart heap for
a 30-season one. Low-end TV now keeps the prefetch window (selected
season plus/minus one); evicted seasons transparently refetch through the
existing unloaded-hub path.

Ref #1349
2026-07-05 09:10:46 +02:00
edde746 aaf27a7f9e fix(android): extend the RSS eviction watchdog beyond desktop and log tier signals
Android trim callbacks are best-effort — LMK can kill without ever
delivering one — yet the RSS watchdog only ran on desktop with a fixed
1.5GB bar no 2GB TV box ever reaches before dying. The watchdog now
runs on Android with a threshold scaled to device RAM, an eviction
floor and cooldown against refetch churn, and a lower backgrounded bar
since a paused app is LMK's first candidate.

The logs-screen header and startup line now record the renderer, the
effects tier with its raw hardware signals, and the boot RSS, so
uploaded reports answer whether the reduced tier engaged.

Ref #1349
2026-07-05 09:10:36 +02:00
edde746 0fb0c7fb4c perf(android): cap engine memory pools on low-RAM devices
Flutter's imageCache budget only bounds decoded CPU bitmaps: Skia's GPU
resource cache is sized from the surface area (hundreds of MB on a
4K-composited TV) and the Dart old gen defaults to a large fraction of
physical RAM. Both drive LMK kills on 2GB boxes. Cap them via engine
shell args on hardware matching the reduced-tier triple.

Ref #1349
2026-07-05 09:10:27 +02:00
edde746 f97ab03111 fix(images): bound artwork decodes on both axes and never fetch unsized originals
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
2026-07-05 09:10:20 +02:00
edde746 3981fac0db feat(settings): surface Manage Libraries and raise the desktop sheet cap
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
2026-07-05 09:08:29 +02:00
edde746 4a43ae2771 fix(android): auto-recover frozen video after pause/resume on stalling decoders
Some TV SoCs (Amlogic Mi Box class) stall the MediaCodec output path
after pause/resume: audio and the clock keep advancing but the picture
freezes until a seek flushes the codec. Arm a watchdog on every
transition to playing with a warm decoder; if rendered frames stop
advancing while the position moves, recover with a 250ms seek-back
(same-position seeks are short-circuited without a codec flush),
capped per session and logged for field diagnosis.

close #1454
2026-07-05 09:08:29 +02:00
edde746 c4184c6148 fix(ci): restore sanity checks 2026-07-05 08:34:55 +02:00
edde746 87eaafe3c4 chore(deps): update MPVKit to v1.0.9
close #1420, close #1457, close #1484
2026-07-05 08:15:00 +02:00
edde746 aee48f6956 feat: extend Force TV mode to desktop for home-theater setups
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
2026-07-05 07:08:40 +02:00
edde746 3d5a1ec966 fix: build break from Sentry.configureScope FutureOr return
The pinned sentry fork declares configureScope as FutureOr<void>,
which unawaited() rejects; wrap in Future.sync to keep the
fire-and-forget behavior.
2026-07-05 06:49:46 +02:00
edde746 2e4ba352f4 fix: multi-trigger audio recovery after Windows sleep/wake
The two prior attempts at #783 failed for lack of a reliable trigger:
re-setting audio-device to its own value is a no-op, and gating
recovery on audio-device-list changes while current-ao is null never
fires in the real flows (a paused player only discovers the dead
WASAPI session at unpause, when the device list is already stable;
a zombie session that consumes samples silently never goes null).

Replace the single gate with a recovery scheduler on the mpv event
thread, ticked every wait-event timeout:

- WM_POWERBROADCAST resume (both PBT variants, via the plugin's
  existing window-proc delegate) requests unconditional ao-reloads at
  +1.5s and +6s after wake — the only trigger that reaches zombie
  sessions; the second shot covers a first reload landing while the
  audio stack is still restoring. The handoff is a single atomic
  flag: no mpv calls or timers from the platform thread, so there is
  nothing to clean up on dispose.
- current-ao falling to null arms clock-driven retries (budget 5,
  x2 backoff capped at 8s); retries must be clock-driven because a
  failed ao-reload falls back to null without a property change
  event. A device-list change while null refreshes the budget, and
  audio-device-list is now observed natively so recovery does not
  depend on the Dart side.

Every trigger, attempt, and outcome logs as [mpv:audio-recovery] at
warn level so exported logs from the reporter are actionable if this
still misses. Verified with synthetic PBT_APMRESUMEAUTOMATIC while
playing and while paused: full WASAPI reinit both times, audio
restarts on unpause.

Ref #783
2026-07-05 06:49:38 +02:00
edde746 31e8c2e1e0 fix(plex): coerce PlexHomeUser fields against account-API drift
Same clients.plex.tv surface that broke in #1488: /home/users powers
first sign-in and profile setup, so a drifted scalar shape would kill
the whole fetch. Coerce strings, bools, and ints tolerantly instead of
hard-casting.
2026-07-05 06:23:41 +02:00
edde746 96510f8aac fix(plex): tolerate account-API schema drift in profile and switch parsing
Around July 3 plex.tv started returning the profile language-list fields
(defaultAudioLanguages, defaultSubtitleLanguages, mediaReviewsLanguages)
as comma-separated strings instead of arrays. The generated cast threw on
the successful 201 /switch response, dropping the freshly minted Home
user token: every rebind failed, the binder retried a /switch mint every
1-2s, and the app sat permanently in offline mode even after re-signing
in. Accounts without language prefs set were unaffected, which is why
the breakage looked sporadic.

Parse the language lists with a CSV-aware coercion, and make
UserSwitchResponse.fromJson strict only about authToken: decorative
fields now coerce tolerantly and a broken profile blob falls back to
defaults, so account-API drift can never brick token minting again.

close #1488
2026-07-05 06:23:34 +02:00
edde746 6e83aeda88 fix(hubs): drop deleted items from recommended and home continue watching
The library Recommended tab never subscribed to deletion events, so
"Delete from server" left the episode sitting in Continue Watching
until a full reload. Make the tab DeletionAware (remove in place across
all hubs, then resync) and give DiscoverProvider the same subscription
so the home row and hubs drop deleted items too.

close #1486
2026-07-05 05:58:52 +02:00
edde746 7b1c150e18 feat(android): enable impeller on capable TV devices
Impeller was blanket-disabled on TV in e759dafa as a precaution when the
Tensor/NVIDIA fallbacks landed, and #749 (API 30 Fire TV Cube freezing
until the Skia build) validated it. Flutter has since deprecated the
Impeller opt-out, so start walking the disable back where the hardware
can take it: TV devices on Android 12+ with a Vulkan 1.1 driver use
Impeller, while Fire OS (modern API levels on GPUs whose drivers can't
back it up), pre-12 boxes, and the existing NVIDIA/Huawei/e-waste quirks
stay on Skia. Tag Sentry events with the active renderer so TV
regressions are attributable while this rolls out.
2026-07-05 02:28:51 +02:00
edde746 11f7fd766d perf(library): cut scroll jank in card grids and hub rows
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.
2026-07-05 02:23:42 +02:00
edde746 d80a1ed15a feat: report real device identity to media servers
Resolve platform, hardware model, and friendly device name once via a
shared DeviceIdentityService and send them to both backends: Plex gets
a real X-Plex-Platform plus X-Plex-Device/X-Plex-Device-Name (shown as
Player in dashboards/Tautulli), Jellyfin gets the device name in the
MediaBrowser auth header. Transcode and live-TV decision requests keep
their pinned platform names, which Plex validates server-side.

close #1270
2026-07-05 01:22:20 +02:00
edde746 f1422feff1 fix(detail): rework hero scrim into full-height eased dim 2026-07-05 00:35:36 +02:00
edde746 d7c0af9ad0 fix(player): fit the performance overlay on short screens
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
2026-07-04 23:53:50 +02:00
edde746 87aea49ab6 fix(servers): stop rebinds from flashing an empty home screen after sign-in
Signing in triggered two back-to-back profile rebinds; the second re-added
the same Jellyfin connection, which tore down the live client and aborted
the home screen's in-flight fetches. The aborted pass was committed as
loaded-empty, flashing 'no content available' until the follow-up load
landed. Fix at the root instead of patching the sign-in window:

- addJellyfinConnection now reuses the live client when the connection is
  unchanged (token, deviceId, URL set), matching the existing Plex
  refreshTokensForProfile behavior; material changes still recreate it.
- Cancelled requests are classified end-to-end: the client's
  treat-as-empty helpers rethrow cancellations, and the aggregation
  fan-outs report cancelledServerIds alongside succeededServerIds.
- A fetch pass in which zero servers succeeded is never authoritative:
  it keeps existing content instead of wiping it (also fixes the
  pre-existing blanking of home/sidebar on a totally failed refresh),
  stays in loading while disrupted (cancellation or binding in flight),
  and only commits loaded-empty on a settled failure.
2026-07-04 23:44:38 +02:00
edde746 c8ca8a7875 fix(discover): restore hero indicators and bottom fade on non-TV
The hero dots/pause row was gated on live input mode, so any navigation
key event (Android back key, BT keyboards, gamepad-source noise) hid it
until the next pointer event - on phones it ended up permanently hidden.
Gate on the TV platform instead (issue #600's actual scope); the TV
layout never renders the carousel, so nothing changes there.

The bottom fade lost its guaranteed opaque band when the section-level
gradient was removed (686a61ac): the per-item overlay only reached full
background at the literal last pixel, letting artwork ghost through the
final 15% and read as a hard cut against the content below - worst on
phones, where square hero art is bright at the bottom. Finish the fade
at solid background from the 0.94 stop (~32px band on a phone hero).
2026-07-04 23:18:33 +02:00
edde746 d9b365b012 fix: failing tests 2026-07-04 23:09:41 +02:00
edde746 05e70ab56d chore: remove caddy 2026-07-04 23:09:41 +02:00
edde746 f9abcd44fe fix(playback): hold timeline preview until playback settles at the target
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).
2026-07-04 22:58:19 +02:00
edde746 3cad91736b fix(playback): raise timeline scrub debounce above the key-repeat delay
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.
2026-07-04 22:57:27 +02:00
edde746 632d66f6ac fix(playback): clear timeline seek dedup state when a gesture finalizes
_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.
2026-07-04 22:57:00 +02:00
edde746 27596bb919 fix(windows): enable WinHTTP IPv6 fast fallback
Dual-stack hostnames with unreachable IPv6 stalled past the endpoint
probe budget because WinHTTP tries addresses sequentially. Enable
WINHTTP_OPTION_IPV6_FAST_FALLBACK (Happy Eyeballs) via win_http 0.2.2
so IPv4 is raced after the IPv6 attempt, matching curl/Cronet/NSURLSession.

close #1128
2026-07-04 21:56:41 +02:00
edde746 6b7aac3372 fix(tv): handle remote play/pause at the screen focus level
When focus drifts to the player's screen-level Focus node (after an
overlay sheet closes with the controls hidden), a hardware play/pause
key only revealed the chrome and leaked to Android's MediaSession,
which pauses unreliably when its state is throttled or suspended.
Intercept media play/pause at the screen node on TV-style navigation:
toggle via the same playback-intent path as the Apple TV bridge and
consume the event.

close #1375
2026-07-04 21:33:45 +02:00
edde746 6af4289289 fix(tv): keep sidebar focused when Back returns to the home tab
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
2026-07-04 20:56:04 +02:00
edde746 eb8796fa26 feat(library): indicate active filters in browse view
Badge the mobile Library options icon with a dot while filters are
active, and replace the "This library is empty" state with a
"No items match the active filters" message plus a reset button.

close #1470
2026-07-04 20:36:23 +02:00
edde746 8fe66f6e36 fix(android): keep the activity alive when a BT keyboard connects
close #1455
2026-07-04 19:54:19 +02:00
edde746 837fdf4031 fix(playback): canonicalize base URL scheme case
FFmpeg's protocol lookup is case-sensitive, so a stored "Https://" base
URL reaches mpv verbatim through the direct-play string concat and fails
with "Protocol not found" (API calls survive because Dart's Uri
lowercases the scheme). Canonicalize at Jellyfin URL intake and in the
connection constructor so persisted configs self-heal on load, and
register mpv-escaped (https\://) redaction variants so option-value
logs stop leaking the server host.

close #1465
2026-07-04 19:43:32 +02:00
edde746 dd09e421bf fix(android): force codec re-init on TV surface changes to recover video after screensaver
close #1481
2026-07-04 19:25:56 +02:00
edde746 50261398bf fix(android): serialize mpv writes to restore fallback ordering
close #1482
2026-07-04 19:15:34 +02:00
edde746 7632d4bea7 feat(jellyfin): favorites library filter + heart favorite in the rate sheet
close #1485
2026-07-04 18:33:58 +02:00
edde746 30de8be2cd fix(images): release artwork slots on non-200 responses 2026-07-04 17:31:52 +02:00
edde746 536e8e3d2a fix(playback): keep the Jellyfin shuffle queue across episodes
close #1466
2026-07-03 10:20:13 +02:00
edde746 3f9c25fe5e fix(ui): hide Play Version while the item's server is unreachable
At most one version of an item can be downloaded, and plain Play now
targets it directly, so an offline Play Version picker is a no-op
detour offering versions that cannot play (#1440). Backend
capabilities are static, so the menu gate and the quality picker in
_handlePlayVersion both get a liveness check — a server dropping
between menu open and tap must not offer transcodes either.
2026-07-03 10:19:55 +02:00
edde746 fd4291aafb fix(playback): play the downloaded version when offline close #1440
Plain Play requests the default media version (index 0 or the saved
preference), but the offline resolvers rejected the single downloaded
row when a non-default version was downloaded, then threw "No video
URL available" with no client to fall back to.

Three-part fix sharing one matcher (downloadedVersionMatches):
- getPlaybackData falls back to the downloaded version when there is no
  client to stream from; the result now carries the effective
  mediaIndex/mediaSourceId so cached media info and the committed
  session describe the file actually played.
- navigateToVideoPlayer seeds the selection from the download record
  for isOffline plays with no explicit version, covering the external
  player branch and offline-library plays with a reachable server.
- PlaybackSession.fromContext prefers the result source id over the
  requested one, keeping in-player state in sync after a fallback.

Online pinning is untouched: with a live client an explicitly requested
non-downloaded version still streams from the server.
2026-07-03 10:19:55 +02:00
edde746 8fa91fe444 chore(apple): pin MPVKit 1.0.8
AV1 perf: NEON dav1d (upstream prebuilt shipped C-only) + fused
software-frame upload in the avfoundation VO (patch 0009).
2026-07-03 08:51:19 +02:00
edde746 7972743a2f fix(windows): stop mpv from double-handling hardware media keys 2026-07-02 18:30:20 +02:00
edde746 6d95e35448 feat(tvos): AVPlayer Atmos sink wiring + Atmos output diagnostics
Re-enables Audio Passthrough on Apple TV against the MPVKit EAC3-JOC
AVPlayer sink (pin bump to v1.0.8 follows), adds an AVPlayer test
harness + diagnostics screen for #1300, and makes loudnorm yield while
passthrough is active (fixes a latent spdif conflict on desktop too).
2026-07-02 15:52:10 +02:00
edde746 86abf3e9da fix(watch-together): retry guest media switches until they commit
Guest switch dispatch pre-marked its dedup key and fired-and-forgot, so
any failure (fetch error, reload busy with an auto-advance, navigation
race with the host exiting) silently stranded the guest on the old media.
A CurrentPlaybackDispatcher now marks a key handled only after the sink
reports success against the committed identity, with a serialized
in-flight slot, timeout, and generation reset; the reconciler re-offers
unattached media on every host heartbeat, making the heartbeat the retry
channel. Fetches that outlive their dispatch are re-validated against the
current snapshot so a stale switch can't override the live one.
hostExitedPlayer now rides the controller's ordered message queue with
host authentication instead of racing state handling in the provider.
2026-07-02 12:44:15 +02:00
edde746 0e3c592205 fix(windows): marshal mpv events onto the platform thread
close #1453

Events were delivered to the Flutter event sink directly on the mpv event
thread while method replies were already marshaled; concurrent messenger
use from two threads intermittently crashed guests during the event burst
of an in-place media switch. Also joins the event thread before draining
platform tasks in the plugin destructor (reverse member destruction order
would otherwise let the event thread enqueue into a destroyed queue) and
coalesces the wakeup PostMessage behind a retry-safe flag.
2026-07-02 12:43:10 +02:00
edde746 5f49dddb4d feat(ui): M3E restyle for settings, auth, and profile screens 2026-07-02 11:47:43 +02:00
edde746 074f92d144 fix(tv): bleed the unfocused-rail dim under the side nav
The rail dim (and its top fade band) was clipped to the rail's footprint,
so it terminated in a vertical seam at the nav edge while the backdrop
continued behind the nav. Generalize _RailBackgroundBleed into
_RailBleedPositioned and render the dim through it, full-width like the
background gradient. Row stripes now apply only while the rail itself is
focused — under the full-width dim they'd re-introduce level seams at the
nav edge (and read as noise anyway).
2026-07-02 11:41:25 +02:00
edde746 6274facc3c fix(tv): fade dim-scrim edges instead of hard rect boundaries
Unlike the content opacity it replaced, the dim quad also darkens the
backdrop showing through around the rows, so its rectangle edge read as
a hard line across the spotlight artwork. Ramp the dim in over narrow
top/bottom gradient bands (flat quad interior, non-AA to avoid seams);
band area keeps the gradient-shader cost negligible on weak TV GPUs.
Browse pass on the Mali-G31 box unchanged: draw p50 7.2ms, 2% janky.
2026-07-02 11:41:25 +02:00
edde746 b08b3b846a perf(tv): rasterize axis-aligned gradients into strip textures
Skia's dithered gradient shaders cost ~10ms per full-screen pass on
Mali-class TV GPUs: the two spotlight scrims alone were ~20ms of a 27ms
raster frame, while flat blended quads at the same coverage are ~free.
Bake axis-aligned LinearGradients once into cached 1x1024 premultiplied
strip textures drawn as stretched quads (shader fallback for unsupported
shapes and the first frame). Converted the spotlight, TV detail backdrop,
app-bar scrims, rail bleed, person-card overlay, and the video-controls
scrim, which now also keeps one widget type across hasFrame flips so the
controls subtree survives in-place source switches.

Scripted-browse on a Mali-G31 box: draw p50 27.5ms -> 7.1ms, swap block
20.3ms -> 0.9ms, janky draws 69% -> 2%; screenshots pixel-identical.
2026-07-02 11:41:25 +02:00
edde746 1059658515 refactor(profiles): remove dead registry surface
Delete unused registry members (ProfileConnectionRegistry.insertIfAbsent /
removeAllForProfile, ProfileRegistry.reorder, ProfilesView.countFor,
ConnectionRegistry.getDefault) and their orphan tests, refreshing the stale
docs that named removeAllForProfile as the profile-delete cleanup path.

Repair the "one default per profile" join-row invariant: the connectionId FK
cascade (foreign_keys=ON) silently drops a profile's default row while its
other rows survive, leaving it defaultless. Add promoteMissingDefaults, share
a deterministic re-promotion helper with remove(), and re-promote in
removeAllForConnection.
2026-07-02 11:41:25 +02:00
edde746 d193633a5d fix(remote): live auth-context refresh + lifecycle races
Keep a running companion-remote host's crypto identity in sync with the
active profile: subscribe to PlexHome/connection/join streams and rebuild
auth contexts (restarting the broadcast when they change) so a removed
home user or revoked borrowed connection stops controlling the host.

Serialize host start/stop/crypto-rebuild through a lifecycle lock, clean
up peer subscriptions before re-listening, and guard the replaced-client
onDone against clobbering the new client's session. Drop the context.mounted
guards that aborted an app-level host start, fix the DiscoveryView
init-flash/stuck-on-throw, and delete the dead initialize*Crypto helpers.
2026-07-02 11:41:25 +02:00
edde746 2b7142bdcd fix(downloads): profile-scoped ownership and watch-sync integrity 2026-07-02 11:41:25 +02:00
edde746 44be03d39c refactor(profiles): shared auth/mint flows + screen fixes 2026-07-02 11:41:25 +02:00
edde746 f9123eb40f fix(profiles): binder settle, active-id, and rebind correctness 2026-07-02 11:41:25 +02:00
edde746 2025e1f9fd fix(profiles): clean up orphaned connections on Plex sign-out
close #1423
2026-07-02 11:41:25 +02:00
edde746 6e8320f782 fix(profiles): parse real Plex Home uuids + migrate pref scopes 2026-07-02 11:41:25 +02:00
edde746 bfcd11538e fix(security): redact Plex Home PIN from logs 2026-07-02 11:41:25 +02:00
edde746 72d37f2362 perf(tv): dim action bars once at row level while unfocused
In d-pad mode every unfocused FocusableActionBar button held its own
AnimatedOpacity(0.6) — 4-5 permanent saveLayers (one render pass each
on tiled TV GPUs) on discover and detail alike, every frame. While no
button in the row has focus (the entire time the user browses content)
the whole row now shares a single dim layer; per-button dims apply only
while the row holds focus.
2026-07-02 11:41:25 +02:00
edde746 e8a99c1108 fix(tv): harden spotlight crossfade broken-artwork path
The base image shared the incoming's fade-riding errorBuilder, so a
404'd backdrop flashed transparent on the next swap, could start the
fade before any frame existed, and an errored provider got promoted
into the base (re-resolve loop). Base now uses a static fallback,
error/frame state is tracked per incoming (keyed by provider), and
promotion settles on the fallback color when no frame ever arrived.
2026-07-02 11:41:25 +02:00
edde746 48568fd412 perf(tv): exclude inactive hub rows from semantics
Their cards can't take focus until the row activates (locked-hub
model), and the per-frame semantics pass scales with node count.
2026-07-02 11:41:25 +02:00
edde746 320bde7f33 perf(tv): spotlight backdrop cross-fades by paint alpha
The whole-stack AnimatedSwitcher kept two full-screen saveLayers (each
compositing backdrop + two full-screen gradients) blending on every
focus move — with CachedNetworkImage's own fade nested inside, the
dominant GPU load while browsing. Gradients are identical between items
so they never fade now; the incoming artwork fades in via Image.opacity
over the still-opaque previous one (no placeholder flash), and only the
bounded info block keeps a real cross-fade.
2026-07-02 11:41:25 +02:00
edde746 868a1469a3 perf: slim per-card build cost
Memoize the per-image SHA-1 disk-cache key (ran on the UI thread on
every image build), fade images in via paint alpha instead of an
AnimatedSwitcher saveLayer per in-flight tile, create SettingsBuilder's
merged listenable once instead of per build, and drop the pointer
chrome (InkWell ink machinery, cursor MouseRegions) from cards on TV
where no pointer exists.
2026-07-02 11:41:25 +02:00
edde746 43ab5d3246 perf: shrink semantics tree on media lists
Poster images are decorative (the card exposes one merged node), and
the default per-child AutomaticKeepAlive/_SelectionKeepAlive/
IndexedSemantics wrappers are inert here — no keep-alive clients or
SelectionArea exist. The semantics phase ran ~9ms every frame on TV
devices with an accessibility service; it scales with node count.
2026-07-02 11:41:25 +02:00
edde746 c38f24aee9 perf(tv): isolate rail rebuilds; dim rows via scrims, not opacity
Hub moves, focus flips and item selects now flow through the rail focus
model + ListenableSelectors (two headers/cards repaint instead of every
visible card rebuilding). DiscoverScreen memoizes the hubs list, gates
provider notifies on a render signature and caches the rail widget so
no-op refreshes skip the subtree entirely. The whole-rail 0.6 and
inactive-row 0.7 AnimatedOpacity layers are replaced by AnimatedDimScrim
quads, removing the permanent per-frame saveLayers that kept low-end TV
GPUs raster-bound.
2026-07-02 11:41:25 +02:00
edde746 a5041cb3db perf(tv): opaque detail route + isolate focused-episode updates
Covered routes stop painting/building once the fade settles, and d-pad
episode scrubbing rebuilds only the foreground info panel instead of
the whole screen with its rail.
2026-07-02 11:41:25 +02:00
edde746 7143bb380f fix(tv): skip desktop hover-arrows machinery on TV 2026-07-02 11:41:25 +02:00
edde746 d5df426ab6 build(libass): bump to 0.18.2 2026-07-02 11:41:24 +02:00
edde746 1d2dea214c ci(windows): set up arm64 Flutter 3.44.0 from git tag 2026-07-01 07:10:24 +02:00
edde746 e07f54921c ci(windows): install patched DComp engine for x64 and arm64 2026-07-01 06:37:51 +02:00
edde746 ba112d7aa6 refactor(windows): remove legacy mpv compositing, require DComp engine 2026-07-01 06:37:35 +02:00
edde746 28e6e349b3 fix(android): restore ExoPlayer audio for tunneled decoded PCM 2026-06-30 18:58:08 +02:00
edde746 2d30686857 fix: decouple library grouping from provider context 2026-06-30 06:57:21 +02:00
edde746 ea86ce5a4b fix(android): preserve bitmap subtitle aspect
close #1444
2026-06-30 02:31:18 +02:00
edde746 4cf94683ae fix: send correct streamID for same-language forced subtitles
close #1443
2026-06-29 07:10:25 +02:00
edde746 6c96830033 fix(android): never-drop multi-page subtitle atlas 2026-06-29 02:31:14 +02:00
edde746 ec1473af5e fix(android): tile oversized subtitle images so the atlas never drops them 2026-06-28 21:49:19 +02:00
edde746 79b75483bd style: fix CI formatting 2026-06-27 10:36:21 +02:00
edde746 78090d8bd6 refactor(trackers): unify session and account store across services
Replace the per-tracker json_serializable session classes and account stores with a shared TrackerSession (service-aware persisted validation), a single TrackerAccountStore, a shared TrackerHttpClient, and an AnimeListTrackerBase mixin for MAL/AniList. Scope the per-service rebind guard so a disconnect can't abort a racing profile load. Add migration-safety, MAL refresh, episode-count cache, and content-type tests.
2026-06-27 10:05:03 +02:00
edde746 2fe0fdb975 fix: hide player queue spoilers
close #1426
2026-06-27 07:22:21 +02:00
edde746 930534ee31 fix: avoid duplicate timeline seek
close #1421
2026-06-27 07:05:06 +02:00
edde746 3b611a1e18 fix: avoid early playback completion 2026-06-27 06:46:32 +02:00
edde746 120749e442 fix(android): regenerate libass prebuilt archive 2026-06-27 06:43:37 +02:00
edde746 d333ac2382 fix(apple): pin MPVKit 1.0.6 2026-06-27 06:03:07 +02:00
edde746 c3ca6e4c49 fix: clear subtitle overlays when disabling subtitles
close #1387
2026-06-26 10:09:45 +02:00
edde746 ad6ed04901 style: abbreviate episode date on TV media detail
close #1380
2026-06-26 08:48:30 +02:00
edde746 25e5ea042f fix: server-qualify TvBrowseRail hub keys to avoid duplicate GlobalKey 2026-06-26 08:34:54 +02:00