Commit Graph
108 Commits
Author SHA1 Message Date
edde746 976bdbd021 fix(ui): keep hero titles and clear logos legible in light theme
On the light theme, the detail hero's fallback title was hard-coded
white over a scrim that washes artwork toward the near-white background,
and white-on-transparent clear logos vanished into the same wash on the
detail, home, and TV spotlight heroes.

The title fallback now defaults to the theme foreground with a
background-side halo, matching what the TV hero already did. Clear logos
run through the channel-logo tone remap: light-toned marks recolor
toward the theme foreground on light backdrops (network and downloaded
artwork alike), colored marks keep their pixels, and dark themes render
the original artwork. The backdrop-luminance gate is shared with the
Live TV guide as logoToneTargetFor.
2026-08-31 18:51:46 +02:00
edde746 274b31ebac fix(livetv): recolor light channel logos for legibility in light theme
White-on-transparent broadcast logos (CBS, FOX, the NBC wordmark) vanish
on the light theme's white guide cards, making channels hard to identify.

Channel logo decodes now classify the frame's tone once per image-cache
entry and bake light-neutral pixels toward the theme foreground:
monochrome white marks recolor fully, mixed marks (colored peacock plus
white wordmark) keep their colored pixels, and self-backed or dark
artwork decodes unchanged. Applied in the guide channel column, guide
search, and favorite reordering whenever the backdrop is light,
including the dark theme's inverted focus card; dark backdrops keep the
original artwork.

close #2197
2026-08-31 18:51:46 +02:00
edde746 a3cbf323b3 feat(livetv): search the guide for channels and programs
Live TV had no search: finding a channel or a program meant scrolling
the guide row by row, which is close to unusable on TV with a few
hundred IPTV channels, and the global search tab covers neither
channels nor EPG data.

A search action on the Live TV tab bar (shown on every tab, since the
D-pad route through the chips selects each tab it crosses) opens a
sheet that filters channels by name, call sign, and number, and the
next 24 hours of programs by title and series name, from one
fetchSchedule fan-out per open. Selecting a result jumps the guide to
it: the favorites filter is dropped when it would hide the target row,
the time window is re-anchored when the airing is outside the visible
six hours, and D-pad focus lands on the channel cell or program block.
While the sheet is open, companion-remote search queries land in it
instead of the global Search tab (same save/restore idiom as the video
player overrides). Jumps requested while a guide load is in flight are
stashed and replayed when the load commits.

Based on #1526 by @l3gitpanda, rebased over the guide virtualization
and source-group work, with the schedule fan-out moved onto
forEachLiveTvServer and the jump focus routed through the guide's
focus-snapshot publisher so the landed cell actually renders focused.

close #2168
2026-08-29 01:54:21 +02:00
edde746 a11da3fec8 feat(livetv): record live TV on Jellyfin and Emby via the timer APIs
Jellyfin and Emby users had no way to record live TV from Plezy at all: no
record button in the EPG guide's program menu, and nothing in the recordings
UI, because every recording surface was gated behind a Plex-only DVR adapter.

The recording surfaces are now backed by a MediaBrowser DVR adapter that
synthesizes the neutral (Plex-shaped) payload models from the timer APIs:

- Template comes from `GET /LiveTv/Timers/Defaults?programId=`, offering a
  "Record Episode" entry and, for series airings, "Record Series". The
  defaults DTO travels JSON-encoded in the template's opaque `parameters`
  and setting ids are DTO field names, so the existing template-driven
  record-options sheet renders and round-trips them unchanged.
- Create POSTs the mutated defaults whole to `/LiveTv/Timers` or
  `/LiveTv/SeriesTimers`; a duplicate one-off create answers 400, which the
  adapter rethrows as `RecordingConflictException` so the sheet can show
  "Already scheduled" without a backend check.
