Commit Graph
163 Commits
Author SHA1 Message Date
edde746 9ecf8db90f fix(startup): stop offering Retry after a repair that needs a restart
A seed-and-restart repair writes the salvaged credentials straight to disk and
leaves this process's store closed, because the plugin still holds the bad
document in memory. repairCorruptStore says so plainly — "nothing may write a
preference before that restart … the caller keeps the app on the failure
screen precisely so nothing does" — but the caller did not. Clearing the
repairing flag re-enabled Retry, and pressing it reopened onto the stale map,
whose first write would flush it back over the seed and orphan every
ciphertext token in the database.

The repair hook returned a bare bool, which cannot express the difference
between "retry now" and "never retry in this process", so replace it with
StartupRepairResult. The restart case latches terminal state on the bootstrap,
withdraws Retry and Repair rather than grey them out — a disabled control
still invites another press — and says what to do instead, which nothing did:
repairNeedsRestart was a dialog title with no body anywhere. Desktop gets a
Quit button through the existing AppExitService seam; Copy and Upload stay
live everywhere, because a stuck user still needs the diagnostic out.

close #1732
2026-08-01 06:59:20 +02:00
edde746 7f0cad339c fix(startup): report and repair a failed launch instead of showing "Error"
Since 2.10.0 the whole app sits behind one all-or-nothing initialization
gate, and that gate discarded the only evidence of its own failure. It
caught the error, logged nothing but `error.runtimeType`, rendered an
icon plus the word "Error" plus Retry, and never reported the error
because catching it kept the crash reporter from ever seeing it. There
is no log file on any platform, the buffer is in memory only, a
double-clicked Windows release build has no console, and the log viewer
lives in Settings, behind the gate that just failed. #1732 is the result:
a Windows 11 user whose app will not boot and who cannot produce a single
byte of diagnostic detail.

The gate now names its phases. Each step is wrapped so a throw carries
the phase it came from, replacing a `Future.wait` that discarded every
error but the first and could not attribute it to any of four concurrent
steps. The failure screen renders the phase, the exception type, the
message and an expandable stack, plus copy and upload actions that reuse
the existing log-relay flow. The record is persisted next to the database
so the next successful launch can surface it in Settings > Logs, and it
is reported to the crash reporter explicitly.

Only preferences and the database still gate the launch. Window chrome,
locale, crash-reporting init, TV/performance detection, the image-cache
budget and download storage are best-effort and time-bounded, so a
stalled platform thread degrades instead of holding the splash forever.
Sentry no longer receives the startup work as its `appRunner`: that made
a startup failure indistinguishable from a Sentry failure, and the guard
would then have re-run migrations and the database open a second time.

The two remaining fatal steps become recoverable. Preference reads
tolerate a value whose stored type no longer matches, dropping the key
and defaulting instead of failing the boot. A store that cannot be parsed
is detected before either desktop plugin backend can memoise it, which is
what makes an in-process repair possible at all. Repair is never
automatic: it states what it will cost, salvages the credential-vault key
and every tracker and Seerr session it can validate out of the damaged
bytes, reseeds them, and moves the original aside rather than deleting
it. Servers and profiles survive a salvaged key because their tokens are
ciphertext in the database; tracker and Seerr sessions are plaintext
preference entries, so the copy says they may still need reconnecting.

Nothing derived from the store reaches a diagnostic. `FormatException`
prints an excerpt of whatever it failed to parse, and during startup that
document holds the vault key, refresh tokens and session cookies while
the redaction manager still has nothing registered, so the wrapper keeps
only the cause's type and offset and the record is an allowlist of
already-redacted fields. The quarantined copy is labelled as containing
credentials, is never offered for upload, and can be deleted from the
dialog.

Also self-heals orphaned WAL/SHM sidecars on desktop rather than only
tvOS, makes every `createTable` migration step idempotent, keeps MSVC
link by-products out of the Windows bundle, and asserts bundle contents
in CI.

Refs #1732
2026-07-31 21:45:32 +02:00
edde746 d55b875855 fix(tvos): raise the system keyboard on arrival, not on every focus
Apple TV single-line fields moved to the engine's UITextField proxy in
2.10.0 (71735354), which made three focus behaviours user-visible.

