diff --git a/lib/focus/card_focus_scope.dart b/lib/focus/card_focus_scope.dart new file mode 100644 index 00000000..02236d40 --- /dev/null +++ b/lib/focus/card_focus_scope.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +import 'focus_theme.dart'; + +/// Exposes the focus state of an enclosing focus wrapper to a descendant +/// [CardFocusBorder] that draws the focus border itself. +/// +/// Wrappers ([FocusableWrapper]/[FocusBuilders.buildFocusableCard]) insert this +/// instead of painting a border when `delegateFocusBorder` is set, so cards can +/// put the border on the exact rect the design highlights (the poster image, +/// not the card-plus-captions rect — issue #1278). Only the [CardFocusBorder] +/// element registers a dependency, so a focus flip rebuilds just that border +/// box: a stable `child:` card subtree (e.g. the TV rail's MediaCard) is not +/// rebuilt. +class CardFocusScope extends InheritedWidget { + const CardFocusScope({super.key, required this.showFocus, required super.child}); + + /// Whether the enclosing wrapper currently shows focus visuals + /// (focused while in keyboard/d-pad input mode). + final bool showFocus; + + /// Null when no delegating wrapper is above (touch mode skips the focus + /// wrappers entirely) — [CardFocusBorder] then renders its child bare. + static bool? maybeOf(BuildContext context) => context.dependOnInheritedWidgetOfExactType()?.showFocus; + + @override + bool updateShouldNotify(CardFocusScope oldWidget) => showFocus != oldWidget.showFocus; +} + +/// Draws the focus border around its child based on the enclosing +/// [CardFocusScope], letting the card decide which rect gets highlighted. +/// +/// Defaults to an outside stroke so the border hugs the child exactly like the +/// full-bleed card treatment (the child's own corner radius nests inside it). +/// Decoration only — never affects layout. Renders the child unchanged when no +/// scope is present. +class CardFocusBorder extends StatelessWidget { + const CardFocusBorder({ + super.key, + required this.borderRadius, + this.strokeAlign = BorderSide.strokeAlignOutside, + required this.child, + }); + + final double borderRadius; + final double strokeAlign; + final Widget child; + + @override + Widget build(BuildContext context) { + final showFocus = CardFocusScope.maybeOf(context); + if (showFocus == null) return child; + + return AnimatedContainer( + duration: FocusTheme.getAnimationDuration(context), + curve: Curves.easeOutCubic, + foregroundDecoration: FocusTheme.focusDecoration( + context, + isFocused: showFocus, + borderRadius: borderRadius, + borderStrokeAlign: strokeAlign, + ), + child: child, + ); + } +} diff --git a/lib/focus/focus_glow_overlay.dart b/lib/focus/focus_glow_overlay.dart index f2f1aad3..547d3f97 100644 --- a/lib/focus/focus_glow_overlay.dart +++ b/lib/focus/focus_glow_overlay.dart @@ -2,6 +2,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import '../services/device_performance.dart'; +import '../services/settings_service.dart'; import 'focus_theme.dart'; /// Renders the focus glow for a focused card in the root [Overlay] so it paints @@ -56,10 +57,15 @@ class _FocusGlowOverlayState extends State { /// fades out before the portal is hidden in [_handleFadeEnd]. bool _visible = false; + /// Glow is skipped on the reduced effects tier (blurred shadows + fade + /// saveLayer are too expensive on weak GPUs) and when the user turned the + /// Focus Glow setting off (#1278). The crisp focus border remains. + static bool get _disabled => DevicePerformance.isReduced || !SettingsService.instance.read(SettingsService.focusGlow); + @override void initState() { super.initState(); - if (widget.isFocused && !DevicePerformance.isReduced) { + if (widget.isFocused && !_disabled) { _visible = true; _controller.show(); } @@ -68,7 +74,7 @@ class _FocusGlowOverlayState extends State { @override void didUpdateWidget(FocusGlowOverlay oldWidget) { super.didUpdateWidget(oldWidget); - if (DevicePerformance.isReduced) return; + if (_disabled) return; if (widget.isFocused == oldWidget.isFocused) return; if (widget.isFocused) { _controller.show(); @@ -92,9 +98,7 @@ class _FocusGlowOverlayState extends State { @override Widget build(BuildContext context) { - // Reduced tier: no glow at all — the blurred shadows + fade saveLayer are - // too expensive on weak GPUs. The crisp in-card focus border remains. - if (DevicePerformance.isReduced) return widget.child; + if (_disabled) return widget.child; // Gate the LeaderLayer to the focused card only: when not focused and not // mid-fade, return the bare child (no OverlayPortal, no leader). diff --git a/lib/focus/focusable_wrapper.dart b/lib/focus/focusable_wrapper.dart index fd180520..047222aa 100644 --- a/lib/focus/focusable_wrapper.dart +++ b/lib/focus/focusable_wrapper.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import '../widgets/clickable_cursor.dart'; import '../utils/text_input_diagnostics.dart'; +import 'card_focus_scope.dart'; import 'dpad_navigator.dart'; import 'focus_glow_overlay.dart'; import 'focus_theme.dart'; @@ -110,14 +111,13 @@ class FocusableWrapper extends StatefulWidget { /// Scale used for the focus animation. final double focusScale; - /// Stroke alignment for the focus border. - final double focusBorderStrokeAlign; - /// Whether to draw a glow around the focused widget. final bool useFocusGlow; - /// Whether to draw the focus border as a foreground decoration. - final bool useForegroundFocusDecoration; + /// Skip drawing the focus border here and expose the focus state through a + /// [CardFocusScope] instead, so the child places the border on the exact + /// rect it wants highlighted (e.g. MediaCard's poster image). + final bool delegateFocusBorder; /// Whether descendants can receive focus. /// Set to false when the child widget has its own Focus (e.g. buttons) @@ -150,9 +150,8 @@ class FocusableWrapper extends StatefulWidget { this.focusColor, this.disableScale = false, this.focusScale = FocusTheme.focusScale, - this.focusBorderStrokeAlign = BorderSide.strokeAlignInside, this.useFocusGlow = false, - this.useForegroundFocusDecoration = false, + this.delegateFocusBorder = false, this.descendantsAreFocusable = true, }); @@ -471,16 +470,6 @@ class _FocusableWrapperState extends State with SingleTickerPr _animationController.duration = duration; } - // Choose decoration based on useBackgroundFocus - final focusDecoration = widget.useBackgroundFocus - ? FocusTheme.focusBackgroundDecoration(isFocused: showFocus, borderRadius: widget.borderRadius) - : FocusTheme.focusDecoration( - context, - isFocused: showFocus, - borderRadius: widget.borderRadius, - color: widget.focusColor, - borderStrokeAlign: widget.focusBorderStrokeAlign, - ); Widget result = Focus( focusNode: _focusNode, autofocus: widget.autofocus, @@ -493,13 +482,25 @@ class _FocusableWrapperState extends State with SingleTickerPr final shouldScale = showFocus && !widget.disableScale; // The glow (full-bleed cards) is drawn in an overlay above siblings so // it stays symmetric; the in-card decoration only carries the border. - Widget card = AnimatedContainer( - duration: duration, - curve: Curves.easeOutCubic, - decoration: widget.useForegroundFocusDecoration ? null : focusDecoration, - foregroundDecoration: widget.useForegroundFocusDecoration ? focusDecoration : null, - child: widget.child, - ); + Widget card; + if (widget.delegateFocusBorder) { + card = CardFocusScope(showFocus: showFocus, child: widget.child); + } else { + final focusDecoration = widget.useBackgroundFocus + ? FocusTheme.focusBackgroundDecoration(isFocused: showFocus, borderRadius: widget.borderRadius) + : FocusTheme.focusDecoration( + context, + isFocused: showFocus, + borderRadius: widget.borderRadius, + color: widget.focusColor, + ); + card = AnimatedContainer( + duration: duration, + curve: Curves.easeOutCubic, + decoration: focusDecoration, + child: widget.child, + ); + } if (widget.useFocusGlow) { card = FocusGlowOverlay( isFocused: showFocus, diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index 6c71dd54..d595d791 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Показвай постера на всеки сезон над неговия таб", "tvFullCardLayout": "Пълни TV карти", "tvFullCardLayoutDescription": "Използвай TV карти само с изображения, с насложени имена на актьорите", + "focusGlow": "Сияние при фокус", + "focusGlowDescription": "Показвай меко сияние около фокусираната карта", "hideSpoilers": "Скривай спойлери за негледани епизоди", "hideSpoilersDescription": "Замазвай миниатюри и описания за негледани епизоди", "playerBackend": "Енджин на плеъра", diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index 241b6885..4ba28d12 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Vis hver sæsons plakat over dens fane", "tvFullCardLayout": "Fuldflade TV-kort", "tvFullCardLayoutDescription": "Brug TV-kort kun med billeder og skuespillernavne ovenpå", + "focusGlow": "Fokusglød", + "focusGlowDescription": "Vis en blød glød omkring det fokuserede kort", "hideSpoilers": "Skjul spoilere for usete episoder", "hideSpoilersDescription": "Slør miniaturebilleder og beskrivelser for usete episoder", "playerBackend": "Afspillerbackend", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index fd6cb07d..e15db119 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Poster jeder Staffel über ihrem Tab anzeigen", "tvFullCardLayout": "Vollflächige TV-Karten", "tvFullCardLayoutDescription": "TV-Karten nur mit Bild verwenden und Darstellernamen einblenden", + "focusGlow": "Fokus-Leuchten", + "focusGlowDescription": "Sanftes Leuchten um die fokussierte Karte anzeigen", "hideSpoilers": "Spoiler für nicht gesehene Episoden verbergen", "hideSpoilersDescription": "Vorschaubilder und Beschreibungen ungesehener Episoden verwischen", "playerBackend": "Player-Backend", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 047e4ff2..b0491220 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Show each season's poster above its tab", "tvFullCardLayout": "Full TV Cards", "tvFullCardLayoutDescription": "Use image-only TV cards with actor names overlaid", + "focusGlow": "Focus Glow", + "focusGlowDescription": "Draw a soft glow around the focused card", "visualEffects": "Visual Effects", "visualEffectsAuto": "Auto", "visualEffectsAutoDescription": "Reduce effects automatically on low-power devices", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index c49de2a6..2f3f46ad 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Mostrar el póster de cada temporada sobre su pestaña", "tvFullCardLayout": "Tarjetas TV completas", "tvFullCardLayoutDescription": "Usar tarjetas TV solo con imagen y nombres de actores superpuestos", + "focusGlow": "Brillo de foco", + "focusGlowDescription": "Mostrar un brillo suave alrededor de la tarjeta con foco", "hideSpoilers": "Ocultar spoilers de episodios no vistos", "hideSpoilersDescription": "Desenfocar miniaturas y descripciones de episodios no vistos", "playerBackend": "Reproductor", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index c89ddba2..bd76c50a 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Afficher l'affiche de chaque saison au-dessus de son onglet", "tvFullCardLayout": "Cartes TV pleines", "tvFullCardLayoutDescription": "Utiliser des cartes TV avec image seule et noms des acteurs superposés", + "focusGlow": "Halo de sélection", + "focusGlowDescription": "Afficher un léger halo autour de la carte sélectionnée", "hideSpoilers": "Masquer les spoilers des épisodes non vus", "hideSpoilersDescription": "Flouter les miniatures et descriptions des épisodes non vus", "playerBackend": "Moteur de lecture", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index f08bdf75..d51a8a86 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Mostra il poster di ogni stagione sopra la sua scheda", "tvFullCardLayout": "Schede TV piene", "tvFullCardLayoutDescription": "Usa schede TV solo immagine con i nomi degli attori sovrapposti", + "focusGlow": "Bagliore di selezione", + "focusGlowDescription": "Mostra un leggero bagliore attorno alla scheda selezionata", "hideSpoilers": "Nascondi spoiler per episodi non visti", "hideSpoilersDescription": "Sfoca miniature e descrizioni degli episodi non visti", "playerBackend": "Motore di riproduzione", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index 4ae713a6..9457afd1 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "各シーズンのポスターをタブの上に表示します", "tvFullCardLayout": "フルTVカード", "tvFullCardLayoutDescription": "TVカードを画像のみで表示し、俳優名を重ねて表示します", + "focusGlow": "フォーカス時の光彩", + "focusGlowDescription": "フォーカス中のカードの周りに柔らかい光彩を表示します", "hideSpoilers": "未視聴エピソードのネタバレを非表示", "hideSpoilersDescription": "未視聴エピソードのサムネイルと説明をぼかします", "playerBackend": "プレーヤーバックエンド", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 1aedea28..d4833fa2 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "각 시즌 포스터를 탭 위에 표시", "tvFullCardLayout": "전체 TV 카드", "tvFullCardLayoutDescription": "TV 카드에 이미지만 표시하고 배우 이름을 오버레이로 표시", + "focusGlow": "포커스 글로우", + "focusGlowDescription": "포커스된 카드 주위에 은은한 빛 효과를 표시", "hideSpoilers": "미시청 에피소드 스포일러 숨기기", "hideSpoilersDescription": "시청하지 않은 에피소드의 썸네일과 설명을 흐리게 처리", "playerBackend": "플레이어 백엔드", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index 4b993119..d7108364 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Vis hver sesongs plakat over fanen", "tvFullCardLayout": "Fulle TV-kort", "tvFullCardLayoutDescription": "Bruk bildebaserte TV-kort med skuespillernavn lagt over", + "focusGlow": "Fokusglød", + "focusGlowDescription": "Vis en myk glød rundt kortet i fokus", "hideSpoilers": "Skjul spoilere for usette episoder", "hideSpoilersDescription": "Slør miniatyrbilder og beskrivelser for usette episoder", "playerBackend": "Spillermotor", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index b4ae51db..8e9129d6 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Toon de poster van elk seizoen boven het tabblad", "tvFullCardLayout": "Volledige tv-kaarten", "tvFullCardLayoutDescription": "Gebruik tv-kaarten met alleen afbeeldingen en namen van acteurs als overlay", + "focusGlow": "Focusgloed", + "focusGlowDescription": "Toon een zachte gloed rond de kaart met focus", "hideSpoilers": "Spoilers voor ongekeken afleveringen verbergen", "hideSpoilersDescription": "Vervaag miniaturen en beschrijvingen voor niet-bekeken afleveringen", "playerBackend": "Speler backend", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index 1a009208..83f6851e 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Pokazuj plakat każdego sezonu nad jego kartą", "tvFullCardLayout": "Pełne karty TV", "tvFullCardLayoutDescription": "Używaj kart TV tylko z obrazem i nałożonymi nazwiskami aktorów", + "focusGlow": "Poświata zaznaczenia", + "focusGlowDescription": "Wyświetlaj delikatną poświatę wokół zaznaczonej karty", "hideSpoilers": "Ukryj spoilery nieobejrzanych odcinków", "hideSpoilersDescription": "Rozmywaj miniatury i opisy nieobejrzanych odcinków", "playerBackend": "Backend odtwarzacza", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index 93586fd4..81d7737b 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Mostrar o pôster de cada temporada acima da aba", "tvFullCardLayout": "Cartões TV completos", "tvFullCardLayoutDescription": "Usar cartões de TV só com imagem e nomes dos atores sobrepostos", + "focusGlow": "Brilho de foco", + "focusGlowDescription": "Mostrar um brilho suave à volta do cartão em foco", "hideSpoilers": "Ocultar Spoilers de Episódios Não Assistidos", "hideSpoilersDescription": "Desfocar miniaturas e descrições de episódios não vistos", "playerBackend": "Backend do Player", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index a61eb0fd..f09718a7 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Показывать постер каждого сезона над его вкладкой", "tvFullCardLayout": "Полные TV-карточки", "tvFullCardLayoutDescription": "Использовать TV-карточки только с изображением и именами актёров поверх него", + "focusGlow": "Свечение при фокусе", + "focusGlowDescription": "Показывать мягкое свечение вокруг карточки в фокусе", "hideSpoilers": "Скрыть спойлеры непросмотренных эпизодов", "hideSpoilersDescription": "Размывать миниатюры и описания непросмотренных серий", "playerBackend": "Бэкенд плеера", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 4534b73f..d5b63de9 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 16 -/// Strings: 20265 (1266 per locale) +/// Strings: 20297 (1268 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_bg.g.dart b/lib/i18n/strings_bg.g.dart index 3bf93b47..195578e7 100644 --- a/lib/i18n/strings_bg.g.dart +++ b/lib/i18n/strings_bg.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsBg extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Показвай постера на всеки сезон над неговия таб'; @override String get tvFullCardLayout => 'Пълни TV карти'; @override String get tvFullCardLayoutDescription => 'Използвай TV карти само с изображения, с насложени имена на актьорите'; + @override String get focusGlow => 'Сияние при фокус'; + @override String get focusGlowDescription => 'Показвай меко сияние около фокусираната карта'; @override String get hideSpoilers => 'Скривай спойлери за негледани епизоди'; @override String get hideSpoilersDescription => 'Замазвай миниатюри и описания за негледани епизоди'; @override String get playerBackend => 'Енджин на плеъра'; @@ -2039,6 +2041,8 @@ extension on TranslationsBg { 'settings.showSeasonPostersOnTabsDescription' => 'Показвай постера на всеки сезон над неговия таб', 'settings.tvFullCardLayout' => 'Пълни TV карти', 'settings.tvFullCardLayoutDescription' => 'Използвай TV карти само с изображения, с насложени имена на актьорите', + 'settings.focusGlow' => 'Сияние при фокус', + 'settings.focusGlowDescription' => 'Показвай меко сияние около фокусираната карта', 'settings.hideSpoilers' => 'Скривай спойлери за негледани епизоди', 'settings.hideSpoilersDescription' => 'Замазвай миниатюри и описания за негледани епизоди', 'settings.playerBackend' => 'Енджин на плеъра', @@ -2425,10 +2429,10 @@ extension on TranslationsBg { 'subtitlingStyling.text' => 'Текст', 'subtitlingStyling.border' => 'Рамка', 'subtitlingStyling.background' => 'Фон', - 'subtitlingStyling.fontSize' => 'Размер на шрифта', - 'subtitlingStyling.textColor' => 'Цвят на текста', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Размер на шрифта', + 'subtitlingStyling.textColor' => 'Цвят на текста', 'subtitlingStyling.borderSize' => 'Размер на рамката', 'subtitlingStyling.borderColor' => 'Цвят на рамката', 'subtitlingStyling.backgroundOpacity' => 'Прозрачност на фона', @@ -2939,10 +2943,10 @@ extension on TranslationsBg { 'companionRemote.remote.tabMore' => 'Още', 'companionRemote.remote.menu' => 'Меню', 'companionRemote.remote.tabNavigation' => 'Навигация с Tab', - 'companionRemote.remote.tabDiscover' => 'Открий', - 'companionRemote.remote.tabLibraries' => 'Библиотеки', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Открий', + 'companionRemote.remote.tabLibraries' => 'Библиотеки', 'companionRemote.remote.tabSearch' => 'Търсене', 'companionRemote.remote.tabDownloads' => 'Изтегляния', 'companionRemote.remote.tabSettings' => 'Настройки', diff --git a/lib/i18n/strings_da.g.dart b/lib/i18n/strings_da.g.dart index f5c83c9b..c9668506 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsDa extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Vis hver sæsons plakat over dens fane'; @override String get tvFullCardLayout => 'Fuldflade TV-kort'; @override String get tvFullCardLayoutDescription => 'Brug TV-kort kun med billeder og skuespillernavne ovenpå'; + @override String get focusGlow => 'Fokusglød'; + @override String get focusGlowDescription => 'Vis en blød glød omkring det fokuserede kort'; @override String get hideSpoilers => 'Skjul spoilere for usete episoder'; @override String get hideSpoilersDescription => 'Slør miniaturebilleder og beskrivelser for usete episoder'; @override String get playerBackend => 'Afspillerbackend'; @@ -2039,6 +2041,8 @@ extension on TranslationsDa { 'settings.showSeasonPostersOnTabsDescription' => 'Vis hver sæsons plakat over dens fane', 'settings.tvFullCardLayout' => 'Fuldflade TV-kort', 'settings.tvFullCardLayoutDescription' => 'Brug TV-kort kun med billeder og skuespillernavne ovenpå', + 'settings.focusGlow' => 'Fokusglød', + 'settings.focusGlowDescription' => 'Vis en blød glød omkring det fokuserede kort', 'settings.hideSpoilers' => 'Skjul spoilere for usete episoder', 'settings.hideSpoilersDescription' => 'Slør miniaturebilleder og beskrivelser for usete episoder', 'settings.playerBackend' => 'Afspillerbackend', @@ -2425,10 +2429,10 @@ extension on TranslationsDa { 'subtitlingStyling.text' => 'Tekst', 'subtitlingStyling.border' => 'Kant', 'subtitlingStyling.background' => 'Baggrund', - 'subtitlingStyling.fontSize' => 'Skriftstørrelse', - 'subtitlingStyling.textColor' => 'Tekstfarve', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Skriftstørrelse', + 'subtitlingStyling.textColor' => 'Tekstfarve', 'subtitlingStyling.borderSize' => 'Kantstørrelse', 'subtitlingStyling.borderColor' => 'Kantfarve', 'subtitlingStyling.backgroundOpacity' => 'Baggrundsgennemsigtighed', @@ -2939,10 +2943,10 @@ extension on TranslationsDa { 'companionRemote.remote.tabMore' => 'Mere', 'companionRemote.remote.menu' => 'Menu', 'companionRemote.remote.tabNavigation' => 'Fanenavigation', - 'companionRemote.remote.tabDiscover' => 'Opdag', - 'companionRemote.remote.tabLibraries' => 'Biblioteker', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Opdag', + 'companionRemote.remote.tabLibraries' => 'Biblioteker', 'companionRemote.remote.tabSearch' => 'Søg', 'companionRemote.remote.tabDownloads' => 'Downloads', 'companionRemote.remote.tabSettings' => 'Indstillinger', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 910a4352..c4e081a5 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsDe extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Poster jeder Staffel über ihrem Tab anzeigen'; @override String get tvFullCardLayout => 'Vollflächige TV-Karten'; @override String get tvFullCardLayoutDescription => 'TV-Karten nur mit Bild verwenden und Darstellernamen einblenden'; + @override String get focusGlow => 'Fokus-Leuchten'; + @override String get focusGlowDescription => 'Sanftes Leuchten um die fokussierte Karte anzeigen'; @override String get hideSpoilers => 'Spoiler für nicht gesehene Episoden verbergen'; @override String get hideSpoilersDescription => 'Vorschaubilder und Beschreibungen ungesehener Episoden verwischen'; @override String get playerBackend => 'Player-Backend'; @@ -2039,6 +2041,8 @@ extension on TranslationsDe { 'settings.showSeasonPostersOnTabsDescription' => 'Poster jeder Staffel über ihrem Tab anzeigen', 'settings.tvFullCardLayout' => 'Vollflächige TV-Karten', 'settings.tvFullCardLayoutDescription' => 'TV-Karten nur mit Bild verwenden und Darstellernamen einblenden', + 'settings.focusGlow' => 'Fokus-Leuchten', + 'settings.focusGlowDescription' => 'Sanftes Leuchten um die fokussierte Karte anzeigen', 'settings.hideSpoilers' => 'Spoiler für nicht gesehene Episoden verbergen', 'settings.hideSpoilersDescription' => 'Vorschaubilder und Beschreibungen ungesehener Episoden verwischen', 'settings.playerBackend' => 'Player-Backend', @@ -2425,10 +2429,10 @@ extension on TranslationsDe { 'subtitlingStyling.text' => 'Text', 'subtitlingStyling.border' => 'Rahmen', 'subtitlingStyling.background' => 'Hintergrund', - 'subtitlingStyling.fontSize' => 'Schriftgröße', - 'subtitlingStyling.textColor' => 'Textfarbe', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Schriftgröße', + 'subtitlingStyling.textColor' => 'Textfarbe', 'subtitlingStyling.borderSize' => 'Rahmengröße', 'subtitlingStyling.borderColor' => 'Rahmenfarbe', 'subtitlingStyling.backgroundOpacity' => 'Hintergrunddeckkraft', @@ -2939,10 +2943,10 @@ extension on TranslationsDe { 'companionRemote.remote.tabMore' => 'Mehr', 'companionRemote.remote.menu' => 'Menü', 'companionRemote.remote.tabNavigation' => 'Tab-Navigation', - 'companionRemote.remote.tabDiscover' => 'Entdecken', - 'companionRemote.remote.tabLibraries' => 'Mediatheken', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Entdecken', + 'companionRemote.remote.tabLibraries' => 'Mediatheken', 'companionRemote.remote.tabSearch' => 'Suche', 'companionRemote.remote.tabDownloads' => 'Downloads', 'companionRemote.remote.tabSettings' => 'Einstellungen', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index f227909a..c28fc612 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -513,6 +513,12 @@ class TranslationsSettingsEn { /// en: 'Use image-only TV cards with actor names overlaid' String get tvFullCardLayoutDescription => 'Use image-only TV cards with actor names overlaid'; + /// en: 'Focus Glow' + String get focusGlow => 'Focus Glow'; + + /// en: 'Draw a soft glow around the focused card' + String get focusGlowDescription => 'Draw a soft glow around the focused card'; + /// en: 'Visual Effects' String get visualEffects => 'Visual Effects'; @@ -4604,6 +4610,8 @@ extension on Translations { 'settings.showSeasonPostersOnTabsDescription' => 'Show each season\'s poster above its tab', 'settings.tvFullCardLayout' => 'Full TV Cards', 'settings.tvFullCardLayoutDescription' => 'Use image-only TV cards with actor names overlaid', + 'settings.focusGlow' => 'Focus Glow', + 'settings.focusGlowDescription' => 'Draw a soft glow around the focused card', 'settings.visualEffects' => 'Visual Effects', 'settings.visualEffectsAuto' => 'Auto', 'settings.visualEffectsAutoDescription' => 'Reduce effects automatically on low-power devices', @@ -4990,10 +4998,10 @@ extension on Translations { 'messages.switchingToCompatiblePlayer' => 'Switching to compatible player...', 'messages.serverLimitTitle' => 'Playback failed', 'messages.serverLimitBody' => 'Server error (HTTP 500). A bandwidth/transcoding limit likely rejected this session. Ask the owner to adjust it.', - 'messages.logsUploaded' => 'Logs uploaded', - 'messages.logsUploadFailed' => 'Failed to upload logs', _ => null, } ?? switch (path) { + 'messages.logsUploaded' => 'Logs uploaded', + 'messages.logsUploadFailed' => 'Failed to upload logs', 'messages.logId' => 'Log ID', 'subtitlingStyling.text' => 'Text', 'subtitlingStyling.border' => 'Border', @@ -5504,10 +5512,10 @@ extension on Translations { 'companionRemote.pairing.sessionNotFound' => 'Device not found. Make sure Plezy is running on the host.', 'companionRemote.pairing.authFailed' => 'Authentication failed. Both devices need the same Plex account.', 'companionRemote.pairing.failedToConnect' => ({required Object error}) => 'Failed to connect: ${error}', - 'companionRemote.remote.disconnectConfirm' => 'Do you want to disconnect from the remote session?', - 'companionRemote.remote.reconnecting' => 'Reconnecting...', _ => null, } ?? switch (path) { + 'companionRemote.remote.disconnectConfirm' => 'Do you want to disconnect from the remote session?', + 'companionRemote.remote.reconnecting' => 'Reconnecting...', 'companionRemote.remote.attemptOf' => ({required Object current}) => 'Attempt ${current} of 5', 'companionRemote.remote.retryNow' => 'Retry Now', 'companionRemote.remote.tabRemote' => 'Remote', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 0a14a041..87f07921 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsEs extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Mostrar el póster de cada temporada sobre su pestaña'; @override String get tvFullCardLayout => 'Tarjetas TV completas'; @override String get tvFullCardLayoutDescription => 'Usar tarjetas TV solo con imagen y nombres de actores superpuestos'; + @override String get focusGlow => 'Brillo de foco'; + @override String get focusGlowDescription => 'Mostrar un brillo suave alrededor de la tarjeta con foco'; @override String get hideSpoilers => 'Ocultar spoilers de episodios no vistos'; @override String get hideSpoilersDescription => 'Desenfocar miniaturas y descripciones de episodios no vistos'; @override String get playerBackend => 'Reproductor'; @@ -2039,6 +2041,8 @@ extension on TranslationsEs { 'settings.showSeasonPostersOnTabsDescription' => 'Mostrar el póster de cada temporada sobre su pestaña', 'settings.tvFullCardLayout' => 'Tarjetas TV completas', 'settings.tvFullCardLayoutDescription' => 'Usar tarjetas TV solo con imagen y nombres de actores superpuestos', + 'settings.focusGlow' => 'Brillo de foco', + 'settings.focusGlowDescription' => 'Mostrar un brillo suave alrededor de la tarjeta con foco', 'settings.hideSpoilers' => 'Ocultar spoilers de episodios no vistos', 'settings.hideSpoilersDescription' => 'Desenfocar miniaturas y descripciones de episodios no vistos', 'settings.playerBackend' => 'Reproductor', @@ -2425,10 +2429,10 @@ extension on TranslationsEs { 'subtitlingStyling.text' => 'Texto', 'subtitlingStyling.border' => 'Borde', 'subtitlingStyling.background' => 'Fondo', - 'subtitlingStyling.fontSize' => 'Tamaño de Fuente', - 'subtitlingStyling.textColor' => 'Color de Texto', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Tamaño de Fuente', + 'subtitlingStyling.textColor' => 'Color de Texto', 'subtitlingStyling.borderSize' => 'Tamaño de Borde', 'subtitlingStyling.borderColor' => 'Color de Borde', 'subtitlingStyling.backgroundOpacity' => 'Opacidad de Fondo', @@ -2939,10 +2943,10 @@ extension on TranslationsEs { 'companionRemote.remote.tabMore' => 'Más', 'companionRemote.remote.menu' => 'Menú', 'companionRemote.remote.tabNavigation' => 'Navegación por pestañas', - 'companionRemote.remote.tabDiscover' => 'Descubrir', - 'companionRemote.remote.tabLibraries' => 'Bibliotecas', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Descubrir', + 'companionRemote.remote.tabLibraries' => 'Bibliotecas', 'companionRemote.remote.tabSearch' => 'Buscar', 'companionRemote.remote.tabDownloads' => 'Descargas', 'companionRemote.remote.tabSettings' => 'Configuración', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 6e81351e..315e2df6 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsFr extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Afficher l\'affiche de chaque saison au-dessus de son onglet'; @override String get tvFullCardLayout => 'Cartes TV pleines'; @override String get tvFullCardLayoutDescription => 'Utiliser des cartes TV avec image seule et noms des acteurs superposés'; + @override String get focusGlow => 'Halo de sélection'; + @override String get focusGlowDescription => 'Afficher un léger halo autour de la carte sélectionnée'; @override String get hideSpoilers => 'Masquer les spoilers des épisodes non vus'; @override String get hideSpoilersDescription => 'Flouter les miniatures et descriptions des épisodes non vus'; @override String get playerBackend => 'Moteur de lecture'; @@ -2039,6 +2041,8 @@ extension on TranslationsFr { 'settings.showSeasonPostersOnTabsDescription' => 'Afficher l\'affiche de chaque saison au-dessus de son onglet', 'settings.tvFullCardLayout' => 'Cartes TV pleines', 'settings.tvFullCardLayoutDescription' => 'Utiliser des cartes TV avec image seule et noms des acteurs superposés', + 'settings.focusGlow' => 'Halo de sélection', + 'settings.focusGlowDescription' => 'Afficher un léger halo autour de la carte sélectionnée', 'settings.hideSpoilers' => 'Masquer les spoilers des épisodes non vus', 'settings.hideSpoilersDescription' => 'Flouter les miniatures et descriptions des épisodes non vus', 'settings.playerBackend' => 'Moteur de lecture', @@ -2425,10 +2429,10 @@ extension on TranslationsFr { 'subtitlingStyling.text' => 'Texte', 'subtitlingStyling.border' => 'Bordure', 'subtitlingStyling.background' => 'Arrière-plan', - 'subtitlingStyling.fontSize' => 'Taille de la police', - 'subtitlingStyling.textColor' => 'Couleur du texte', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Taille de la police', + 'subtitlingStyling.textColor' => 'Couleur du texte', 'subtitlingStyling.borderSize' => 'Taille de la bordure', 'subtitlingStyling.borderColor' => 'Couleur de la bordure', 'subtitlingStyling.backgroundOpacity' => 'Opacité d\'arrière-plan', @@ -2939,10 +2943,10 @@ extension on TranslationsFr { 'companionRemote.remote.tabMore' => 'Plus', 'companionRemote.remote.menu' => 'Menu', 'companionRemote.remote.tabNavigation' => 'Navigation par onglets', - 'companionRemote.remote.tabDiscover' => 'Découvrir', - 'companionRemote.remote.tabLibraries' => 'Bibliothèques', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Découvrir', + 'companionRemote.remote.tabLibraries' => 'Bibliothèques', 'companionRemote.remote.tabSearch' => 'Rechercher', 'companionRemote.remote.tabDownloads' => 'Téléchargements', 'companionRemote.remote.tabSettings' => 'Paramètres', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index aec8fb7d..19a43c03 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsIt extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Mostra il poster di ogni stagione sopra la sua scheda'; @override String get tvFullCardLayout => 'Schede TV piene'; @override String get tvFullCardLayoutDescription => 'Usa schede TV solo immagine con i nomi degli attori sovrapposti'; + @override String get focusGlow => 'Bagliore di selezione'; + @override String get focusGlowDescription => 'Mostra un leggero bagliore attorno alla scheda selezionata'; @override String get hideSpoilers => 'Nascondi spoiler per episodi non visti'; @override String get hideSpoilersDescription => 'Sfoca miniature e descrizioni degli episodi non visti'; @override String get playerBackend => 'Motore di riproduzione'; @@ -2039,6 +2041,8 @@ extension on TranslationsIt { 'settings.showSeasonPostersOnTabsDescription' => 'Mostra il poster di ogni stagione sopra la sua scheda', 'settings.tvFullCardLayout' => 'Schede TV piene', 'settings.tvFullCardLayoutDescription' => 'Usa schede TV solo immagine con i nomi degli attori sovrapposti', + 'settings.focusGlow' => 'Bagliore di selezione', + 'settings.focusGlowDescription' => 'Mostra un leggero bagliore attorno alla scheda selezionata', 'settings.hideSpoilers' => 'Nascondi spoiler per episodi non visti', 'settings.hideSpoilersDescription' => 'Sfoca miniature e descrizioni degli episodi non visti', 'settings.playerBackend' => 'Motore di riproduzione', @@ -2425,10 +2429,10 @@ extension on TranslationsIt { 'subtitlingStyling.text' => 'Testo', 'subtitlingStyling.border' => 'Bordo', 'subtitlingStyling.background' => 'Sfondo', - 'subtitlingStyling.fontSize' => 'Dimensione', - 'subtitlingStyling.textColor' => 'Colore testo', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Dimensione', + 'subtitlingStyling.textColor' => 'Colore testo', 'subtitlingStyling.borderSize' => 'Dimensione bordo', 'subtitlingStyling.borderColor' => 'Colore bordo', 'subtitlingStyling.backgroundOpacity' => 'Opacità sfondo', @@ -2939,10 +2943,10 @@ extension on TranslationsIt { 'companionRemote.remote.tabMore' => 'Altro', 'companionRemote.remote.menu' => 'Menu', 'companionRemote.remote.tabNavigation' => 'Navigazione schede', - 'companionRemote.remote.tabDiscover' => 'Esplora', - 'companionRemote.remote.tabLibraries' => 'Librerie', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Esplora', + 'companionRemote.remote.tabLibraries' => 'Librerie', 'companionRemote.remote.tabSearch' => 'Cerca', 'companionRemote.remote.tabDownloads' => 'Download', 'companionRemote.remote.tabSettings' => 'Impostazioni', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index df2ea151..54b92d2f 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsJa extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => '各シーズンのポスターをタブの上に表示します'; @override String get tvFullCardLayout => 'フルTVカード'; @override String get tvFullCardLayoutDescription => 'TVカードを画像のみで表示し、俳優名を重ねて表示します'; + @override String get focusGlow => 'フォーカス時の光彩'; + @override String get focusGlowDescription => 'フォーカス中のカードの周りに柔らかい光彩を表示します'; @override String get hideSpoilers => '未視聴エピソードのネタバレを非表示'; @override String get hideSpoilersDescription => '未視聴エピソードのサムネイルと説明をぼかします'; @override String get playerBackend => 'プレーヤーバックエンド'; @@ -2039,6 +2041,8 @@ extension on TranslationsJa { 'settings.showSeasonPostersOnTabsDescription' => '各シーズンのポスターをタブの上に表示します', 'settings.tvFullCardLayout' => 'フルTVカード', 'settings.tvFullCardLayoutDescription' => 'TVカードを画像のみで表示し、俳優名を重ねて表示します', + 'settings.focusGlow' => 'フォーカス時の光彩', + 'settings.focusGlowDescription' => 'フォーカス中のカードの周りに柔らかい光彩を表示します', 'settings.hideSpoilers' => '未視聴エピソードのネタバレを非表示', 'settings.hideSpoilersDescription' => '未視聴エピソードのサムネイルと説明をぼかします', 'settings.playerBackend' => 'プレーヤーバックエンド', @@ -2425,10 +2429,10 @@ extension on TranslationsJa { 'subtitlingStyling.text' => 'テキスト', 'subtitlingStyling.border' => '枠線', 'subtitlingStyling.background' => '背景', - 'subtitlingStyling.fontSize' => 'フォントサイズ', - 'subtitlingStyling.textColor' => 'テキストの色', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'フォントサイズ', + 'subtitlingStyling.textColor' => 'テキストの色', 'subtitlingStyling.borderSize' => '枠線サイズ', 'subtitlingStyling.borderColor' => '枠線の色', 'subtitlingStyling.backgroundOpacity' => '背景の不透明度', @@ -2939,10 +2943,10 @@ extension on TranslationsJa { 'companionRemote.remote.tabMore' => 'その他', 'companionRemote.remote.menu' => 'メニュー', 'companionRemote.remote.tabNavigation' => 'タブナビゲーション', - 'companionRemote.remote.tabDiscover' => '探す', - 'companionRemote.remote.tabLibraries' => 'ライブラリ', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => '探す', + 'companionRemote.remote.tabLibraries' => 'ライブラリ', 'companionRemote.remote.tabSearch' => '検索', 'companionRemote.remote.tabDownloads' => 'ダウンロード', 'companionRemote.remote.tabSettings' => '設定', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index bc92f250..0ef9c2fc 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsKo extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => '각 시즌 포스터를 탭 위에 표시'; @override String get tvFullCardLayout => '전체 TV 카드'; @override String get tvFullCardLayoutDescription => 'TV 카드에 이미지만 표시하고 배우 이름을 오버레이로 표시'; + @override String get focusGlow => '포커스 글로우'; + @override String get focusGlowDescription => '포커스된 카드 주위에 은은한 빛 효과를 표시'; @override String get hideSpoilers => '미시청 에피소드 스포일러 숨기기'; @override String get hideSpoilersDescription => '시청하지 않은 에피소드의 썸네일과 설명을 흐리게 처리'; @override String get playerBackend => '플레이어 백엔드'; @@ -2039,6 +2041,8 @@ extension on TranslationsKo { 'settings.showSeasonPostersOnTabsDescription' => '각 시즌 포스터를 탭 위에 표시', 'settings.tvFullCardLayout' => '전체 TV 카드', 'settings.tvFullCardLayoutDescription' => 'TV 카드에 이미지만 표시하고 배우 이름을 오버레이로 표시', + 'settings.focusGlow' => '포커스 글로우', + 'settings.focusGlowDescription' => '포커스된 카드 주위에 은은한 빛 효과를 표시', 'settings.hideSpoilers' => '미시청 에피소드 스포일러 숨기기', 'settings.hideSpoilersDescription' => '시청하지 않은 에피소드의 썸네일과 설명을 흐리게 처리', 'settings.playerBackend' => '플레이어 백엔드', @@ -2425,10 +2429,10 @@ extension on TranslationsKo { 'subtitlingStyling.text' => '텍스트', 'subtitlingStyling.border' => '테두리', 'subtitlingStyling.background' => '배경', - 'subtitlingStyling.fontSize' => '글자 크기', - 'subtitlingStyling.textColor' => '텍스트 색상', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => '글자 크기', + 'subtitlingStyling.textColor' => '텍스트 색상', 'subtitlingStyling.borderSize' => '테두리 크기', 'subtitlingStyling.borderColor' => '테두리 색상', 'subtitlingStyling.backgroundOpacity' => '배경 불투명도', @@ -2939,10 +2943,10 @@ extension on TranslationsKo { 'companionRemote.remote.tabMore' => '더 보기', 'companionRemote.remote.menu' => '메뉴', 'companionRemote.remote.tabNavigation' => '탭 탐색', - 'companionRemote.remote.tabDiscover' => '발견', - 'companionRemote.remote.tabLibraries' => '미디어 라이브러리', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => '발견', + 'companionRemote.remote.tabLibraries' => '미디어 라이브러리', 'companionRemote.remote.tabSearch' => '검색', 'companionRemote.remote.tabDownloads' => '다운로드', 'companionRemote.remote.tabSettings' => '설정', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index 1ffba3b5..2174b9fd 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsNb extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Vis hver sesongs plakat over fanen'; @override String get tvFullCardLayout => 'Fulle TV-kort'; @override String get tvFullCardLayoutDescription => 'Bruk bildebaserte TV-kort med skuespillernavn lagt over'; + @override String get focusGlow => 'Fokusglød'; + @override String get focusGlowDescription => 'Vis en myk glød rundt kortet i fokus'; @override String get hideSpoilers => 'Skjul spoilere for usette episoder'; @override String get hideSpoilersDescription => 'Slør miniatyrbilder og beskrivelser for usette episoder'; @override String get playerBackend => 'Spillermotor'; @@ -2039,6 +2041,8 @@ extension on TranslationsNb { 'settings.showSeasonPostersOnTabsDescription' => 'Vis hver sesongs plakat over fanen', 'settings.tvFullCardLayout' => 'Fulle TV-kort', 'settings.tvFullCardLayoutDescription' => 'Bruk bildebaserte TV-kort med skuespillernavn lagt over', + 'settings.focusGlow' => 'Fokusglød', + 'settings.focusGlowDescription' => 'Vis en myk glød rundt kortet i fokus', 'settings.hideSpoilers' => 'Skjul spoilere for usette episoder', 'settings.hideSpoilersDescription' => 'Slør miniatyrbilder og beskrivelser for usette episoder', 'settings.playerBackend' => 'Spillermotor', @@ -2425,10 +2429,10 @@ extension on TranslationsNb { 'subtitlingStyling.text' => 'Tekst', 'subtitlingStyling.border' => 'Kantlinje', 'subtitlingStyling.background' => 'Bakgrunn', - 'subtitlingStyling.fontSize' => 'Skriftstørrelse', - 'subtitlingStyling.textColor' => 'Tekstfarge', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Skriftstørrelse', + 'subtitlingStyling.textColor' => 'Tekstfarge', 'subtitlingStyling.borderSize' => 'Kantstørrelse', 'subtitlingStyling.borderColor' => 'Kantfarge', 'subtitlingStyling.backgroundOpacity' => 'Bakgrunnsopasitet', @@ -2939,10 +2943,10 @@ extension on TranslationsNb { 'companionRemote.remote.tabMore' => 'Mer', 'companionRemote.remote.menu' => 'Meny', 'companionRemote.remote.tabNavigation' => 'Fanenavigering', - 'companionRemote.remote.tabDiscover' => 'Oppdag', - 'companionRemote.remote.tabLibraries' => 'Biblioteker', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Oppdag', + 'companionRemote.remote.tabLibraries' => 'Biblioteker', 'companionRemote.remote.tabSearch' => 'Søk', 'companionRemote.remote.tabDownloads' => 'Nedlastinger', 'companionRemote.remote.tabSettings' => 'Innstillinger', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 6cc2c336..e978c392 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsNl extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Toon de poster van elk seizoen boven het tabblad'; @override String get tvFullCardLayout => 'Volledige tv-kaarten'; @override String get tvFullCardLayoutDescription => 'Gebruik tv-kaarten met alleen afbeeldingen en namen van acteurs als overlay'; + @override String get focusGlow => 'Focusgloed'; + @override String get focusGlowDescription => 'Toon een zachte gloed rond de kaart met focus'; @override String get hideSpoilers => 'Spoilers voor ongekeken afleveringen verbergen'; @override String get hideSpoilersDescription => 'Vervaag miniaturen en beschrijvingen voor niet-bekeken afleveringen'; @override String get playerBackend => 'Speler backend'; @@ -2039,6 +2041,8 @@ extension on TranslationsNl { 'settings.showSeasonPostersOnTabsDescription' => 'Toon de poster van elk seizoen boven het tabblad', 'settings.tvFullCardLayout' => 'Volledige tv-kaarten', 'settings.tvFullCardLayoutDescription' => 'Gebruik tv-kaarten met alleen afbeeldingen en namen van acteurs als overlay', + 'settings.focusGlow' => 'Focusgloed', + 'settings.focusGlowDescription' => 'Toon een zachte gloed rond de kaart met focus', 'settings.hideSpoilers' => 'Spoilers voor ongekeken afleveringen verbergen', 'settings.hideSpoilersDescription' => 'Vervaag miniaturen en beschrijvingen voor niet-bekeken afleveringen', 'settings.playerBackend' => 'Speler backend', @@ -2425,10 +2429,10 @@ extension on TranslationsNl { 'subtitlingStyling.text' => 'Tekst', 'subtitlingStyling.border' => 'Rand', 'subtitlingStyling.background' => 'Achtergrond', - 'subtitlingStyling.fontSize' => 'Lettergrootte', - 'subtitlingStyling.textColor' => 'Tekstkleur', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Lettergrootte', + 'subtitlingStyling.textColor' => 'Tekstkleur', 'subtitlingStyling.borderSize' => 'Rand grootte', 'subtitlingStyling.borderColor' => 'Randkleur', 'subtitlingStyling.backgroundOpacity' => 'Achtergrond transparantie', @@ -2939,10 +2943,10 @@ extension on TranslationsNl { 'companionRemote.remote.tabMore' => 'Meer', 'companionRemote.remote.menu' => 'Menu', 'companionRemote.remote.tabNavigation' => 'Tabnavigatie', - 'companionRemote.remote.tabDiscover' => 'Ontdekken', - 'companionRemote.remote.tabLibraries' => 'Bibliotheken', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Ontdekken', + 'companionRemote.remote.tabLibraries' => 'Bibliotheken', 'companionRemote.remote.tabSearch' => 'Zoeken', 'companionRemote.remote.tabDownloads' => 'Downloads', 'companionRemote.remote.tabSettings' => 'Instellingen', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index d3b3723d..f71be480 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsPl extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Pokazuj plakat każdego sezonu nad jego kartą'; @override String get tvFullCardLayout => 'Pełne karty TV'; @override String get tvFullCardLayoutDescription => 'Używaj kart TV tylko z obrazem i nałożonymi nazwiskami aktorów'; + @override String get focusGlow => 'Poświata zaznaczenia'; + @override String get focusGlowDescription => 'Wyświetlaj delikatną poświatę wokół zaznaczonej karty'; @override String get hideSpoilers => 'Ukryj spoilery nieobejrzanych odcinków'; @override String get hideSpoilersDescription => 'Rozmywaj miniatury i opisy nieobejrzanych odcinków'; @override String get playerBackend => 'Backend odtwarzacza'; @@ -2039,6 +2041,8 @@ extension on TranslationsPl { 'settings.showSeasonPostersOnTabsDescription' => 'Pokazuj plakat każdego sezonu nad jego kartą', 'settings.tvFullCardLayout' => 'Pełne karty TV', 'settings.tvFullCardLayoutDescription' => 'Używaj kart TV tylko z obrazem i nałożonymi nazwiskami aktorów', + 'settings.focusGlow' => 'Poświata zaznaczenia', + 'settings.focusGlowDescription' => 'Wyświetlaj delikatną poświatę wokół zaznaczonej karty', 'settings.hideSpoilers' => 'Ukryj spoilery nieobejrzanych odcinków', 'settings.hideSpoilersDescription' => 'Rozmywaj miniatury i opisy nieobejrzanych odcinków', 'settings.playerBackend' => 'Backend odtwarzacza', @@ -2425,10 +2429,10 @@ extension on TranslationsPl { 'subtitlingStyling.text' => 'Tekst', 'subtitlingStyling.border' => 'Obramowanie', 'subtitlingStyling.background' => 'Tło', - 'subtitlingStyling.fontSize' => 'Rozmiar czcionki', - 'subtitlingStyling.textColor' => 'Kolor tekstu', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Rozmiar czcionki', + 'subtitlingStyling.textColor' => 'Kolor tekstu', 'subtitlingStyling.borderSize' => 'Rozmiar obramowania', 'subtitlingStyling.borderColor' => 'Kolor obramowania', 'subtitlingStyling.backgroundOpacity' => 'Przezroczystość tła', @@ -2939,10 +2943,10 @@ extension on TranslationsPl { 'companionRemote.remote.tabMore' => 'Więcej', 'companionRemote.remote.menu' => 'Menu', 'companionRemote.remote.tabNavigation' => 'Nawigacja', - 'companionRemote.remote.tabDiscover' => 'Odkryj', - 'companionRemote.remote.tabLibraries' => 'Biblioteki', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Odkryj', + 'companionRemote.remote.tabLibraries' => 'Biblioteki', 'companionRemote.remote.tabSearch' => 'Szukaj', 'companionRemote.remote.tabDownloads' => 'Pobrania', 'companionRemote.remote.tabSettings' => 'Ustawienia', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index 513a6f6f..72221645 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsPt extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Mostrar o pôster de cada temporada acima da aba'; @override String get tvFullCardLayout => 'Cartões TV completos'; @override String get tvFullCardLayoutDescription => 'Usar cartões de TV só com imagem e nomes dos atores sobrepostos'; + @override String get focusGlow => 'Brilho de foco'; + @override String get focusGlowDescription => 'Mostrar um brilho suave à volta do cartão em foco'; @override String get hideSpoilers => 'Ocultar Spoilers de Episódios Não Assistidos'; @override String get hideSpoilersDescription => 'Desfocar miniaturas e descrições de episódios não vistos'; @override String get playerBackend => 'Backend do Player'; @@ -2039,6 +2041,8 @@ extension on TranslationsPt { 'settings.showSeasonPostersOnTabsDescription' => 'Mostrar o pôster de cada temporada acima da aba', 'settings.tvFullCardLayout' => 'Cartões TV completos', 'settings.tvFullCardLayoutDescription' => 'Usar cartões de TV só com imagem e nomes dos atores sobrepostos', + 'settings.focusGlow' => 'Brilho de foco', + 'settings.focusGlowDescription' => 'Mostrar um brilho suave à volta do cartão em foco', 'settings.hideSpoilers' => 'Ocultar Spoilers de Episódios Não Assistidos', 'settings.hideSpoilersDescription' => 'Desfocar miniaturas e descrições de episódios não vistos', 'settings.playerBackend' => 'Backend do Player', @@ -2425,10 +2429,10 @@ extension on TranslationsPt { 'subtitlingStyling.text' => 'Texto', 'subtitlingStyling.border' => 'Borda', 'subtitlingStyling.background' => 'Fundo', - 'subtitlingStyling.fontSize' => 'Tamanho da Fonte', - 'subtitlingStyling.textColor' => 'Cor do Texto', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Tamanho da Fonte', + 'subtitlingStyling.textColor' => 'Cor do Texto', 'subtitlingStyling.borderSize' => 'Tamanho da Borda', 'subtitlingStyling.borderColor' => 'Cor da Borda', 'subtitlingStyling.backgroundOpacity' => 'Opacidade do Fundo', @@ -2939,10 +2943,10 @@ extension on TranslationsPt { 'companionRemote.remote.tabMore' => 'Mais', 'companionRemote.remote.menu' => 'Menu', 'companionRemote.remote.tabNavigation' => 'Navegação', - 'companionRemote.remote.tabDiscover' => 'Descobrir', - 'companionRemote.remote.tabLibraries' => 'Bibliotecas', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Descobrir', + 'companionRemote.remote.tabLibraries' => 'Bibliotecas', 'companionRemote.remote.tabSearch' => 'Buscar', 'companionRemote.remote.tabDownloads' => 'Downloads', 'companionRemote.remote.tabSettings' => 'Configurações', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index 8d3bc89b..a07c0d3b 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsRu extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Показывать постер каждого сезона над его вкладкой'; @override String get tvFullCardLayout => 'Полные TV-карточки'; @override String get tvFullCardLayoutDescription => 'Использовать TV-карточки только с изображением и именами актёров поверх него'; + @override String get focusGlow => 'Свечение при фокусе'; + @override String get focusGlowDescription => 'Показывать мягкое свечение вокруг карточки в фокусе'; @override String get hideSpoilers => 'Скрыть спойлеры непросмотренных эпизодов'; @override String get hideSpoilersDescription => 'Размывать миниатюры и описания непросмотренных серий'; @override String get playerBackend => 'Бэкенд плеера'; @@ -2039,6 +2041,8 @@ extension on TranslationsRu { 'settings.showSeasonPostersOnTabsDescription' => 'Показывать постер каждого сезона над его вкладкой', 'settings.tvFullCardLayout' => 'Полные TV-карточки', 'settings.tvFullCardLayoutDescription' => 'Использовать TV-карточки только с изображением и именами актёров поверх него', + 'settings.focusGlow' => 'Свечение при фокусе', + 'settings.focusGlowDescription' => 'Показывать мягкое свечение вокруг карточки в фокусе', 'settings.hideSpoilers' => 'Скрыть спойлеры непросмотренных эпизодов', 'settings.hideSpoilersDescription' => 'Размывать миниатюры и описания непросмотренных серий', 'settings.playerBackend' => 'Бэкенд плеера', @@ -2425,10 +2429,10 @@ extension on TranslationsRu { 'subtitlingStyling.text' => 'Текст', 'subtitlingStyling.border' => 'Обводка', 'subtitlingStyling.background' => 'Фон', - 'subtitlingStyling.fontSize' => 'Размер шрифта', - 'subtitlingStyling.textColor' => 'Цвет текста', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Размер шрифта', + 'subtitlingStyling.textColor' => 'Цвет текста', 'subtitlingStyling.borderSize' => 'Размер обводки', 'subtitlingStyling.borderColor' => 'Цвет обводки', 'subtitlingStyling.backgroundOpacity' => 'Прозрачность фона', @@ -2939,10 +2943,10 @@ extension on TranslationsRu { 'companionRemote.remote.tabMore' => 'Ещё', 'companionRemote.remote.menu' => 'Меню', 'companionRemote.remote.tabNavigation' => 'Навигация', - 'companionRemote.remote.tabDiscover' => 'Обзор', - 'companionRemote.remote.tabLibraries' => 'Библиотеки', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Обзор', + 'companionRemote.remote.tabLibraries' => 'Библиотеки', 'companionRemote.remote.tabSearch' => 'Поиск', 'companionRemote.remote.tabDownloads' => 'Загрузки', 'companionRemote.remote.tabSettings' => 'Настройки', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 655b356e..2df2b1bf 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsSv extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => 'Visa varje säsongs affisch ovanför fliken'; @override String get tvFullCardLayout => 'Heltäckande TV-kort'; @override String get tvFullCardLayoutDescription => 'Använd TV-kort med enbart bild och skådespelarnamn ovanpå'; + @override String get focusGlow => 'Fokusglöd'; + @override String get focusGlowDescription => 'Visa en mjuk glöd runt kortet i fokus'; @override String get hideSpoilers => 'Dölj spoilers för osedda avsnitt'; @override String get hideSpoilersDescription => 'Sudda miniatyrbilder och beskrivningar för osedda avsnitt'; @override String get playerBackend => 'Spelarmotor'; @@ -2039,6 +2041,8 @@ extension on TranslationsSv { 'settings.showSeasonPostersOnTabsDescription' => 'Visa varje säsongs affisch ovanför fliken', 'settings.tvFullCardLayout' => 'Heltäckande TV-kort', 'settings.tvFullCardLayoutDescription' => 'Använd TV-kort med enbart bild och skådespelarnamn ovanpå', + 'settings.focusGlow' => 'Fokusglöd', + 'settings.focusGlowDescription' => 'Visa en mjuk glöd runt kortet i fokus', 'settings.hideSpoilers' => 'Dölj spoilers för osedda avsnitt', 'settings.hideSpoilersDescription' => 'Sudda miniatyrbilder och beskrivningar för osedda avsnitt', 'settings.playerBackend' => 'Spelarmotor', @@ -2425,10 +2429,10 @@ extension on TranslationsSv { 'subtitlingStyling.text' => 'Text', 'subtitlingStyling.border' => 'Kantlinje', 'subtitlingStyling.background' => 'Bakgrund', - 'subtitlingStyling.fontSize' => 'Teckenstorlek', - 'subtitlingStyling.textColor' => 'Textfärg', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => 'Teckenstorlek', + 'subtitlingStyling.textColor' => 'Textfärg', 'subtitlingStyling.borderSize' => 'Kantstorlek', 'subtitlingStyling.borderColor' => 'Kantfärg', 'subtitlingStyling.backgroundOpacity' => 'Bakgrundsopacitet', @@ -2939,10 +2943,10 @@ extension on TranslationsSv { 'companionRemote.remote.tabMore' => 'Mer', 'companionRemote.remote.menu' => 'Meny', 'companionRemote.remote.tabNavigation' => 'Fliknavigering', - 'companionRemote.remote.tabDiscover' => 'Upptäck', - 'companionRemote.remote.tabLibraries' => 'Bibliotek', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => 'Upptäck', + 'companionRemote.remote.tabLibraries' => 'Bibliotek', 'companionRemote.remote.tabSearch' => 'Sök', 'companionRemote.remote.tabDownloads' => 'Nedladdningar', 'companionRemote.remote.tabSettings' => 'Inställningar', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 26b1575e..c56c64e5 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -263,6 +263,8 @@ class _TranslationsSettingsZh extends TranslationsSettingsEn { @override String get showSeasonPostersOnTabsDescription => '在每季标签上方显示该季海报'; @override String get tvFullCardLayout => '完整 TV 卡片'; @override String get tvFullCardLayoutDescription => '使用仅显示图片的 TV 卡片,并叠加演员姓名'; + @override String get focusGlow => '焦点光晕'; + @override String get focusGlowDescription => '在获得焦点的卡片周围显示柔和的光晕'; @override String get hideSpoilers => '隐藏未看剧集的剧透内容'; @override String get hideSpoilersDescription => '模糊未观看剧集的缩略图和描述'; @override String get playerBackend => '播放器引擎'; @@ -2039,6 +2041,8 @@ extension on TranslationsZh { 'settings.showSeasonPostersOnTabsDescription' => '在每季标签上方显示该季海报', 'settings.tvFullCardLayout' => '完整 TV 卡片', 'settings.tvFullCardLayoutDescription' => '使用仅显示图片的 TV 卡片,并叠加演员姓名', + 'settings.focusGlow' => '焦点光晕', + 'settings.focusGlowDescription' => '在获得焦点的卡片周围显示柔和的光晕', 'settings.hideSpoilers' => '隐藏未看剧集的剧透内容', 'settings.hideSpoilersDescription' => '模糊未观看剧集的缩略图和描述', 'settings.playerBackend' => '播放器引擎', @@ -2425,10 +2429,10 @@ extension on TranslationsZh { 'subtitlingStyling.text' => '文本', 'subtitlingStyling.border' => '边框', 'subtitlingStyling.background' => '背景', - 'subtitlingStyling.fontSize' => '字号', - 'subtitlingStyling.textColor' => '文本颜色', _ => null, } ?? switch (path) { + 'subtitlingStyling.fontSize' => '字号', + 'subtitlingStyling.textColor' => '文本颜色', 'subtitlingStyling.borderSize' => '边框大小', 'subtitlingStyling.borderColor' => '边框颜色', 'subtitlingStyling.backgroundOpacity' => '背景不透明度', @@ -2939,10 +2943,10 @@ extension on TranslationsZh { 'companionRemote.remote.tabMore' => '更多', 'companionRemote.remote.menu' => '菜单', 'companionRemote.remote.tabNavigation' => '标签导航', - 'companionRemote.remote.tabDiscover' => '发现', - 'companionRemote.remote.tabLibraries' => '媒体库', _ => null, } ?? switch (path) { + 'companionRemote.remote.tabDiscover' => '发现', + 'companionRemote.remote.tabLibraries' => '媒体库', 'companionRemote.remote.tabSearch' => '搜索', 'companionRemote.remote.tabDownloads' => '下载', 'companionRemote.remote.tabSettings' => '设置', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 82d53e68..60b8ab10 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "Visa varje säsongs affisch ovanför fliken", "tvFullCardLayout": "Heltäckande TV-kort", "tvFullCardLayoutDescription": "Använd TV-kort med enbart bild och skådespelarnamn ovanpå", + "focusGlow": "Fokusglöd", + "focusGlowDescription": "Visa en mjuk glöd runt kortet i fokus", "hideSpoilers": "Dölj spoilers för osedda avsnitt", "hideSpoilersDescription": "Sudda miniatyrbilder och beskrivningar för osedda avsnitt", "playerBackend": "Spelarmotor", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index e70ba6d7..07449425 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -134,6 +134,8 @@ "showSeasonPostersOnTabsDescription": "在每季标签上方显示该季海报", "tvFullCardLayout": "完整 TV 卡片", "tvFullCardLayoutDescription": "使用仅显示图片的 TV 卡片,并叠加演员姓名", + "focusGlow": "焦点光晕", + "focusGlowDescription": "在获得焦点的卡片周围显示柔和的光晕", "hideSpoilers": "隐藏未看剧集的剧透内容", "hideSpoilersDescription": "模糊未观看剧集的缩略图和描述", "playerBackend": "播放器引擎", diff --git a/lib/screens/media_detail_screen.dart b/lib/screens/media_detail_screen.dart index aeb9d061..11f83a07 100644 --- a/lib/screens/media_detail_screen.dart +++ b/lib/screens/media_detail_screen.dart @@ -21,6 +21,7 @@ import '../focus/focusable_action_bar.dart'; import '../focus/focusable_wrapper.dart'; import '../focus/key_event_utils.dart'; import '../focus/input_mode_tracker.dart'; +import '../focus/card_focus_scope.dart'; import '../widgets/focus_builders.dart'; import '../media/media_hub.dart'; import '../utils/provider_extensions.dart'; @@ -4177,6 +4178,7 @@ class _MediaDetailScreenState extends State isFocused: isFocused, borderRadius: tokens(context).radiusSm, onTap: () => _navigateToActorMedia(actor), + delegateFocusBorder: true, child: Padding( padding: const EdgeInsets.all(innerPadding), child: SizedBox( @@ -4184,16 +4186,19 @@ class _MediaDetailScreenState extends State child: Column( crossAxisAlignment: .start, children: [ - ClipRRect( - borderRadius: BorderRadius.circular(tokens(context).radiusSm), - child: OptimizedMediaImage( - client: getServerBoundMediaClient(context), - imagePath: actor.thumbPath, - width: imageSize, - height: imageSize, - fit: BoxFit.cover, - imageType: ImageType.avatar, - fallbackIcon: Symbols.person_rounded, + CardFocusBorder( + borderRadius: tokens(context).radiusSm, + child: ClipRRect( + borderRadius: BorderRadius.circular(tokens(context).radiusSm), + child: OptimizedMediaImage( + client: getServerBoundMediaClient(context), + imagePath: actor.thumbPath, + width: imageSize, + height: imageSize, + fit: BoxFit.cover, + imageType: ImageType.avatar, + fallbackIcon: Symbols.person_rounded, + ), ), ), const SizedBox(height: 8), @@ -4266,6 +4271,7 @@ class _MediaDetailScreenState extends State context: context, isFocused: isFocused, onTap: () => navigateToVideoPlayer(context, metadata: extra), + delegateFocusBorder: true, child: MediaCard( key: cardKey, item: extra, diff --git a/lib/screens/settings/add_jellyfin_screen.dart b/lib/screens/settings/add_jellyfin_screen.dart index 237a90da..1a58d76c 100644 --- a/lib/screens/settings/add_jellyfin_screen.dart +++ b/lib/screens/settings/add_jellyfin_screen.dart @@ -9,6 +9,7 @@ import 'package:uuid/uuid.dart'; import '../../connection/connection.dart'; import '../../exceptions/media_server_exceptions.dart'; +import '../../focus/card_focus_scope.dart'; import '../../focus/focusable_button.dart'; import '../../focus/focusable_text_field.dart'; import '../../focus/focusable_wrapper.dart'; @@ -759,45 +760,49 @@ class _DiscoveredJellyfinServerTile extends StatelessWidget { return FocusableWrapper( focusNode: focusNode, disableScale: true, - borderRadius: 12, - useForegroundFocusDecoration: true, + // Border drawn by CardFocusBorder so it paints over the opaque Material. + delegateFocusBorder: true, descendantsAreFocusable: false, onSelect: onTap, onNavigateUp: onNavigateUp, onNavigateDown: onNavigateDown, - child: Material( - color: theme.colorScheme.surfaceContainerHighest, - borderRadius: BorderRadius.circular(12), - child: InkWell( - onTap: onTap, + child: CardFocusBorder( + borderRadius: 12, + strokeAlign: BorderSide.strokeAlignInside, + child: Material( + color: theme.colorScheme.surfaceContainerHighest, borderRadius: BorderRadius.circular(12), - child: Padding( - padding: const EdgeInsets.all(12), - child: Row( - children: [ - const AppIcon(Symbols.dns_rounded, fill: 1), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: .start, - mainAxisSize: .min, - children: [ - Text(server.name, style: theme.textTheme.titleSmall), - const SizedBox(height: 2), - Text( - server.address, - maxLines: 1, - overflow: .ellipsis, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + const AppIcon(Symbols.dns_rounded, fill: 1), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: .start, + mainAxisSize: .min, + children: [ + Text(server.name, style: theme.textTheme.titleSmall), + const SizedBox(height: 2), + Text( + server.address, + maxLines: 1, + overflow: .ellipsis, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurface.withValues(alpha: 0.7), + ), ), - ), - ], + ], + ), ), - ), - const SizedBox(width: 12), - const AppIcon(Symbols.chevron_right_rounded, fill: 1), - ], + const SizedBox(width: 12), + const AppIcon(Symbols.chevron_right_rounded, fill: 1), + ], + ), ), ), ), diff --git a/lib/screens/settings/appearance_settings_screen.dart b/lib/screens/settings/appearance_settings_screen.dart index 16076b38..62c435af 100644 --- a/lib/screens/settings/appearance_settings_screen.dart +++ b/lib/screens/settings/appearance_settings_screen.dart @@ -43,6 +43,13 @@ class AppearanceSettingsScreen extends StatelessWidget { title: t.settings.tvFullCardLayout, subtitle: t.settings.tvFullCardLayoutDescription, ), + if (PlatformDetector.isTV()) + SettingSwitchTile( + pref: SettingsService.focusGlow, + icon: Symbols.lightbulb_rounded, + title: t.settings.focusGlow, + subtitle: t.settings.focusGlowDescription, + ), if (Platform.isAndroid) _visualEffectsSelector(context), SettingSwitchTile( pref: SettingsService.showEpisodeNumberOnCards, diff --git a/lib/services/settings_service.dart b/lib/services/settings_service.dart index 2721995e..6edde3db 100644 --- a/lib/services/settings_service.dart +++ b/lib/services/settings_service.dart @@ -290,6 +290,7 @@ class SettingsService extends BaseSharedPreferencesService { static const rewindOnResume = IntPref('rewind_on_resume'); static const showHeroSection = BoolPref('show_hero_section', defaultValue: true); static const tvFullCardLayout = BoolPref('tv_full_card_layout', defaultValue: false); + static const focusGlow = BoolPref('focus_glow', defaultValue: true); static const useGlobalHubs = BoolPref('use_global_hubs', defaultValue: true); static const showServerNameOnHubs = BoolPref('show_server_name_on_hubs'); static const groupLibrariesByServer = BoolPref('group_libraries_by_server', defaultValue: true); diff --git a/lib/widgets/focus_builders.dart b/lib/widgets/focus_builders.dart index 58d81dea..3035a1c3 100644 --- a/lib/widgets/focus_builders.dart +++ b/lib/widgets/focus_builders.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../focus/card_focus_scope.dart'; import '../focus/focus_glow_overlay.dart'; import '../focus/focus_theme.dart'; import '../focus/input_mode_tracker.dart'; @@ -75,9 +76,8 @@ class FocusBuilders { VoidCallback? onLongPress, double borderRadius = FocusTheme.defaultBorderRadius, double focusScale = FocusTheme.focusScale, - double focusBorderStrokeAlign = BorderSide.strokeAlignInside, bool useFocusGlow = false, - bool useForegroundFocusDecoration = false, + bool delegateFocusBorder = false, Size? glowSize, required Widget child, }) { @@ -99,21 +99,16 @@ class FocusBuilders { final duration = FocusTheme.getAnimationDuration(context); final showFocus = isFocused && isKeyboardMode; - final focusDecoration = FocusTheme.focusDecoration( - context, - isFocused: showFocus, - borderRadius: borderRadius, - borderStrokeAlign: focusBorderStrokeAlign, - ); // Glow (full-bleed cards) renders in an overlay above siblings so it stays // symmetric; the in-card decoration only carries the border. - Widget card = AnimatedContainer( - duration: duration, - curve: Curves.easeOutCubic, - decoration: useForegroundFocusDecoration ? null : focusDecoration, - foregroundDecoration: useForegroundFocusDecoration ? focusDecoration : null, - child: child, - ); + Widget card = delegateFocusBorder + ? CardFocusScope(showFocus: showFocus, child: child) + : AnimatedContainer( + duration: duration, + curve: Curves.easeOutCubic, + decoration: FocusTheme.focusDecoration(context, isFocused: showFocus, borderRadius: borderRadius), + child: child, + ); if (useFocusGlow) { card = FocusGlowOverlay( isFocused: showFocus, @@ -164,9 +159,8 @@ class FocusBuilders { VoidCallback? onLongPress, double borderRadius = FocusTheme.defaultBorderRadius, double focusScale = FocusTheme.focusScale, - double focusBorderStrokeAlign = BorderSide.strokeAlignInside, bool useFocusGlow = false, - bool useForegroundFocusDecoration = false, + bool delegateFocusBorder = false, Size? glowSize, required Widget child, }) { @@ -179,9 +173,8 @@ class FocusBuilders { onLongPress: onLongPress, borderRadius: borderRadius, focusScale: focusScale, - focusBorderStrokeAlign: focusBorderStrokeAlign, useFocusGlow: useFocusGlow, - useForegroundFocusDecoration: useForegroundFocusDecoration, + delegateFocusBorder: delegateFocusBorder, glowSize: glowSize, child: child, ); diff --git a/lib/widgets/focusable_media_card.dart b/lib/widgets/focusable_media_card.dart index 38d40a7c..88051dce 100644 --- a/lib/widgets/focusable_media_card.dart +++ b/lib/widgets/focusable_media_card.dart @@ -117,9 +117,10 @@ class _FocusableMediaCardState extends State { enableLongPress: true, disableScale: widget.disableScale, focusScale: widget.fullBleedImage ? FocusTheme.fullCardFocusScale : FocusTheme.focusScale, - focusBorderStrokeAlign: widget.fullBleedImage ? BorderSide.strokeAlignOutside : BorderSide.strokeAlignInside, useFocusGlow: widget.fullBleedImage, - useForegroundFocusDecoration: widget.fullBleedImage, + // MediaCard draws the focus border itself, on the rect its layout + // highlights (poster for standard grid cards, whole card otherwise). + delegateFocusBorder: true, useComfortableZone: !PlatformDetector.isTV(), // Always center on TV scrollAlignment: 0.5, child: MediaCard( diff --git a/lib/widgets/hub_section.dart b/lib/widgets/hub_section.dart index 8decad38..3d8c1f1c 100644 --- a/lib/widgets/hub_section.dart +++ b/lib/widgets/hub_section.dart @@ -558,6 +558,7 @@ class HubSectionState extends State with MountedSetStateMixin { isFocused: isItemFocused, onTap: () => _onItemTapped(index), onLongPress: () => _mediaCardKeys[index]?.currentState?.showContextMenu(), + delegateFocusBorder: true, child: MediaCard( key: _getMediaCardKey(index), item: item, diff --git a/lib/widgets/media_card.dart b/lib/widgets/media_card.dart index 4738579c..b94e3f67 100644 --- a/lib/widgets/media_card.dart +++ b/lib/widgets/media_card.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:plezy/widgets/app_icon.dart'; import 'package:material_symbols_icons/symbols.dart'; import 'package:provider/provider.dart'; +import '../focus/card_focus_scope.dart'; import '../focus/input_mode_tracker.dart'; import '../media/media_item.dart'; import '../media/media_item_types.dart'; @@ -299,23 +300,26 @@ class MediaCardState extends State with ContextMenuTapMixin with ContextMenuTapMixin with ContextMenuTapMixin _navigateToFocusedDetail(context, item as MediaItem, isOffline: isOffline), - ) - else - Text( - _displayTitle(), - maxLines: 2, - overflow: .ellipsis, - style: TextStyle(fontWeight: .w600, fontSize: _titleFontSize, height: 1.2), - ), - const SizedBox(height: 4), - if (metadataLine.isNotEmpty) ...[ - Text( - metadataLine, - maxLines: 1, - overflow: .ellipsis, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: tokens(context).textMuted.withValues(alpha: 0.9), - fontSize: _metadataFontSize, - fontWeight: .w500, + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: .start, + mainAxisAlignment: .start, + children: [ + if (item is MediaItem && _hasClickableTitle(item as MediaItem)) + _ClickableText( + text: (item as MediaItem).displayTitle, + style: TextStyle(fontWeight: .w600, fontSize: _titleFontSize, height: 1.2), + onTap: () => _navigateToFocusedDetail(context, item as MediaItem, isOffline: isOffline), + ) + else + Text( + _displayTitle(), + maxLines: 2, + overflow: .ellipsis, + style: TextStyle(fontWeight: .w600, fontSize: _titleFontSize, height: 1.2), ), - ), - const SizedBox(height: 2), - ], - if (item is MediaItem && - (item as MediaItem).isEpisode && - (item as MediaItem).parentIndex != null && - (item as MediaItem).parentId != null) ...[ - _buildEpisodeSubtitle(context, item as MediaItem), const SizedBox(height: 4), - ] else if (subtitle != null) ...[ - Text( - subtitle, - maxLines: 1, - overflow: .ellipsis, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: tokens(context).textMuted.withValues(alpha: 0.85), - fontSize: _subtitleFontSize, - ), - ), - const SizedBox(height: 4), - ], - if (!(item is MediaItem && - SettingsService.instance.read(SettingsService.hideSpoilers) && - (item as MediaItem).shouldHideSpoiler) && - _summary() != null) ...[ - Text( - _summary()!, - maxLines: _summaryMaxLines, - overflow: .ellipsis, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: tokens(context).textMuted.withValues(alpha: 0.7), - fontSize: _summaryFontSize, - height: 1.3, - ), - ), - ], - if (showServerName && item is MediaItem && (item as MediaItem).serverName != null) ...[ - const SizedBox(height: 4), - Row( - children: [ - BackendBadge( - backend: (item as MediaItem).backend, - size: _metadataFontSize + 2, - color: tokens(context).textMuted.withValues(alpha: 0.6), + if (metadataLine.isNotEmpty) ...[ + Text( + metadataLine, + maxLines: 1, + overflow: .ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: tokens(context).textMuted.withValues(alpha: 0.9), + fontSize: _metadataFontSize, + fontWeight: .w500, ), - const SizedBox(width: 4), - Flexible( - child: Text( - (item as MediaItem).serverName!, - maxLines: 1, - overflow: .ellipsis, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: tokens(context).textMuted.withValues(alpha: 0.6), - fontSize: _metadataFontSize, + ), + const SizedBox(height: 2), + ], + if (item is MediaItem && + (item as MediaItem).isEpisode && + (item as MediaItem).parentIndex != null && + (item as MediaItem).parentId != null) ...[ + _buildEpisodeSubtitle(context, item as MediaItem), + const SizedBox(height: 4), + ] else if (subtitle != null) ...[ + Text( + subtitle, + maxLines: 1, + overflow: .ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: tokens(context).textMuted.withValues(alpha: 0.85), + fontSize: _subtitleFontSize, + ), + ), + const SizedBox(height: 4), + ], + if (!(item is MediaItem && + SettingsService.instance.read(SettingsService.hideSpoilers) && + (item as MediaItem).shouldHideSpoiler) && + _summary() != null) ...[ + Text( + _summary()!, + maxLines: _summaryMaxLines, + overflow: .ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: tokens(context).textMuted.withValues(alpha: 0.7), + fontSize: _summaryFontSize, + height: 1.3, + ), + ), + ], + if (showServerName && item is MediaItem && (item as MediaItem).serverName != null) ...[ + const SizedBox(height: 4), + Row( + children: [ + BackendBadge( + backend: (item as MediaItem).backend, + size: _metadataFontSize + 2, + color: tokens(context).textMuted.withValues(alpha: 0.6), + ), + const SizedBox(width: 4), + Flexible( + child: Text( + (item as MediaItem).serverName!, + maxLines: 1, + overflow: .ellipsis, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: tokens(context).textMuted.withValues(alpha: 0.6), + fontSize: _metadataFontSize, + ), ), ), - ), - ], - ), + ], + ), + ], ], - ], + ), ), - ), - ], + ], + ), ), ), ); diff --git a/lib/widgets/tv_browse_rail.dart b/lib/widgets/tv_browse_rail.dart index 5de086f3..fac76d23 100644 --- a/lib/widgets/tv_browse_rail.dart +++ b/lib/widgets/tv_browse_rail.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:material_symbols_icons/symbols.dart'; +import '../focus/card_focus_scope.dart'; import '../focus/dpad_navigator.dart'; import '../focus/focus_theme.dart'; import '../focus/key_event_utils.dart'; @@ -1275,11 +1276,10 @@ class TvBrowseRailState extends State { isFocused: isFocused, borderRadius: tokens(context).radiusSm, focusScale: fullCardLayout ? TvBrowseRailLayout.fullCardFocusScale : FocusTheme.focusScale, - focusBorderStrokeAlign: fullCardLayout - ? BorderSide.strokeAlignOutside - : BorderSide.strokeAlignInside, useFocusGlow: fullCardLayout, - useForegroundFocusDecoration: fullCardLayout, + // The card draws the border itself (poster rect for + // standard cards, whole card when full-bleed). + delegateFocusBorder: true, glowSize: fullCardLayout ? Size(metrics.cardWidth, metrics.posterHeight) : null, onTap: () { _selectHubItem(hub, hubIndex, itemIndex); @@ -1352,62 +1352,65 @@ class TvBrowseRailState extends State { return SizedBox( width: cardWidth, height: imageSize, - child: ClipRRect( - borderRadius: BorderRadius.circular(tokens(context).radiusSm), - child: Stack( - fit: StackFit.expand, - children: [ - OptimizedMediaImage( - client: context.tryGetMediaClientWithFallback(serverIdOrNull(item.serverId)), - imagePath: item.thumbPath, - width: cardWidth, - height: imageSize, - fit: BoxFit.cover, - imageType: ImageType.avatar, - fallbackIcon: Symbols.person_rounded, - ), - DecoratedBox( - decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Colors.transparent, Colors.black.withValues(alpha: 0.78)], - stops: const [0.45, 1.0], + child: CardFocusBorder( + borderRadius: tokens(context).radiusSm, + child: ClipRRect( + borderRadius: BorderRadius.circular(tokens(context).radiusSm), + child: Stack( + fit: StackFit.expand, + children: [ + OptimizedMediaImage( + client: context.tryGetMediaClientWithFallback(serverIdOrNull(item.serverId)), + imagePath: item.thumbPath, + width: cardWidth, + height: imageSize, + fit: BoxFit.cover, + imageType: ImageType.avatar, + fallbackIcon: Symbols.person_rounded, + ), + DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.transparent, Colors.black.withValues(alpha: 0.78)], + stops: const [0.45, 1.0], + ), ), ), - ), - Positioned( - left: 10 * scale, - right: 10 * scale, - bottom: 9 * scale, - child: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - Text( - item.displayTitle, - maxLines: 1, - overflow: .ellipsis, - style: TextStyle(color: Colors.white, fontSize: 13 * scale, height: 1.1, fontWeight: .w800), - ), - if (characterName != null && characterName.isNotEmpty) ...[ - SizedBox(height: 2 * scale), + Positioned( + left: 10 * scale, + right: 10 * scale, + bottom: 9 * scale, + child: Column( + mainAxisSize: .min, + crossAxisAlignment: .start, + children: [ Text( - characterName, + item.displayTitle, maxLines: 1, overflow: .ellipsis, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.82), - fontSize: 11 * scale, - height: 1.1, - fontWeight: .w600, - ), + style: TextStyle(color: Colors.white, fontSize: 13 * scale, height: 1.1, fontWeight: .w800), ), + if (characterName != null && characterName.isNotEmpty) ...[ + SizedBox(height: 2 * scale), + Text( + characterName, + maxLines: 1, + overflow: .ellipsis, + style: TextStyle( + color: Colors.white.withValues(alpha: 0.82), + fontSize: 11 * scale, + height: 1.1, + fontWeight: .w600, + ), + ), + ], ], - ], + ), ), - ), - ], + ], + ), ), ), ); @@ -1421,16 +1424,19 @@ class TvBrowseRailState extends State { mainAxisSize: .min, crossAxisAlignment: .start, children: [ - ClipRRect( - borderRadius: BorderRadius.circular(tokens(context).radiusSm), - child: OptimizedMediaImage( - client: context.tryGetMediaClientWithFallback(serverIdOrNull(item.serverId)), - imagePath: item.thumbPath, - width: imageSize, - height: imageSize, - fit: BoxFit.cover, - imageType: ImageType.avatar, - fallbackIcon: Symbols.person_rounded, + CardFocusBorder( + borderRadius: tokens(context).radiusSm, + child: ClipRRect( + borderRadius: BorderRadius.circular(tokens(context).radiusSm), + child: OptimizedMediaImage( + client: context.tryGetMediaClientWithFallback(serverIdOrNull(item.serverId)), + imagePath: item.thumbPath, + width: imageSize, + height: imageSize, + fit: BoxFit.cover, + imageType: ImageType.avatar, + fallbackIcon: Symbols.person_rounded, + ), ), ), SizedBox(height: 6 * scale), diff --git a/test/widgets/tv_browse_rail_test.dart b/test/widgets/tv_browse_rail_test.dart index e13b5c4d..95dee89a 100644 --- a/test/widgets/tv_browse_rail_test.dart +++ b/test/widgets/tv_browse_rail_test.dart @@ -14,6 +14,7 @@ import 'package:plezy/services/multi_server_manager.dart'; import 'package:plezy/services/settings_service.dart'; import 'package:plezy/theme/mono_theme.dart'; import 'package:plezy/utils/platform_detector.dart'; +import 'package:plezy/widgets/media_card.dart'; import 'package:plezy/widgets/side_navigation_rail.dart'; import 'package:plezy/widgets/tv_browse_rail.dart'; import 'package:provider/provider.dart'; @@ -559,7 +560,7 @@ void main() { expect(find.text('Visible Movie'), findsOneWidget); }); - testWidgets('detailed card focus ring wraps card content height', (tester) async { + testWidgets('detailed card focus border hugs the poster, captions outside', (tester) async { await SettingsService.instanceOrNull!.write(SettingsService.tvFullCardLayout, false); TvDetectionService.debugSetAppleTVOverride(true); tester.view.devicePixelRatio = 1.0; @@ -599,11 +600,13 @@ void main() { ); await tester.pump(); - final focusDecoration = find.ancestor( - of: find.text('Visible Movie'), + // The border is drawn inside MediaCard around the poster only (#1278); + // the title/year captions render below it, outside the focus rect. + final focusDecoration = find.descendant( + of: find.ancestor(of: find.text('Visible Movie'), matching: find.byType(MediaCard)), matching: find.byWidgetPredicate((widget) { - if (widget is! AnimatedContainer || widget.decoration is! BoxDecoration) return false; - return (widget.decoration as BoxDecoration).border is Border && widget.foregroundDecoration == null; + if (widget is! AnimatedContainer || widget.foregroundDecoration is! BoxDecoration) return false; + return (widget.foregroundDecoration as BoxDecoration).border is Border; }), ); @@ -611,8 +614,12 @@ void main() { expect(find.text('2024'), findsOneWidget); final focusRect = tester.getRect(focusDecoration); + final titleRect = tester.getRect(find.text('Visible Movie')); final subtitleRect = tester.getRect(find.text('2024')); - expect(focusRect.bottom - subtitleRect.bottom, lessThan(8)); + expect(focusRect.bottom, lessThanOrEqualTo(titleRect.top)); + expect(focusRect.bottom, lessThanOrEqualTo(subtitleRect.top)); + // Still a tight ring: the poster fills the card width above the captions. + expect(focusRect.top, lessThan(titleRect.top)); }); testWidgets('view all item uses compact pill focus style', (tester) async {