- Scheduled recordings read `/LiveTv/Timers` minus `Cancelled`/`Completed`
  tombstones, rules read `/LiveTv/SeriesTimers` with their child timers
  nested. Rule keys carry a `timer:`/`series:` prefix so cancel, edit and
  delete dispatch to the right timer space inside the adapter.

Guide programs now carry their recording state: the program id doubles as the
record seed, and `TimerId`/`SeriesTimerId` become the rule keys that drive the
guide's scheduled dot and the Manage action. The series key is stamped only
when an airing actually records, so an episode a series rule skips does not
show a false indicator.

Three neutrality fixes on the shared UI: the record-options sheet only demands
a target library when the template declares one (MediaBrowser records into its
own configured folder), and "Re-evaluate rules" is hidden unless a connected
DVR supports it, since only Plex has that endpoint.

`fetchDvrs` deliberately stays empty so the synthesized per-server Live TV
identity that channel fetches, favorites and playback key off is preserved.

Verified end-to-end against a disposable jellyfin/jellyfin:10.11.11 container
with an M3U tuner and XMLTV guide, driven through the real client: template,
create, duplicate conflict, cancel, series create with child grabs, edit
round-trip and delete.

close #1645
2026-08-24 00:08:11 +02:00
edde746 5d83d4833c fix(images): stop decoding artwork nothing ever displays
The image pipeline funnels through `MediaImageHelper.serverArtworkProvider`, which
sizes the request, bounds the decode and namespaces the disk key. Five places
escaped that funnel, and on the reduced tier they compete for a 64 MiB image cache.

- Jellyfin trickplay prefetch built a bare `CachedNetworkImageProvider` and called
  `resolve()` on it, fully decoding a sprite sheet -- roughly 22 MiB of RGBA for a
  10x10 grid of 320-wide tiles. None of it was ever painted: the render path wraps
  the same provider in `ResizeImage`, so it decodes again under a different cache
  key. The comment already said the intent was only to warm the disk cache, so it
  now does exactly that through the cache manager and resolves no image stream.
- `CyclingMediaBackdrop` re-fetched and re-decoded a full-screen backdrop every
  10 s forever, with no `DevicePerformance` term in `_canRotate` even though its
  own fade duration is tier-gated. At the reduced tier's 1280x720 art cap that is
  3.52 MiB per rotation, ~7 MiB live across the crossfade, churning while the
  viewer sits still and evicting the rail posters behind it. The fade is already
  zero-duration there, so the rotation bought variety and nothing else.
- Catalog detail passed `item.posterUrl`/`backdropUrl` with no client, which
  `getOptimizedImageUrl` deliberately returns unmodified -- so a 40x60 slot pulled
  a 600x900 TMDB asset and a 320-tall backdrop pulled 1920x800. `posterFor`/
  `backdropFor` already existed for this; the sites now use them.
- The season strip, the Live TV programme sheet and the profile avatar built
  `CachedNetworkImage` directly, keeping the sized request but losing the
  `plex_optimized_<sha1>` disk key, so the same artwork was cached twice and
  re-downloaded on detail open. The season strip also discarded its height bound.

Sources without published variants (Trakt, AniList, MAL, MDBList) still fall back
to the base URL.
2026-08-23 17:51:27 +02:00
edde746 61268fab73 fix(livetv): derive recording state from grid subscription attributes
Recording indicators never appeared in the EPG and the program sheet kept
offering "Record" for already-subscribed programmes, so rules were created
repeatedly (nine duplicate subscriptions in the linked report). Two defects,
both confirmed against the official web client bundle:

- The sheet's scheduled-state check called
  /media/providers/<identifier>/media/subscriptions/mapping/<ratingKey>,
  which 404s: PMS mounts that route under the numeric MediaProvider id from
  /media/providers. The check failed instantly and unconditionally on every
  platform, so the button always read "Record".
- The guide derived its dot solely from cross-matching grab metadata, and
  the grab refresh issued right after scheduling raced the server
  materializing the grab, wiping the optimistic key before one frame showed.