Submitting re-attached the input connection. EditableText schedules a
restart when a submit action fires with a non-null onSubmitted, and that
microtask runs before the setState flipping readOnly, so the field
re-showed a keyboard the form had just dismissed. The native path now
withholds onSubmitted from EditableText and invokes it from the host,
independently of onEditingComplete as _finalizeEditing does.

Auto-open fired on every focus entry, so D-pad traversal of a multi-field
form raised and dismissed the modal system keyboard on each step.
TvTextInputAutoOpenBehavior gains onFirstFocus, and the new `automatic`
default resolves to it on Apple TV: arriving at a field opens it once,
returning to it does not. Android TV keeps its docked-IME auto-open, and
explicit modes stay literal on both. The autofocused Jellyfin and Seerr
URL fields keep an explicit exception so entering the screen still does
not bury the form (#1217).

EditableText.connectionClosed unfocuses the field outright, so a UIKit
keyboard dismissal left nothing focused at all. The host takes focus back,
keyed on identity with the field's own enclosing scope so a dialog or
route claiming focus meanwhile is left alone.

close #1728
2026-07-31 01:05:10 +02:00
edde746 644c782edd fix(player): tick the frame-clock keep-alive only on Linux
The 1x1 keep-alive repaint loop was extended to Windows in a87aa296 to
paper over the legacy compositing path's resize desync (#227); the DComp
rework replaced that presentation path entirely. On the DComp engine the
100ms repaints become DirectComposition commits during playback, and
once fullscreen focus engages VRR (FreeSync/G-Sync) every commit forces
a scanout off the video's cadence - the micro-stutter of #1707.

The widget now owns the platform decision behind a test seam, and a new
quiescence test pins the hidden-chrome player UI to zero scheduled
frames so no future ticker can silently reintroduce the defect.
2026-07-29 19:04:57 +02:00
edde746 27acbaf435 feat(explore): surface the catalog data providers already return
Explore shelf cards drew a poster, a title and a year. An audit of all six
catalog sources found the rest was lost at two boundaries — the wire-to-DTO
mapping and the DTO-to-CatalogItem mapping — and then simply not drawn: the
grid card fell through every branch of buildMetadataSubtitle to the year-only
case, while the list card used by search already composed certification,
runtime and rating from fields the synthesized MediaItem already held.

Extend CatalogItem with the neutral facts every provider had been dropping:
attributed rating sources, leaderboard ranks that keep their season window,
audience counters that keep their timeframe, broadcast slots, next-episode air
times, server availability and request state, exact release dates, alternate
titles, format, source material, studios, countries, languages, credits, tags,
links, artwork variants, play state, gallery art and background prose. Replace
fetchCast and fetchRelated with one fetchDetail returning the enriched item,
its cast, its recommendations and labelled franchise relations without adding
a request: sources needing two calls keep two and run them concurrently with
isolated failures.

Map those fields in all six sources, widening only field selections that cost
no extra round trip — MAL's fields list, AniList's selection set and a bounded
row cast that lets detail skip its character call, Trakt's guest stars, Seerr's
language parameter and TMDB size ladder, and Plex's includeUserState. Plex hub
artwork widens only on TV, where the spotlight is its only consumer, because it
doubles the payload.

Render them: a rating-first caption and bounded badges on the shelf card,
labelled sections on the detail screen, provider hub styles and result counts
on shelves, and logo, banner and accent art in the TV spotlight.

Verified against live Plex, AniList, Simkl and MAL responses, and on a Pixel 7.
2026-07-29 06:47:54 +02:00
edde746 0ef71e6489 feat(file-info): detail every version, file and stream the server reports
The sheet collapsed an item to `Media.first` / `MediaSources.first` and
rendered a fixed set of rows, so split files, extra versions, per-track
properties, HDR classification and Dolby Vision were all invisible.

Model the payload the way both servers shape it — versions own parts,
parts own streams — and project every property either backend populates
onto `MediaStreamDetails`. The field set comes from sweeping both test
servers in full through the clients' own request shapes (Plex 6842
Media / 6842 Part / 39864 Stream entries, Jellyfin 6349 sources / 37763
streams / 61224 attachments), so file presence, Dolby Vision layers,
dynamic range, sample rate, spatial audio, sidecar provenance, embedded
attachments, rotation and the lyric stream type all survive. A coverage
test fails when a server key is neither carried, folded into a sibling,
nor excluded with a reason.

