fix(tv): hug poster with focus border and add focus glow toggle
close #1278
This commit is contained in:
@@ -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<CardFocusScope>()?.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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<FocusGlowOverlay> {
|
||||
/// 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<FocusGlowOverlay> {
|
||||
@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<FocusGlowOverlay> {
|
||||
|
||||
@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).
|
||||
|
||||
@@ -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<FocusableWrapper> 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<FocusableWrapper> 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,
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
"showSeasonPostersOnTabsDescription": "Показвай постера на всеки сезон над неговия таб",
|
||||
"tvFullCardLayout": "Пълни TV карти",
|
||||
"tvFullCardLayoutDescription": "Използвай TV карти само с изображения, с насложени имена на актьорите",
|
||||
"focusGlow": "Сияние при фокус",
|
||||
"focusGlowDescription": "Показвай меко сияние около фокусираната карта",
|
||||
"hideSpoilers": "Скривай спойлери за негледани епизоди",
|
||||
"hideSpoilersDescription": "Замазвай миниатюри и описания за негледани епизоди",
|
||||
"playerBackend": "Енджин на плеъра",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
"showSeasonPostersOnTabsDescription": "各シーズンのポスターをタブの上に表示します",
|
||||
"tvFullCardLayout": "フルTVカード",
|
||||
"tvFullCardLayoutDescription": "TVカードを画像のみで表示し、俳優名を重ねて表示します",
|
||||
"focusGlow": "フォーカス時の光彩",
|
||||
"focusGlowDescription": "フォーカス中のカードの周りに柔らかい光彩を表示します",
|
||||
"hideSpoilers": "未視聴エピソードのネタバレを非表示",
|
||||
"hideSpoilersDescription": "未視聴エピソードのサムネイルと説明をぼかします",
|
||||
"playerBackend": "プレーヤーバックエンド",
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
"showSeasonPostersOnTabsDescription": "각 시즌 포스터를 탭 위에 표시",
|
||||
"tvFullCardLayout": "전체 TV 카드",
|
||||
"tvFullCardLayoutDescription": "TV 카드에 이미지만 표시하고 배우 이름을 오버레이로 표시",
|
||||
"focusGlow": "포커스 글로우",
|
||||
"focusGlowDescription": "포커스된 카드 주위에 은은한 빛 효과를 표시",
|
||||
"hideSpoilers": "미시청 에피소드 스포일러 숨기기",
|
||||
"hideSpoilersDescription": "시청하지 않은 에피소드의 썸네일과 설명을 흐리게 처리",
|
||||
"playerBackend": "플레이어 백엔드",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
"showSeasonPostersOnTabsDescription": "Показывать постер каждого сезона над его вкладкой",
|
||||
"tvFullCardLayout": "Полные TV-карточки",
|
||||
"tvFullCardLayoutDescription": "Использовать TV-карточки только с изображением и именами актёров поверх него",
|
||||
"focusGlow": "Свечение при фокусе",
|
||||
"focusGlowDescription": "Показывать мягкое свечение вокруг карточки в фокусе",
|
||||
"hideSpoilers": "Скрыть спойлеры непросмотренных эпизодов",
|
||||
"hideSpoilersDescription": "Размывать миниатюры и описания непросмотренных серий",
|
||||
"playerBackend": "Бэкенд плеера",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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' => 'Настройки',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => '設定',
|
||||
|
||||
@@ -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' => '설정',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => 'Настройки',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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' => '设置',
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -134,6 +134,8 @@
|
||||
"showSeasonPostersOnTabsDescription": "在每季标签上方显示该季海报",
|
||||
"tvFullCardLayout": "完整 TV 卡片",
|
||||
"tvFullCardLayoutDescription": "使用仅显示图片的 TV 卡片,并叠加演员姓名",
|
||||
"focusGlow": "焦点光晕",
|
||||
"focusGlowDescription": "在获得焦点的卡片周围显示柔和的光晕",
|
||||
"hideSpoilers": "隐藏未看剧集的剧透内容",
|
||||
"hideSpoilersDescription": "模糊未观看剧集的缩略图和描述",
|
||||
"playerBackend": "播放器引擎",
|
||||
|
||||
@@ -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<MediaDetailScreen>
|
||||
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<MediaDetailScreen>
|
||||
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<MediaDetailScreen>
|
||||
context: context,
|
||||
isFocused: isFocused,
|
||||
onTap: () => navigateToVideoPlayer(context, metadata: extra),
|
||||
delegateFocusBorder: true,
|
||||
child: MediaCard(
|
||||
key: cardKey,
|
||||
item: extra,
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -117,9 +117,10 @@ class _FocusableMediaCardState extends State<FocusableMediaCard> {
|
||||
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(
|
||||
|
||||
@@ -558,6 +558,7 @@ class HubSectionState extends State<HubSection> with MountedSetStateMixin {
|
||||
isFocused: isItemFocused,
|
||||
onTap: () => _onItemTapped(index),
|
||||
onLongPress: () => _mediaCardKeys[index]?.currentState?.showContextMenu(),
|
||||
delegateFocusBorder: true,
|
||||
child: MediaCard(
|
||||
key: _getMediaCardKey(index),
|
||||
item: item,
|
||||
|
||||
+171
-175
@@ -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<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
onSecondaryTapDown: storeTapPosition,
|
||||
onSecondaryTap: showContextMenuFromTap,
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
_buildPosterImage(
|
||||
context,
|
||||
item,
|
||||
isOffline: widget.isOffline,
|
||||
localPosterPath: localPosterPath,
|
||||
mixedHubContext: widget.mixedHubContext,
|
||||
episodePosterModeOverride: widget.episodePosterModeOverride,
|
||||
knownWidth: width,
|
||||
knownHeight: height,
|
||||
),
|
||||
if (item is MediaItem) _MediaCardHelpers.buildWatchProgress(context, item),
|
||||
],
|
||||
child: CardFocusBorder(
|
||||
borderRadius: tokens(context).radiusSm,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
_buildPosterImage(
|
||||
context,
|
||||
item,
|
||||
isOffline: widget.isOffline,
|
||||
localPosterPath: localPosterPath,
|
||||
mixedHubContext: widget.mixedHubContext,
|
||||
episodePosterModeOverride: widget.episodePosterModeOverride,
|
||||
knownWidth: width,
|
||||
knownHeight: height,
|
||||
),
|
||||
if (item is MediaItem) _MediaCardHelpers.buildWatchProgress(context, item),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -327,6 +331,30 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
final posterWidth = widget.width != null ? widget.width! - 6 : null;
|
||||
final posterHeight = widget.height;
|
||||
|
||||
// The focus border hugs the poster (captions stay outside it), matching
|
||||
// the full-bleed card treatment.
|
||||
final poster = CardFocusBorder(
|
||||
borderRadius: tokens(context).radiusSm,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: _buildPosterImage(
|
||||
context,
|
||||
item,
|
||||
isOffline: widget.isOffline,
|
||||
localPosterPath: localPosterPath,
|
||||
mixedHubContext: widget.mixedHubContext,
|
||||
episodePosterModeOverride: widget.episodePosterModeOverride,
|
||||
knownWidth: posterHeight != null ? posterWidth : null,
|
||||
knownHeight: posterHeight,
|
||||
),
|
||||
),
|
||||
if (item is MediaItem) _MediaCardHelpers.buildWatchProgress(context, item),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
return SizedBox(
|
||||
width: widget.width,
|
||||
child: InkWell(
|
||||
@@ -346,47 +374,9 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
||||
children: [
|
||||
// Poster with overlay
|
||||
if (posterHeight != null)
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: posterHeight,
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: _buildPosterImage(
|
||||
context,
|
||||
item,
|
||||
isOffline: widget.isOffline,
|
||||
localPosterPath: localPosterPath,
|
||||
mixedHubContext: widget.mixedHubContext,
|
||||
episodePosterModeOverride: widget.episodePosterModeOverride,
|
||||
knownWidth: posterWidth,
|
||||
knownHeight: posterHeight,
|
||||
),
|
||||
),
|
||||
if (item is MediaItem) _MediaCardHelpers.buildWatchProgress(context, item),
|
||||
],
|
||||
),
|
||||
)
|
||||
SizedBox(width: double.infinity, height: posterHeight, child: poster)
|
||||
else
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: _buildPosterImage(
|
||||
context,
|
||||
item,
|
||||
isOffline: widget.isOffline,
|
||||
localPosterPath: localPosterPath,
|
||||
mixedHubContext: widget.mixedHubContext,
|
||||
episodePosterModeOverride: widget.episodePosterModeOverride,
|
||||
),
|
||||
),
|
||||
if (item is MediaItem) _MediaCardHelpers.buildWatchProgress(context, item),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(child: poster),
|
||||
const SizedBox(height: 2),
|
||||
// Title (flattened — no inner Column)
|
||||
if (item is MediaItem && _hasClickableTitle(item))
|
||||
@@ -592,133 +582,139 @@ class _MediaCardList extends StatelessWidget {
|
||||
final metadataLine = _buildMetadataLine();
|
||||
final subtitle = _buildSubtitleText();
|
||||
|
||||
return InkWell(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
canRequestFocus: false, // Keyboard handled by FocusableMediaCard
|
||||
onTap: onTap,
|
||||
onTapDown: onTapDown,
|
||||
onLongPress: onLongPress,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
crossAxisAlignment: .start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: _posterWidth(),
|
||||
height: _posterHeight(),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: _buildPosterImage(
|
||||
context,
|
||||
item,
|
||||
isOffline: isOffline,
|
||||
localPosterPath: localPosterPath,
|
||||
episodePosterModeOverride: episodePosterModeOverride,
|
||||
// List rows keep the whole-row border; inside stroke so adjacent rows
|
||||
// don't overlap.
|
||||
return CardFocusBorder(
|
||||
borderRadius: tokens(context).radiusSm,
|
||||
strokeAlign: BorderSide.strokeAlignInside,
|
||||
child: InkWell(
|
||||
mouseCursor: SystemMouseCursors.click,
|
||||
canRequestFocus: false, // Keyboard handled by FocusableMediaCard
|
||||
onTap: onTap,
|
||||
onTapDown: onTapDown,
|
||||
onLongPress: onLongPress,
|
||||
onSecondaryTapDown: onSecondaryTapDown,
|
||||
onSecondaryTap: onSecondaryTap,
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: Row(
|
||||
crossAxisAlignment: .start,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: _posterWidth(),
|
||||
height: _posterHeight(),
|
||||
child: Stack(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||
child: _buildPosterImage(
|
||||
context,
|
||||
item,
|
||||
isOffline: isOffline,
|
||||
localPosterPath: localPosterPath,
|
||||
episodePosterModeOverride: episodePosterModeOverride,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (item is MediaItem) _MediaCardHelpers.buildWatchProgress(context, item as MediaItem),
|
||||
],
|
||||
if (item is MediaItem) _MediaCardHelpers.buildWatchProgress(context, item as MediaItem),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
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: 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<TvBrowseRail> {
|
||||
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<TvBrowseRail> {
|
||||
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<TvBrowseRail> {
|
||||
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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user