The grid response already tags subscribed airings with subscriptionID /
grandparentSubscriptionID - the signal the official client renders from.
LiveTvProgram now carries those attributes; the guide checks them first
(grab matching stays as the secondary signal) and keeps local
schedule/cancel actions authoritative until the next grid load. The sheet
resolves Record vs Manage from the tagged rule key without a network call,
falling back to the fixed numeric-id mapping route for untagged airings.
Grab parsing also accepts the nested airing under "Video", which PMS uses
for every non-scheduled grab status in JSON.

close #2009
2026-08-21 07:44:27 +02:00
edde746 417f3a5e36 fix(livetv): keep the guide mounted during reloads and apply day picks immediately
The guide replaced itself with a bare spinner on every window change,
unmounting the day-picker anchor and the guide focus node - after one
day or time change the pickers silently no-oped and the remote went
dead. Picking a day also applied nothing by itself: the window only
moved after the second (time-slot) menu, so backing out of it read as
'the day picker does not work'.

The grid now stays mounted during in-session reloads behind a light
loading overlay, a day pick re-anchors the window immediately (the slot
menu is an optional refinement), D-pad SELECT arms the key-up
suppressor before opening the menu, and the day chip uses the existing
'Tomorrow' translation.
2026-08-17 21:36:35 +02:00
edde746 b38df008ec refactor(screens): delete never-passed knobs, dead enum values, and the unconsumed context-menu onTap
plex_pin_auth_flow's QR knobs, ProfileNameField's navigation params, MusicDetailHeader.wideArtworkSize, and _buildReorderableList's bool were never passed; DownloadType.manage was never constructed; MediaContextMenu.onTap was declared but never invoked — removed with its four card passes (each card's own gesture handlers stay); the pin dialog's obscure chain was always true and _PinKey.label's icon branches unreachable; libraries_screen._isInitialLoad was write-only; LiveTvActionsMixin loses dead findChannel (the generic parameter stays — required by the on-clause, the report was wrong); add_jellyfin's focus-map removal branch ran only against an empty map; the recommended tab's hub-key null fallback was dead behind _ensureHubKeys. media_card also loses its always-false seasonal-rank comparison from the CatalogRankScope removal.
2026-08-17 19:02:24 +02:00
edde746 fb048097ab refactor(livetv): one refresh-gating mixin, one multi-server iteration helper, simpler recordings coalescer
The pause/resume/ticker state machine (three gates, lifecycle observer, TickerMode subscription, timer sync) was triplicated across the What's On, Recordings, and Guide tabs — now LiveTvRefreshMixin, with the guide's drift catch-up layered on its hooks and the lifecycle test asserting the same contract against the shared implementation. Six hand-rolled multi-server loops share forEachLiveTvServer (the channel/favorites loaders deliberately iterate per DVR entry and say so). Recordings' Completer-based load drain became the same whenComplete coalescing shape the screen's channel loader already uses.
2026-08-17 19:02:20 +02:00
edde746 369c6279d6 fix(i18n): translate the player, downloads and server-setup text left in English
A Portuguese user reported "Skip Intro" rendering in English on Android TV.
The locale files were not the problem - all 22 were structurally complete.
skip_marker_button.dart simply never imported strings.g.dart and assigned
'Skip Intro' / 'Skip Credits' / 'Next Episode' as plain literals. An audit of
lib/ found ~120 more sites in the same state, in four shapes that need
different fixes:

A literal in a file that never imported the i18n layer is the easy one -
skip_marker_button, performance_stats, track_label_builder and codec_utils all
render text with no `t` in the file at all. TrackLabelBuilder._compose now takes
a fallbackLabel builder instead of an English fallbackPrefix, so the caller
supplies t.audioTracks.track / t.videoControls.subtitleTrack and every unnamed
audio and subtitle row in the track menus is localized.