File Info also stopped trusting the shared `/library/metadata/{id}`
cache row: `getPlaybackExtras` writes it without `includeStreams` /
`checkFiles`, so the sheet could render with no stream table at all.
Detect that shape and refetch once under the request context captured
before the cache read, so the outgoing token and the cache namespace
stay on one profile.

Rework the layout to match: a summary chip row, then flat tonal cards
with a two-column field grid that collapses to one column on narrow
viewports, per-stream cards with flag chips, and a copyable monospace
path row. The card fill is a tonal step off the text colour rather than
the `bg` token, which is one shade from the sheet surface on OLED.
2026-07-29 05:19:27 +02:00
edde746 f02924b9a5 fix(player): require a fresh double tap for every mobile skip-zone seek
The skip badge doubled as an armed state: while it was up, any single tap
in the same-direction zone seeked again. The badge is also raised by
keyboard, D-pad, media-transport and live seeks, so one remote press armed
one-tap seeking on the touch surface with no double tap at all. It stayed
armed for 1200 ms plus the fade and renewed on every tap, leaving the side
zones - 35% of the width each, over 70% of the height - unable to raise
the chrome.

Pair taps off the pending single-tap timer rather than differencing
DateTime.now(). The window is then one deadline that a clock adjustment
cannot stretch, suppressing touch taps disarms a half-finished pair, and
_lastSkipTapTime belongs to the desktop double-click paths alone.

Consecutive completed skips still accumulate into one running badge total.
2026-07-29 05:05:40 +02:00
edde746 f3795d49eb feat(player): answer transport keys with transient indicators, not the chrome
Pressing pause or seeking while the player's on-screen controls were hidden raised
the entire OSD, covering the subtitles the viewer was rewinding to read. Transport
keys now answer with a transient indicator and leave the chrome down; Select,
D-pad Center and a centre tap remain the deliberate way to bring the controls
back.

Play/pause confirms with an icon-only translucent disc at the centre of the frame,
72px around a 44px glyph, which grows and fades in, holds half a second at rest,
then runs the same motion in reverse. Seeking shows the amount plus a single
chevron on the same line at the edge it travels toward, with no backdrop at all:
anything large enough to read as a surface is large enough to cover picture and
subtitles, so legibility comes from shadows instead. Only the chevron moves, and
it eases outward across most of its cycle and returns briefly, holding a visible
opacity floor rather than blinking out. Type is scaled per platform, since a
television is read from across the room. The existing text pill stays for genuine
notices - rate changes, chapter titles, zoom, errors - because an earlier centred
pill overlapped ASS \an8 subtitle placement, which is the readability complaint
this feedback exists to answer.

Every relative seek entry point now shares one coalescing primitive. The keyboard
shortcuts fell through to KeyboardShortcutsService and previously reported
nothing, and both they and the remote's chapter fallback rebased each press off
player.state.position, so a burst against a slow backend pinned every request near
one step while the indicator climbed to a total that was never committed. A
released key commits its pending target immediately and resets the acceleration
tier, including on live TV where seeks bypass the accumulator. A chapter seek with
nowhere to go, past the last chapter or already at the start, no longer announces a
jump it does not perform.

Rewind-on-resume follows the resolved intent rather than the current state, so a
directed pause on an already-paused video neither resumes nor rewinds. Indicators
carry their own liveRegion semantics nodes: their labels previously merged into the
full-screen "show playback controls" target, corrupting its accessible name, and
they keep announcing "Paused"/"Playing" and the seek amount from icon-only visuals.

close #1676
2026-07-29 04:24:04 +02:00
edde746 c64e6fc519 fix(artwork): stop memoizing posters that merely have no client yet
Distinguish an unresolvable URL from a failed load at the error-widget
boundary. A transiently null media client during a profile switch or
reconnect marked the primary poster dead in a process-global set, pinning the
item to fallback artwork for the rest of the session.
2026-07-27 17:44:52 +02:00
edde746 8e1904ddee fix(sheets): let system back dismiss a hosted sheet on touch platforms
Back left the Manage Libraries sheet open on Android with no way to
dismiss it. The host answered the platform pop with
`BackKeyCoordinator.consumeIfHandled()`, which dedups the focused key
path against the platform pop. Only TV routes one Back through both;
touch platforms never deliver Back to the sheet's key handler, verified
on device — a physical Back produced only `popRoute` and no key event.
So there was nothing to dedup against, and the global one-shot marker,
once set by any other handler, silently swallowed the only signal that
closes the sheet.