English reaching the user through an exception message is the widest one, and
it needs care: MediaServerException.message feeds both toString() - logs and
Sentry grouping - and verbatim UI display. Localizing it in place would make
bug-report logs follow the user's locale and split one Sentry issue into 22.
The MediaServer and Seerr families instead gain a nullable `display` alongside
the English `message`, and the six screens that print these errors read
`display ?? message`. PlaybackException keeps the opposite rule, because it
already carries a PlaybackFailureReason for logic and classifyPlaybackFailure
already builds it from t.messages: its stragglers are localized at the throw
site. That also removes the literal "Exception: " prefix Live TV users saw on
a tune failure, since PlaybackException.toString() returns the bare message.

Localized parts hand-concatenated with bare English are the shape no search for
Text('...') can find: '${t.common.pause} auto-scroll' on the home carousel,
'${day} at ${time}' on the Live TV schedule row, and an actor-screen count that
hand-rolled its plural as `n == 1 ? 'title' : 'titles'` - wrong for ru and pl
regardless of translation, now a real Slang plural.

Finally a literal assigned to provider state that a widget renders later:
DownloadProgress.errorMessage, and the four background_downloader notification
bodies, which sit inside a plugin config call where no widget-shaped search
reaches them.

Two things surfaced while converting. track_chapter_controls compared a track
label against 'Audio Track N' to swap in a localized version; once the builder
localized its own fallback that branch became unreachable, so it and the
orphaned _joinTrackLabel are gone. And discovery_view's PeerError fallback arm
looks like a leak but is not - its producers already localize, and a test says
so - so it stays as it is.

All 21 non-base locales are translated, including the 21 keys left empty by
earlier commits that were falling back to English. No locale has an empty value.

scripts/check_hardcoded_strings.py guards the three shapes a structural check
can see, and runs in ci_checks.sh after translation hygiene. Its first draft
passed its own tests while missing this very bug, because 'Skip Intro' is bound
to a local rather than handed to Text(); the name-bound rule that closes that
gap is restricted to phrase-shaped literals, or it cannot tell copy from the
identifiers this codebase binds constantly ('cast_row', 'auto', 'liveTv'). It
cannot see English inside a throw or assigned to a provider field - neither is
distinguishable from a log message without dataflow analysis - and the docstring
says so. label: and actionLabel: are deliberately unscanned: here they name a
diagnostic operation, and a check that is chronically red is a check that gets
switched off.

One commit rather than one per area: the keys, the 22 locale files and the
generated output are a single unit, and any partial split fails the repo's own
unused-key scan on the way through.

close #1856
2026-08-10 15:32:43 +02:00
edde746 63f2bedf2c fix(livetv): navigate guide rows in displayed source-group order
Vertical D-pad/arrow navigation stepped through the flat channel list,
which is number-sorted across servers. With overlapping channel numbers
from multiple DVRs, focus interleaved source groups and could dead-end
before the last displayed row. Derive the up/down order from the same
grouped rows the guide renders.

close #1843
2026-08-09 11:12:09 +02:00
edde746 ff461d9f71 fix(livetv): explain a guide emptied by the favorites filter
With "Default to Favorite Channels" enabled and no favorites stored —
or only favorites left over from a since-rebuilt lineup — the favorites
filter reduced the guide to zero channels and GuideTab rendered just
the timeline bar: no rows, no message, no sign a filter was active.
Users read it as Live TV being broken; the Aug 8 report in #887 shows
44 channels and 447 grid programs loading in the log while the
screenshot shows a blank guide and 0 favorite channels.

When the filter removes every loaded channel, the guide tab now shows
an empty state naming the cause with a "Show All Channels" action that
clears the filter. The action stays D-pad reachable: the tab-bar focus
handoff falls through to the action's focus node while the empty state
replaces GuideTab, and activating it hands focus back to the restored
guide content. Favorites that match no loaded channel get the same
treatment as an empty favorites list.

Verified: flutter test test/screens/livetv/, analyzer parity,
clean_translations --check --strict, and slang codegen freshness.

Refs #887.
2026-08-09 11:12:08 +02:00
edde746 24a041977b fix(sheets): size sheets to their content instead of 75% of the window
Sheets rendered at the host's maximum height regardless of content, so a
one-item player queue or a two-track picker filled ~75% of a desktop window
with empty space.

BottomSheetPageScaffold now always lays out Column(mainAxisSize: .min) plus
Flexible(child:), and each sheet body shrink-wraps its own scrollable. The
scaffold's shrinkWrap flag is gone: its old true branch put the child on an
unbounded axis, where an over-tall list overflowed instead of clamping and
scrolling. Measured on a 1600x1000 window, the chapter sheet goes from 750px
to 118px for one chapter and the two-column track sheet from 750px to 154px
for one audio and one subtitle track, both still clamping at the cap.

Add SheetSplitColumns for the three side-by-side sheet layouts. A bare
VerticalDivider has no intrinsic height, so it inflated those rows to the cap
on its own; the rule now paints from a Positioned.fill that cannot size the
Stack. IntrinsicHeight is not an option because a Viewport has no intrinsics.

Because sheets are bottom-anchored, a content-driven height moves the sheet's
top edge and everything above the change point. Three surfaces opt out for
that reason and say so at the call site: SubtitleSearchSheet and its language
picker keep filling, since both refilter under an autofocused field;
FiltersBottomSheet holds the outgoing page's height through its loading
transient; and RatingBottomSheet no longer hides MAL/AniList rows
asynchronously, which used to slide live rating controls down two rows several
hundred ms after open. Wrap the shared StateMessageWidget at the filters sheet
boundary rather than editing a widget with 33 filling call sites.

The host gains an AnimatedSize keyed per sheet session so nested pushes ease
while a replacing show adopts its own height, a 720px absolute height ceiling
on desktop windows only, and a min(max(25%, 96px), 60%) drag-dismiss threshold
so short sheets neither close on a nudge nor become undismissable.

Add videoControls.noAudioDevicesAvailable so the audio output page shows a
placeholder instead of a bare header while devices load.
2026-08-08 00:37:17 +02:00
edde746 6b87b6551e fix(livetv): keep the favorites filter narrow while favorites reload
Only the load that commits a favorites set writes the loaded flag, so a
refresh keeps the previous set authoritative. Clearing it up front widened
the guide to the full lineup for the whole round-trip and moved the D-pad
cursor when it collapsed back.
2026-07-27 17:44:52 +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 9429a76acc refactor: share search field, auth dialog, and list-download plumbing
The search screens, the out-of-band auth dialogs, the live TV guide and the
list-download paths each carried their own copy of the same shell. Extracts
SearchInputField and PendingAuthDialog and routes the duplicated download
and guide helpers through one implementation.
2026-07-26 06:09:49 +02:00
edde746 c68ffe9ed0 refactor: share the toolbar scrim and dedupe playback and download paths
Extracts the repeated toolbar fade into a single ToolbarScrim widget, folds
duplicated request/retry handling in the media server HTTP client, and
collapses the parallel playback-source, download-manager and live TV helper
paths into shared implementations.
2026-07-26 06:09:49 +02:00
edde746 83f4e2a263 refactor: fold single-use helpers into their call sites
Collapses indirection layers and one-caller abstractions across the video
player, shortcut dispatch, shader loading and context-menu code, including
the VideoPIPManager pass-through over PipService.
2026-07-26 06:09:49 +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 04d8070fd4 refactor: pin the look-alike code paths that must not be merged
Several pairs of near-identical code paths differ in one load-bearing
line. Each site now carries a comment naming the invariant that forces it
apart, backed by a characterization test so a future deduplication fails
loudly instead of silently changing behaviour.

Pinned: focusable wrapper vs. chip D-pad activation policy, profile
connection cleanup's raw-id vs. ServerId-typed server projections, live TV
tab loaders, video player display matching and playback service wiring,
track selection container ordering, tracker HTTP client status ladder, and
the MediaServerHttpClient shutdown/cancellation contract versus
ManagedHttpClient's closing guard.