Scopes the dedup to TV. The TV regression that guarded this never set
the TV override, so it asserted the swallow on every platform and hid
the defect; it now enables the override and a touch counterpart pins the
dismissal.

Also blocks semantics behind the barrier. The barrier takes every
pointer event but left the screen underneath in the semantics tree, so
assistive tech and UI automation still saw rows that could not be
activated — Maestro read an occluded settings row as visible and tapped
its stale coordinates into the sheet. Flutter's own ModalBarrier blocks
semantics for the same reason.

Verified by replaying the failing Maestro sequence
(.maestro/subflows/settings_deep_checks.yaml lines 42-60) on a device:
back dismisses the sheet, Services opens, and back returns to settings.
2026-07-27 01:11:23 +02:00
edde746 e7f97cc090 refactor(music): move the playback stub beside the tests that use it
StubMusicPlaybackService is a base for test doubles with no production
caller, so `check-unused-code lib` flagged it and the analysis job
failed. Moves it to test/test_helpers/, where shared fakes belong.
2026-07-26 23:05:53 +02:00
edde746 15b54e2ec6 fix(settings): restore the shared compact row size
Settings rows carried their own platform-conditional typography and
density, so on desktop and TV they rendered a 16px title, 14px subtitle
and 80px row while every other row in the app — the Focusable*ListTile
defaults plus ThemeData.listTileTheme's `dense: true` — renders 13/12
in 61px.

Drop the overrides instead of re-tuning them: the tile defaults already
encode the app's row style, and the explicit title styles were redundant
under a dense ListTile (they also masked the disabled/selected title
color). settingsOptionTitleStyle now only serves group children that are
not ListTiles, and matches the dense title unconditionally.

SettingsGroup hands its children that same compact density, so the plain
ListTiles used as non-interactive info rows stop standing 11px taller
than their interactive siblings.
2026-07-26 21:04:53 +02:00
edde746 a56b9a3dfb Merge the deduplication and dead-code removal pass
Consolidates duplicated logic behind shared implementations — paginated
grid tabs, focus chrome, cached remote stores, sheet selection columns,
the server artifact store and a test fixture layer — and removes code
that had become unreachable. Net reduction of about 5,500 lines with no
behaviour change.

Where a fix had landed separately in code that moved into a shared
helper, the fix was re-applied inside the helper rather than left behind
in the copy that went away.
2026-07-26 19:41:23 +02:00
edde746 f5661c766e perf(tv): tint inactive row artwork 2026-07-26 14:58:21 +02:00
edde746 829d3745a1 fix(tvos): restore native text input navigation 2026-07-26 07:08:01 +02:00
edde746 71735354b9 fix(tvos): unify remote and text input ownership 2026-07-26 07:08:01 +02:00
edde746 eb3ed45af1 refactor: share future coalescing, Plex client access, and event helpers
Deduplicates the hand-rolled coalescing/caching maps, the Plex client cast,
the missing-serverId event guard and the progress-failure backoff, and drops
the MusicPlaybackService availability gate, which could never fail in
production.
2026-07-26 06:09:50 +02:00
edde746 316a69a1de refactor: share the paginated grid tab, cached remote store, and tile focus
- 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.
2026-07-26 06:09:48 +02:00
edde746 352b88109b refactor: extract shared mixins and helpers, drop dead abstractions
Introduces shared seams for paginated views, D-pad reorder, media control
routing, async singletons and the device method channel, then points the
open-coded copies at them.

Also removes unused models and duplicated provider/server plumbing, folds
the twice-implemented artifact store in the server, and factors the
repeated Flutter toolchain prologue in CI into a composite action.
2026-07-26 06:09:48 +02:00
edde746 61344f7862 test: extract shared fixtures and scaffolds
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.
2026-07-26 06:09:47 +02:00
edde746 fcaa81bf3c fix(artwork): preserve clear-logo aspect ratios 2026-07-26 04:34:08 +02:00
edde746 3b1e71b3fa feat(player): support playback speeds up to 8x
close #1545
2026-07-26 04:24:55 +02:00
edde746 e251273322 feat(downloads): warn about Android background restrictions 2026-07-26 04:24:55 +02:00
edde746 1b3c74550f fix(tvos): make remote input lifecycle engine-owned 2026-07-26 03:41:19 +02:00
edde746 cd6716df47 perf(tv): trim media card focus semantics 2026-07-25 16:47:04 +02:00
edde746 4af77f4696 fix(app): restore playback and state lifecycle contracts 2026-07-25 16:16:04 +02:00
edde746 73f4a3e055 fix(tv): focus late Continue Watching on startup
close #1602
2026-07-25 10:54:43 +02:00
edde746 fb27621c75 perf(tv): cut semantics work during card navigation 2026-07-25 08:13:09 +02:00
ad7ef112fc feat(i18n): add Hungarian and Traditional Chinese locales
Complete and revise every shipped locale against the current English source, preserve locale-specific plurals, and map script-specific Chinese locales through device, Intl, duration, and Plex boundaries.

Co-authored-by: emgeje <mgj@mgj.hu>

Co-authored-by: junyou1998 <junyou1998@gmail.com>
2026-07-24 07:30:43 +02:00
edde746 b41fb4fe75 fix(ui): harden settings focus and semantics 2026-07-24 03:46:50 +02:00
edde746 e0bf66eea8 fix(runtime): harden application service boundaries 2026-07-24 03:46:46 +02:00
AlbertandClaude Fable 5 f8cb550be7 feat(tv): add corner spotlight backdrop option (#1628)
New TV-only Appearance toggle that pins the spotlight artwork to the
top-right corner (68% width, 72% height) with the left and bottom edges
feathered into the scaffold background, so the logo, metadata, and
shelves sit on a calm surface instead of the image. Default is off —
the full-bleed spotlight stays exactly as it is.

The artwork keeps its full-screen request size: the corner box only
crops the layout, so transcode URLs and image caches are unchanged and
toggling the setting stays instant.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-07-20 10:06:25 +02:00
edde746 0707d4d9b1 fix(live-tv): stabilize HLS playback 2026-07-18 20:04:09 +02:00
edde746 46dfc7f992 feat(detail): show episode file sizes 2026-07-18 13:32:40 +02:00
edde746 a1b6a89714 fix(player): load subtitle sidecars with media
close #1583
2026-07-17 23:09:07 +02:00
edde746 70cd7e8c67 fix(music): keep now playing state current
close #1600
2026-07-17 16:30:10 +02:00
edde746 5b90c4bca0 fix(images): decode cast cards at the square grid-cell budget
close #1591
2026-07-17 09:04:01 +02:00
edde746 6df8aba097 fix(nav): preserve HTPC escape behavior
Use Flutter's supported desktop application-exit API so the second root Back press closes Plezy on Windows. Preserve fullscreen when video player navigation is enabled while keeping fullscreen-first Escape behavior for normal desktop use.

close #1582
2026-07-16 20:43:10 +02:00
edde746 517c3f57ce fix(music): keep collection and playlist artwork square 2026-07-16 12:37:02 +02:00
edde746 d253450e9d fix(plex): show all artist releases
close #1577
2026-07-15 08:49:06 +02:00
edde746 ea4bd21977 fix(tv): refresh retained spotlight metadata 2026-07-15 06:40:59 +02:00
edde746 3168f6327f fix(tv): make select activation one-shot 2026-07-15 06:40:47 +02:00
edde746 139cf83507 feat(test): add Maestro end-to-end coverage 2026-07-15 06:40:35 +02:00
edde746 abf1027b77 feat(plex): migrate video transcoding to HLS 2026-07-14 18:11:04 +02:00
edde746 8c88385977 feat(jellyfin): cycle media backdrops
close #1568
2026-07-14 07:24:11 +02:00
edde746 d5f3e581df fix: eliminate cross-app consistency drift 2026-07-13 23:13:53 +02:00
edde746 ec249d2ed4 fix(jellyfin): repair music library browsing
Fixes #1557
2026-07-13 22:58:12 +02:00
edde746 d6ad6a9506 fix(settings): align mobile option typography 2026-07-13 22:14:27 +02:00
edde746 40792e2779 fix: restore focus and interaction behavior 2026-07-13 17:00:50 +02:00
edde746 e4db04fa62 fix: align UI focus and sheet behavior 2026-07-13 11:28:32 +02:00