New tests:
  test/focus/dpad_activation_policy_test.dart
  test/services/track_selection_container_ordinal_test.dart
  test/services/trackers/tracker_status_ladder_test.dart
  test/utils/media_server_http_client_shutdown_test.dart
2026-07-26 06:09:47 +02:00
edde746 c1cfb9609e perf(livetv): virtualize guide rendering 2026-07-25 16:47:10 +02:00
edde746andjunyou1998 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 f8bfecf57d fix(media): serialize browsing and metadata mutations 2026-07-24 03:46:50 +02:00
edde746 e0bf66eea8 fix(runtime): harden application service boundaries 2026-07-24 03:46:46 +02:00
edde746 d5f3e581df fix: eliminate cross-app consistency drift 2026-07-13 23:13:53 +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
edde746 e03edad824 fix: prevent async state and platform regressions 2026-07-13 00:03:52 +02:00
edde746 b9f828ae88 refactor(livetv): reuse standard hub cards 2026-07-12 22:11:30 +02:00
edde746 b4575c9780 fix: preserve live TV state across refreshes 2026-07-12 18:59:42 +02:00
edde746 97f7508067 refactor(core): consolidate shared app foundations 2026-07-12 17:31:12 +02:00
edde746 359a6bf02a refactor(live-tv): split optional DVR support 2026-07-12 08:42:25 +02:00
edde746 f78089edf8 refactor(input): share D-pad long press state 2026-07-12 08:42:24 +02:00
edde746 397de2698d feat(livetv): flat M3E card redesign for guide, recordings, and details 2026-07-10 06:41:15 +02:00
edde746 632d2ec16b feat(livetv): let user pick and remember the recording target library
close #1513
2026-07-10 06:41:15 +02:00
edde746 ef75887504 style: apply analyzer lint fixes across lib
Initializing formals, unawaited() on fire-and-forget futures, unused
import removal, wildcard params, and mounted vs context.mounted.
2026-07-06 15:56:56 +02:00
edde746 adf732a1a1 fix(ui): app-bar tab chip strips scroll instead of overflowing
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.
2026-07-06 12:17:03 +02:00
edde746 dcfe0f8308 refactor(player): live playback drives LiveTvPlaybackSession 2026-06-12 16:15:20 +02:00
edde746 133db721a2 fix(livetv): auto-catch-up stale guide window to now
close #1297
2026-06-12 04:13:59 +02:00
edde746 cbaed8cdfb fix(livetv): tune current guide programs on tv
close #1298
2026-06-11 07:15:31 +02:00
edde746 248b5fd91a refactor(back): adopt host-owned sheet back in watch-together + player
watch_together, video_player, mobile_remote and live_tv_show_schedule now
pass canPop/onSystemBack to OverlaySheetHost instead of hand-rolling a
PopScope. Restores the iOS swipe-back on watch_together and centralizes
the sheet-close-on-back logic.
2026-06-07 09:13:49 +02:00
edde746 32421e4c84 fix(tv): reach app bar actions via up from tabs
close #1202
2026-06-06 08:51:08 +02:00
edde746 75b1c86306 feat(ui): add m3 app menus 2026-06-04 16:22:31 +02:00
edde746 145881318e fix: harden server-scoped state 2026-06-04 13:47:06 +02:00
edde746 74b8dc4561 refactor: type server identifiers 2026-06-01 11:06:03 +02:00
edde746 048d22cb99 fix(livetv): expand program summaries 2026-05-22 20:52:21 +02:00
edde746 0582ab2947 fix(livetv): show active recording indicators
close #1076
2026-05-21 00:19:21 +02:00
edde746 e98d7febdd chore: upgrade Flutter to 3.44 2026-05-18 22:52:51 +02:00
edde746 5a68711f7b feat(ui): add pointer cursors to clickable controls
close #1068
2026-05-18 15:56:41 +02:00