feat(tv): add optional full-card layouts
This commit is contained in:
@@ -5,8 +5,12 @@ class FocusTheme {
|
|||||||
FocusTheme._();
|
FocusTheme._();
|
||||||
|
|
||||||
static const double focusScale = 1.02;
|
static const double focusScale = 1.02;
|
||||||
|
static const double fullCardFocusScale = 1.03;
|
||||||
static const double focusBorderWidth = 2.5;
|
static const double focusBorderWidth = 2.5;
|
||||||
static const double defaultBorderRadius = 8.0;
|
static const double defaultBorderRadius = 8.0;
|
||||||
|
static const double focusGlowInnerBlurRadius = 18;
|
||||||
|
static const double focusGlowOuterBlurRadius = 34;
|
||||||
|
static const double focusGlowSpreadRadius = 1.5;
|
||||||
|
|
||||||
static Color getFocusBorderColor(BuildContext context) {
|
static Color getFocusBorderColor(BuildContext context) {
|
||||||
return Theme.of(context).colorScheme.primary;
|
return Theme.of(context).colorScheme.primary;
|
||||||
@@ -20,13 +24,42 @@ class FocusTheme {
|
|||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required bool isFocused,
|
required bool isFocused,
|
||||||
double borderRadius = defaultBorderRadius,
|
double borderRadius = defaultBorderRadius,
|
||||||
|
double borderStrokeAlign = BorderSide.strokeAlignInside,
|
||||||
Color? color,
|
Color? color,
|
||||||
}) {
|
}) {
|
||||||
final focusColor = color ?? getFocusBorderColor(context);
|
final focusColor = color ?? getFocusBorderColor(context);
|
||||||
|
|
||||||
return BoxDecoration(
|
return BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(borderRadius),
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
border: Border.all(color: isFocused ? focusColor : Colors.transparent, width: focusBorderWidth),
|
border: Border.all(
|
||||||
|
color: isFocused ? focusColor : Colors.transparent,
|
||||||
|
width: focusBorderWidth,
|
||||||
|
strokeAlign: borderStrokeAlign,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static BoxDecoration focusGlowDecoration(
|
||||||
|
BuildContext context, {
|
||||||
|
required bool isFocused,
|
||||||
|
double borderRadius = defaultBorderRadius,
|
||||||
|
Color? color,
|
||||||
|
}) {
|
||||||
|
final focusColor = color ?? getFocusBorderColor(context);
|
||||||
|
|
||||||
|
return BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(borderRadius),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: isFocused ? focusColor.withValues(alpha: 0.34) : Colors.transparent,
|
||||||
|
blurRadius: focusGlowInnerBlurRadius,
|
||||||
|
spreadRadius: focusGlowSpreadRadius,
|
||||||
|
),
|
||||||
|
BoxShadow(
|
||||||
|
color: isFocused ? focusColor.withValues(alpha: 0.20) : Colors.transparent,
|
||||||
|
blurRadius: focusGlowOuterBlurRadius,
|
||||||
|
),
|
||||||
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -106,6 +106,18 @@ class FocusableWrapper extends StatefulWidget {
|
|||||||
/// Useful for elements like sliders where scaling looks odd.
|
/// Useful for elements like sliders where scaling looks odd.
|
||||||
final bool disableScale;
|
final bool disableScale;
|
||||||
|
|
||||||
|
/// 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;
|
||||||
|
|
||||||
/// Whether descendants can receive focus.
|
/// Whether descendants can receive focus.
|
||||||
/// Set to false when the child widget has its own Focus (e.g. buttons)
|
/// Set to false when the child widget has its own Focus (e.g. buttons)
|
||||||
/// that would compete with this wrapper's focus handling.
|
/// that would compete with this wrapper's focus handling.
|
||||||
@@ -136,6 +148,10 @@ class FocusableWrapper extends StatefulWidget {
|
|||||||
this.useBackgroundFocus = false,
|
this.useBackgroundFocus = false,
|
||||||
this.focusColor,
|
this.focusColor,
|
||||||
this.disableScale = false,
|
this.disableScale = false,
|
||||||
|
this.focusScale = FocusTheme.focusScale,
|
||||||
|
this.focusBorderStrokeAlign = BorderSide.strokeAlignInside,
|
||||||
|
this.useFocusGlow = false,
|
||||||
|
this.useForegroundFocusDecoration = false,
|
||||||
this.descendantsAreFocusable = true,
|
this.descendantsAreFocusable = true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,7 +165,7 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
|||||||
bool _isFocused = false;
|
bool _isFocused = false;
|
||||||
|
|
||||||
late final AnimationController _animationController;
|
late final AnimationController _animationController;
|
||||||
late final Animation<double> _scaleAnimation;
|
late Animation<double> _scaleAnimation;
|
||||||
|
|
||||||
// Long-press detection for SELECT key
|
// Long-press detection for SELECT key
|
||||||
Timer? _longPressTimer;
|
Timer? _longPressTimer;
|
||||||
@@ -178,9 +194,13 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
|||||||
void _initAnimations() {
|
void _initAnimations() {
|
||||||
_animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 150));
|
_animationController = AnimationController(vsync: this, duration: const Duration(milliseconds: 150));
|
||||||
|
|
||||||
_scaleAnimation = Tween<double>(
|
_scaleAnimation = _createScaleAnimation();
|
||||||
|
}
|
||||||
|
|
||||||
|
Animation<double> _createScaleAnimation() {
|
||||||
|
return Tween<double>(
|
||||||
begin: 1.0,
|
begin: 1.0,
|
||||||
end: FocusTheme.focusScale,
|
end: widget.focusScale,
|
||||||
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic));
|
).animate(CurvedAnimation(parent: _animationController, curve: Curves.easeOutCubic));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,6 +220,10 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
|||||||
if (widget.canRequestFocus != oldWidget.canRequestFocus) {
|
if (widget.canRequestFocus != oldWidget.canRequestFocus) {
|
||||||
_focusNode.canRequestFocus = widget.canRequestFocus;
|
_focusNode.canRequestFocus = widget.canRequestFocus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (widget.focusScale != oldWidget.focusScale) {
|
||||||
|
_scaleAnimation = _createScaleAnimation();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -447,14 +471,23 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Choose decoration based on useBackgroundFocus
|
// Choose decoration based on useBackgroundFocus
|
||||||
final decoration = widget.useBackgroundFocus
|
final focusDecoration = widget.useBackgroundFocus
|
||||||
? FocusTheme.focusBackgroundDecoration(isFocused: showFocus, borderRadius: widget.borderRadius)
|
? FocusTheme.focusBackgroundDecoration(isFocused: showFocus, borderRadius: widget.borderRadius)
|
||||||
: FocusTheme.focusDecoration(
|
: FocusTheme.focusDecoration(
|
||||||
context,
|
context,
|
||||||
isFocused: showFocus,
|
isFocused: showFocus,
|
||||||
borderRadius: widget.borderRadius,
|
borderRadius: widget.borderRadius,
|
||||||
color: widget.focusColor,
|
color: widget.focusColor,
|
||||||
|
borderStrokeAlign: widget.focusBorderStrokeAlign,
|
||||||
);
|
);
|
||||||
|
final glowDecoration = widget.useFocusGlow
|
||||||
|
? FocusTheme.focusGlowDecoration(
|
||||||
|
context,
|
||||||
|
isFocused: showFocus,
|
||||||
|
borderRadius: widget.borderRadius,
|
||||||
|
color: widget.focusColor,
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
Widget result = Focus(
|
Widget result = Focus(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
@@ -471,7 +504,8 @@ class _FocusableWrapperState extends State<FocusableWrapper> with SingleTickerPr
|
|||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: duration,
|
duration: duration,
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
decoration: decoration,
|
decoration: widget.useForegroundFocusDecoration ? glowDecoration : focusDecoration,
|
||||||
|
foregroundDecoration: widget.useForegroundFocusDecoration ? focusDecoration : null,
|
||||||
child: widget.child,
|
child: widget.child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Показвай сезон и номер на епизод върху картите на епизодите",
|
"showEpisodeNumberOnCardsDescription": "Показвай сезон и номер на епизод върху картите на епизодите",
|
||||||
"showSeasonPostersOnTabs": "Показвай постери на сезоните в табовете",
|
"showSeasonPostersOnTabs": "Показвай постери на сезоните в табовете",
|
||||||
"showSeasonPostersOnTabsDescription": "Показвай постера на всеки сезон над неговия таб",
|
"showSeasonPostersOnTabsDescription": "Показвай постера на всеки сезон над неговия таб",
|
||||||
|
"tvFullCardLayout": "Пълни TV карти",
|
||||||
|
"tvFullCardLayoutDescription": "Използвай TV карти само с изображения, с насложени имена на актьорите",
|
||||||
"hideSpoilers": "Скривай спойлери за негледани епизоди",
|
"hideSpoilers": "Скривай спойлери за негледани епизоди",
|
||||||
"hideSpoilersDescription": "Замазвай миниатюри и описания за негледани епизоди",
|
"hideSpoilersDescription": "Замазвай миниатюри и описания за негледани епизоди",
|
||||||
"playerBackend": "Енджин на плеъра",
|
"playerBackend": "Енджин на плеъра",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Vis sæson- og episodenummer på episodekort",
|
"showEpisodeNumberOnCardsDescription": "Vis sæson- og episodenummer på episodekort",
|
||||||
"showSeasonPostersOnTabs": "Vis sæsonplakater på faner",
|
"showSeasonPostersOnTabs": "Vis sæsonplakater på faner",
|
||||||
"showSeasonPostersOnTabsDescription": "Vis hver sæsons plakat over dens fane",
|
"showSeasonPostersOnTabsDescription": "Vis hver sæsons plakat over dens fane",
|
||||||
|
"tvFullCardLayout": "Fuldflade TV-kort",
|
||||||
|
"tvFullCardLayoutDescription": "Brug TV-kort kun med billeder og skuespillernavne ovenpå",
|
||||||
"hideSpoilers": "Skjul spoilere for usete episoder",
|
"hideSpoilers": "Skjul spoilere for usete episoder",
|
||||||
"hideSpoilersDescription": "Slør miniaturebilleder og beskrivelser for usete episoder",
|
"hideSpoilersDescription": "Slør miniaturebilleder og beskrivelser for usete episoder",
|
||||||
"playerBackend": "Afspillerbackend",
|
"playerBackend": "Afspillerbackend",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Staffel- und Episodennummer auf Episodenkarten anzeigen",
|
"showEpisodeNumberOnCardsDescription": "Staffel- und Episodennummer auf Episodenkarten anzeigen",
|
||||||
"showSeasonPostersOnTabs": "Staffelposter auf Tabs anzeigen",
|
"showSeasonPostersOnTabs": "Staffelposter auf Tabs anzeigen",
|
||||||
"showSeasonPostersOnTabsDescription": "Poster jeder Staffel über ihrem Tab anzeigen",
|
"showSeasonPostersOnTabsDescription": "Poster jeder Staffel über ihrem Tab anzeigen",
|
||||||
|
"tvFullCardLayout": "Vollflächige TV-Karten",
|
||||||
|
"tvFullCardLayoutDescription": "TV-Karten nur mit Bild verwenden und Darstellernamen einblenden",
|
||||||
"hideSpoilers": "Spoiler für nicht gesehene Episoden verbergen",
|
"hideSpoilers": "Spoiler für nicht gesehene Episoden verbergen",
|
||||||
"hideSpoilersDescription": "Vorschaubilder und Beschreibungen ungesehener Episoden verwischen",
|
"hideSpoilersDescription": "Vorschaubilder und Beschreibungen ungesehener Episoden verwischen",
|
||||||
"playerBackend": "Player-Backend",
|
"playerBackend": "Player-Backend",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Show season and episode number on episode cards",
|
"showEpisodeNumberOnCardsDescription": "Show season and episode number on episode cards",
|
||||||
"showSeasonPostersOnTabs": "Show Season Posters on Tabs",
|
"showSeasonPostersOnTabs": "Show Season Posters on Tabs",
|
||||||
"showSeasonPostersOnTabsDescription": "Show each season's poster above its tab",
|
"showSeasonPostersOnTabsDescription": "Show each season's poster above its tab",
|
||||||
|
"tvFullCardLayout": "Full TV Cards",
|
||||||
|
"tvFullCardLayoutDescription": "Use image-only TV cards with actor names overlaid",
|
||||||
"hideSpoilers": "Hide Spoilers for Unwatched Episodes",
|
"hideSpoilers": "Hide Spoilers for Unwatched Episodes",
|
||||||
"hideSpoilersDescription": "Blur thumbnails and descriptions for unwatched episodes",
|
"hideSpoilersDescription": "Blur thumbnails and descriptions for unwatched episodes",
|
||||||
"playerBackend": "Player Backend",
|
"playerBackend": "Player Backend",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Mostrar temporada y episodio en tarjetas de episodio",
|
"showEpisodeNumberOnCardsDescription": "Mostrar temporada y episodio en tarjetas de episodio",
|
||||||
"showSeasonPostersOnTabs": "Mostrar pósters de temporada en las pestañas",
|
"showSeasonPostersOnTabs": "Mostrar pósters de temporada en las pestañas",
|
||||||
"showSeasonPostersOnTabsDescription": "Mostrar el póster de cada temporada sobre su pestaña",
|
"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",
|
||||||
"hideSpoilers": "Ocultar spoilers de episodios no vistos",
|
"hideSpoilers": "Ocultar spoilers de episodios no vistos",
|
||||||
"hideSpoilersDescription": "Desenfocar miniaturas y descripciones de episodios no vistos",
|
"hideSpoilersDescription": "Desenfocar miniaturas y descripciones de episodios no vistos",
|
||||||
"playerBackend": "Reproductor",
|
"playerBackend": "Reproductor",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Afficher la saison et l'épisode sur les cartes d'épisode",
|
"showEpisodeNumberOnCardsDescription": "Afficher la saison et l'épisode sur les cartes d'épisode",
|
||||||
"showSeasonPostersOnTabs": "Afficher les posters de saison sur les onglets",
|
"showSeasonPostersOnTabs": "Afficher les posters de saison sur les onglets",
|
||||||
"showSeasonPostersOnTabsDescription": "Afficher l'affiche de chaque saison au-dessus de son onglet",
|
"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",
|
||||||
"hideSpoilers": "Masquer les spoilers des épisodes non vus",
|
"hideSpoilers": "Masquer les spoilers des épisodes non vus",
|
||||||
"hideSpoilersDescription": "Flouter les miniatures et descriptions des épisodes non vus",
|
"hideSpoilersDescription": "Flouter les miniatures et descriptions des épisodes non vus",
|
||||||
"playerBackend": "Moteur de lecture",
|
"playerBackend": "Moteur de lecture",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Mostra stagione ed episodio sulle schede episodio",
|
"showEpisodeNumberOnCardsDescription": "Mostra stagione ed episodio sulle schede episodio",
|
||||||
"showSeasonPostersOnTabs": "Mostra poster delle stagioni sulle schede",
|
"showSeasonPostersOnTabs": "Mostra poster delle stagioni sulle schede",
|
||||||
"showSeasonPostersOnTabsDescription": "Mostra il poster di ogni stagione sopra la sua scheda",
|
"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",
|
||||||
"hideSpoilers": "Nascondi spoiler per episodi non visti",
|
"hideSpoilers": "Nascondi spoiler per episodi non visti",
|
||||||
"hideSpoilersDescription": "Sfoca miniature e descrizioni degli episodi non visti",
|
"hideSpoilersDescription": "Sfoca miniature e descrizioni degli episodi non visti",
|
||||||
"playerBackend": "Motore di riproduzione",
|
"playerBackend": "Motore di riproduzione",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "エピソードカードにシーズン番号とエピソード番号を表示します",
|
"showEpisodeNumberOnCardsDescription": "エピソードカードにシーズン番号とエピソード番号を表示します",
|
||||||
"showSeasonPostersOnTabs": "タブにシーズンポスターを表示",
|
"showSeasonPostersOnTabs": "タブにシーズンポスターを表示",
|
||||||
"showSeasonPostersOnTabsDescription": "各シーズンのポスターをタブの上に表示します",
|
"showSeasonPostersOnTabsDescription": "各シーズンのポスターをタブの上に表示します",
|
||||||
|
"tvFullCardLayout": "フルTVカード",
|
||||||
|
"tvFullCardLayoutDescription": "TVカードを画像のみで表示し、俳優名を重ねて表示します",
|
||||||
"hideSpoilers": "未視聴エピソードのネタバレを非表示",
|
"hideSpoilers": "未視聴エピソードのネタバレを非表示",
|
||||||
"hideSpoilersDescription": "未視聴エピソードのサムネイルと説明をぼかします",
|
"hideSpoilersDescription": "未視聴エピソードのサムネイルと説明をぼかします",
|
||||||
"playerBackend": "プレーヤーバックエンド",
|
"playerBackend": "プレーヤーバックエンド",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "에피소드 카드에 시즌 및 에피소드 번호 표시",
|
"showEpisodeNumberOnCardsDescription": "에피소드 카드에 시즌 및 에피소드 번호 표시",
|
||||||
"showSeasonPostersOnTabs": "탭에 시즌 포스터 표시",
|
"showSeasonPostersOnTabs": "탭에 시즌 포스터 표시",
|
||||||
"showSeasonPostersOnTabsDescription": "각 시즌 포스터를 탭 위에 표시",
|
"showSeasonPostersOnTabsDescription": "각 시즌 포스터를 탭 위에 표시",
|
||||||
|
"tvFullCardLayout": "전체 TV 카드",
|
||||||
|
"tvFullCardLayoutDescription": "TV 카드에 이미지만 표시하고 배우 이름을 오버레이로 표시",
|
||||||
"hideSpoilers": "미시청 에피소드 스포일러 숨기기",
|
"hideSpoilers": "미시청 에피소드 스포일러 숨기기",
|
||||||
"hideSpoilersDescription": "시청하지 않은 에피소드의 썸네일과 설명을 흐리게 처리",
|
"hideSpoilersDescription": "시청하지 않은 에피소드의 썸네일과 설명을 흐리게 처리",
|
||||||
"playerBackend": "플레이어 백엔드",
|
"playerBackend": "플레이어 백엔드",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Vis sesong- og episodenummer på episodekort",
|
"showEpisodeNumberOnCardsDescription": "Vis sesong- og episodenummer på episodekort",
|
||||||
"showSeasonPostersOnTabs": "Vis sesongplakater på faner",
|
"showSeasonPostersOnTabs": "Vis sesongplakater på faner",
|
||||||
"showSeasonPostersOnTabsDescription": "Vis hver sesongs plakat over fanen",
|
"showSeasonPostersOnTabsDescription": "Vis hver sesongs plakat over fanen",
|
||||||
|
"tvFullCardLayout": "Fulle TV-kort",
|
||||||
|
"tvFullCardLayoutDescription": "Bruk bildebaserte TV-kort med skuespillernavn lagt over",
|
||||||
"hideSpoilers": "Skjul spoilere for usette episoder",
|
"hideSpoilers": "Skjul spoilere for usette episoder",
|
||||||
"hideSpoilersDescription": "Slør miniatyrbilder og beskrivelser for usette episoder",
|
"hideSpoilersDescription": "Slør miniatyrbilder og beskrivelser for usette episoder",
|
||||||
"playerBackend": "Spillermotor",
|
"playerBackend": "Spillermotor",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Toon seizoen- en afleveringsnummer op afleveringskaarten",
|
"showEpisodeNumberOnCardsDescription": "Toon seizoen- en afleveringsnummer op afleveringskaarten",
|
||||||
"showSeasonPostersOnTabs": "Toon seizoensposters op tabbladen",
|
"showSeasonPostersOnTabs": "Toon seizoensposters op tabbladen",
|
||||||
"showSeasonPostersOnTabsDescription": "Toon de poster van elk seizoen boven het tabblad",
|
"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",
|
||||||
"hideSpoilers": "Spoilers voor ongekeken afleveringen verbergen",
|
"hideSpoilers": "Spoilers voor ongekeken afleveringen verbergen",
|
||||||
"hideSpoilersDescription": "Vervaag miniaturen en beschrijvingen voor niet-bekeken afleveringen",
|
"hideSpoilersDescription": "Vervaag miniaturen en beschrijvingen voor niet-bekeken afleveringen",
|
||||||
"playerBackend": "Speler backend",
|
"playerBackend": "Speler backend",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Pokazuj numer sezonu i odcinka na kartach odcinków",
|
"showEpisodeNumberOnCardsDescription": "Pokazuj numer sezonu i odcinka na kartach odcinków",
|
||||||
"showSeasonPostersOnTabs": "Pokaż plakaty sezonów na zakładkach",
|
"showSeasonPostersOnTabs": "Pokaż plakaty sezonów na zakładkach",
|
||||||
"showSeasonPostersOnTabsDescription": "Pokazuj plakat każdego sezonu nad jego kartą",
|
"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",
|
||||||
"hideSpoilers": "Ukryj spoilery nieobejrzanych odcinków",
|
"hideSpoilers": "Ukryj spoilery nieobejrzanych odcinków",
|
||||||
"hideSpoilersDescription": "Rozmywaj miniatury i opisy nieobejrzanych odcinków",
|
"hideSpoilersDescription": "Rozmywaj miniatury i opisy nieobejrzanych odcinków",
|
||||||
"playerBackend": "Backend odtwarzacza",
|
"playerBackend": "Backend odtwarzacza",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Mostrar temporada e episódio nos cartões de episódio",
|
"showEpisodeNumberOnCardsDescription": "Mostrar temporada e episódio nos cartões de episódio",
|
||||||
"showSeasonPostersOnTabs": "Mostrar Pôsteres de Temporada nas Abas",
|
"showSeasonPostersOnTabs": "Mostrar Pôsteres de Temporada nas Abas",
|
||||||
"showSeasonPostersOnTabsDescription": "Mostrar o pôster de cada temporada acima da aba",
|
"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",
|
||||||
"hideSpoilers": "Ocultar Spoilers de Episódios Não Assistidos",
|
"hideSpoilers": "Ocultar Spoilers de Episódios Não Assistidos",
|
||||||
"hideSpoilersDescription": "Desfocar miniaturas e descrições de episódios não vistos",
|
"hideSpoilersDescription": "Desfocar miniaturas e descrições de episódios não vistos",
|
||||||
"playerBackend": "Backend do Player",
|
"playerBackend": "Backend do Player",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Показывать номер сезона и серии на карточках серий",
|
"showEpisodeNumberOnCardsDescription": "Показывать номер сезона и серии на карточках серий",
|
||||||
"showSeasonPostersOnTabs": "Показывать постеры сезонов на вкладках",
|
"showSeasonPostersOnTabs": "Показывать постеры сезонов на вкладках",
|
||||||
"showSeasonPostersOnTabsDescription": "Показывать постер каждого сезона над его вкладкой",
|
"showSeasonPostersOnTabsDescription": "Показывать постер каждого сезона над его вкладкой",
|
||||||
|
"tvFullCardLayout": "Полные TV-карточки",
|
||||||
|
"tvFullCardLayoutDescription": "Использовать TV-карточки только с изображением и именами актёров поверх него",
|
||||||
"hideSpoilers": "Скрыть спойлеры непросмотренных эпизодов",
|
"hideSpoilers": "Скрыть спойлеры непросмотренных эпизодов",
|
||||||
"hideSpoilersDescription": "Размывать миниатюры и описания непросмотренных серий",
|
"hideSpoilersDescription": "Размывать миниатюры и описания непросмотренных серий",
|
||||||
"playerBackend": "Бэкенд плеера",
|
"playerBackend": "Бэкенд плеера",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 16
|
/// Locales: 16
|
||||||
/// Strings: 18832 (1177 per locale)
|
/// Strings: 18864 (1179 per locale)
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsBg extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Показвай сезон и номер на епизод върху картите на епизодите';
|
@override String get showEpisodeNumberOnCardsDescription => 'Показвай сезон и номер на епизод върху картите на епизодите';
|
||||||
@override String get showSeasonPostersOnTabs => 'Показвай постери на сезоните в табовете';
|
@override String get showSeasonPostersOnTabs => 'Показвай постери на сезоните в табовете';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Показвай постера на всеки сезон над неговия таб';
|
@override String get showSeasonPostersOnTabsDescription => 'Показвай постера на всеки сезон над неговия таб';
|
||||||
|
@override String get tvFullCardLayout => 'Пълни TV карти';
|
||||||
|
@override String get tvFullCardLayoutDescription => 'Използвай TV карти само с изображения, с насложени имена на актьорите';
|
||||||
@override String get hideSpoilers => 'Скривай спойлери за негледани епизоди';
|
@override String get hideSpoilers => 'Скривай спойлери за негледани епизоди';
|
||||||
@override String get hideSpoilersDescription => 'Замазвай миниатюри и описания за негледани епизоди';
|
@override String get hideSpoilersDescription => 'Замазвай миниатюри и описания за негледани епизоди';
|
||||||
@override String get playerBackend => 'Енджин на плеъра';
|
@override String get playerBackend => 'Енджин на плеъра';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsBg {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Показвай сезон и номер на епизод върху картите на епизодите',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Показвай сезон и номер на епизод върху картите на епизодите',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Показвай постери на сезоните в табовете',
|
'settings.showSeasonPostersOnTabs' => 'Показвай постери на сезоните в табовете',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Показвай постера на всеки сезон над неговия таб',
|
'settings.showSeasonPostersOnTabsDescription' => 'Показвай постера на всеки сезон над неговия таб',
|
||||||
|
'settings.tvFullCardLayout' => 'Пълни TV карти',
|
||||||
|
'settings.tvFullCardLayoutDescription' => 'Използвай TV карти само с изображения, с насложени имена на актьорите',
|
||||||
'settings.hideSpoilers' => 'Скривай спойлери за негледани епизоди',
|
'settings.hideSpoilers' => 'Скривай спойлери за негледани епизоди',
|
||||||
'settings.hideSpoilersDescription' => 'Замазвай миниатюри и описания за негледани епизоди',
|
'settings.hideSpoilersDescription' => 'Замазвай миниатюри и описания за негледани епизоди',
|
||||||
'settings.playerBackend' => 'Енджин на плеъра',
|
'settings.playerBackend' => 'Енджин на плеъра',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsBg {
|
|||||||
'profiles.active' => 'Активен',
|
'profiles.active' => 'Активен',
|
||||||
'profiles.manage' => 'Управление',
|
'profiles.manage' => 'Управление',
|
||||||
'profiles.delete' => 'Изтрий',
|
'profiles.delete' => 'Изтрий',
|
||||||
'profiles.signOut' => 'Изход',
|
|
||||||
'profiles.signOutPlexTitle' => 'Изход от Plex?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Изход',
|
||||||
|
'profiles.signOutPlexTitle' => 'Изход от Plex?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Премахване на ${displayName} и всички Plex Home потребители? Можете да влезете отново по всяко време.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Премахване на ${displayName} и всички Plex Home потребители? Можете да влезете отново по всяко време.',
|
||||||
'profiles.signedOutPlex' => 'Излязохте от Plex.',
|
'profiles.signedOutPlex' => 'Излязохте от Plex.',
|
||||||
'profiles.signOutFailed' => 'Изходът е неуспешен.',
|
'profiles.signOutFailed' => 'Изходът е неуспешен.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsBg {
|
|||||||
'metadataEdit.originalTitle' => 'Оригинално заглавие',
|
'metadataEdit.originalTitle' => 'Оригинално заглавие',
|
||||||
'metadataEdit.releaseDate' => 'Дата на излизане',
|
'metadataEdit.releaseDate' => 'Дата на излизане',
|
||||||
'metadataEdit.contentRating' => 'Възрастов рейтинг',
|
'metadataEdit.contentRating' => 'Възрастов рейтинг',
|
||||||
'metadataEdit.studio' => 'Студио',
|
|
||||||
'metadataEdit.tagline' => 'Слоган',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Студио',
|
||||||
|
'metadataEdit.tagline' => 'Слоган',
|
||||||
'metadataEdit.summary' => 'Резюме',
|
'metadataEdit.summary' => 'Резюме',
|
||||||
'metadataEdit.poster' => 'Постер',
|
'metadataEdit.poster' => 'Постер',
|
||||||
'metadataEdit.background' => 'Фон',
|
'metadataEdit.background' => 'Фон',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsDa extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Vis sæson- og episodenummer på episodekort';
|
@override String get showEpisodeNumberOnCardsDescription => 'Vis sæson- og episodenummer på episodekort';
|
||||||
@override String get showSeasonPostersOnTabs => 'Vis sæsonplakater på faner';
|
@override String get showSeasonPostersOnTabs => 'Vis sæsonplakater på faner';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Vis hver sæsons plakat over dens fane';
|
@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 hideSpoilers => 'Skjul spoilere for usete episoder';
|
@override String get hideSpoilers => 'Skjul spoilere for usete episoder';
|
||||||
@override String get hideSpoilersDescription => 'Slør miniaturebilleder og beskrivelser for usete episoder';
|
@override String get hideSpoilersDescription => 'Slør miniaturebilleder og beskrivelser for usete episoder';
|
||||||
@override String get playerBackend => 'Afspillerbackend';
|
@override String get playerBackend => 'Afspillerbackend';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsDa {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Vis sæson- og episodenummer på episodekort',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Vis sæson- og episodenummer på episodekort',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Vis sæsonplakater på faner',
|
'settings.showSeasonPostersOnTabs' => 'Vis sæsonplakater på faner',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Vis hver sæsons plakat over dens fane',
|
'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.hideSpoilers' => 'Skjul spoilere for usete episoder',
|
'settings.hideSpoilers' => 'Skjul spoilere for usete episoder',
|
||||||
'settings.hideSpoilersDescription' => 'Slør miniaturebilleder og beskrivelser for usete episoder',
|
'settings.hideSpoilersDescription' => 'Slør miniaturebilleder og beskrivelser for usete episoder',
|
||||||
'settings.playerBackend' => 'Afspillerbackend',
|
'settings.playerBackend' => 'Afspillerbackend',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsDa {
|
|||||||
'profiles.active' => 'Aktiv',
|
'profiles.active' => 'Aktiv',
|
||||||
'profiles.manage' => 'Administrer',
|
'profiles.manage' => 'Administrer',
|
||||||
'profiles.delete' => 'Slet',
|
'profiles.delete' => 'Slet',
|
||||||
'profiles.signOut' => 'Log ud',
|
|
||||||
'profiles.signOutPlexTitle' => 'Log ud af Plex?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Log ud',
|
||||||
|
'profiles.signOutPlexTitle' => 'Log ud af Plex?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Fjern ${displayName} og alle Plex Home-brugere? Log ind igen når som helst.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Fjern ${displayName} og alle Plex Home-brugere? Log ind igen når som helst.',
|
||||||
'profiles.signedOutPlex' => 'Logget ud af Plex.',
|
'profiles.signedOutPlex' => 'Logget ud af Plex.',
|
||||||
'profiles.signOutFailed' => 'Log ud mislykkedes.',
|
'profiles.signOutFailed' => 'Log ud mislykkedes.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsDa {
|
|||||||
'metadataEdit.originalTitle' => 'Originaltitel',
|
'metadataEdit.originalTitle' => 'Originaltitel',
|
||||||
'metadataEdit.releaseDate' => 'Udgivelsesdato',
|
'metadataEdit.releaseDate' => 'Udgivelsesdato',
|
||||||
'metadataEdit.contentRating' => 'Aldersgrænse',
|
'metadataEdit.contentRating' => 'Aldersgrænse',
|
||||||
'metadataEdit.studio' => 'Studie',
|
|
||||||
'metadataEdit.tagline' => 'Tagline',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Studie',
|
||||||
|
'metadataEdit.tagline' => 'Tagline',
|
||||||
'metadataEdit.summary' => 'Resumé',
|
'metadataEdit.summary' => 'Resumé',
|
||||||
'metadataEdit.poster' => 'Plakat',
|
'metadataEdit.poster' => 'Plakat',
|
||||||
'metadataEdit.background' => 'Baggrund',
|
'metadataEdit.background' => 'Baggrund',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsDe extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Staffel- und Episodennummer auf Episodenkarten anzeigen';
|
@override String get showEpisodeNumberOnCardsDescription => 'Staffel- und Episodennummer auf Episodenkarten anzeigen';
|
||||||
@override String get showSeasonPostersOnTabs => 'Staffelposter auf Tabs anzeigen';
|
@override String get showSeasonPostersOnTabs => 'Staffelposter auf Tabs anzeigen';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Poster jeder Staffel über ihrem Tab anzeigen';
|
@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 hideSpoilers => 'Spoiler für nicht gesehene Episoden verbergen';
|
@override String get hideSpoilers => 'Spoiler für nicht gesehene Episoden verbergen';
|
||||||
@override String get hideSpoilersDescription => 'Vorschaubilder und Beschreibungen ungesehener Episoden verwischen';
|
@override String get hideSpoilersDescription => 'Vorschaubilder und Beschreibungen ungesehener Episoden verwischen';
|
||||||
@override String get playerBackend => 'Player-Backend';
|
@override String get playerBackend => 'Player-Backend';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsDe {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Staffel- und Episodennummer auf Episodenkarten anzeigen',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Staffel- und Episodennummer auf Episodenkarten anzeigen',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Staffelposter auf Tabs anzeigen',
|
'settings.showSeasonPostersOnTabs' => 'Staffelposter auf Tabs anzeigen',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Poster jeder Staffel über ihrem Tab anzeigen',
|
'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.hideSpoilers' => 'Spoiler für nicht gesehene Episoden verbergen',
|
'settings.hideSpoilers' => 'Spoiler für nicht gesehene Episoden verbergen',
|
||||||
'settings.hideSpoilersDescription' => 'Vorschaubilder und Beschreibungen ungesehener Episoden verwischen',
|
'settings.hideSpoilersDescription' => 'Vorschaubilder und Beschreibungen ungesehener Episoden verwischen',
|
||||||
'settings.playerBackend' => 'Player-Backend',
|
'settings.playerBackend' => 'Player-Backend',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsDe {
|
|||||||
'profiles.active' => 'Aktiv',
|
'profiles.active' => 'Aktiv',
|
||||||
'profiles.manage' => 'Verwalten',
|
'profiles.manage' => 'Verwalten',
|
||||||
'profiles.delete' => 'Löschen',
|
'profiles.delete' => 'Löschen',
|
||||||
'profiles.signOut' => 'Abmelden',
|
|
||||||
'profiles.signOutPlexTitle' => 'Von Plex abmelden?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Abmelden',
|
||||||
|
'profiles.signOutPlexTitle' => 'Von Plex abmelden?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} und alle Plex Home-Benutzer entfernen? Du kannst dich jederzeit wieder anmelden.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} und alle Plex Home-Benutzer entfernen? Du kannst dich jederzeit wieder anmelden.',
|
||||||
'profiles.signedOutPlex' => 'Von Plex abgemeldet.',
|
'profiles.signedOutPlex' => 'Von Plex abgemeldet.',
|
||||||
'profiles.signOutFailed' => 'Abmeldung fehlgeschlagen.',
|
'profiles.signOutFailed' => 'Abmeldung fehlgeschlagen.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsDe {
|
|||||||
'metadataEdit.originalTitle' => 'Originaltitel',
|
'metadataEdit.originalTitle' => 'Originaltitel',
|
||||||
'metadataEdit.releaseDate' => 'Erscheinungsdatum',
|
'metadataEdit.releaseDate' => 'Erscheinungsdatum',
|
||||||
'metadataEdit.contentRating' => 'Altersfreigabe',
|
'metadataEdit.contentRating' => 'Altersfreigabe',
|
||||||
'metadataEdit.studio' => 'Studio',
|
|
||||||
'metadataEdit.tagline' => 'Tagline',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Studio',
|
||||||
|
'metadataEdit.tagline' => 'Tagline',
|
||||||
'metadataEdit.summary' => 'Zusammenfassung',
|
'metadataEdit.summary' => 'Zusammenfassung',
|
||||||
'metadataEdit.poster' => 'Poster',
|
'metadataEdit.poster' => 'Poster',
|
||||||
'metadataEdit.background' => 'Hintergrund',
|
'metadataEdit.background' => 'Hintergrund',
|
||||||
|
|||||||
@@ -488,6 +488,12 @@ class TranslationsSettingsEn {
|
|||||||
/// en: 'Show each season's poster above its tab'
|
/// en: 'Show each season's poster above its tab'
|
||||||
String get showSeasonPostersOnTabsDescription => 'Show each season\'s poster above its tab';
|
String get showSeasonPostersOnTabsDescription => 'Show each season\'s poster above its tab';
|
||||||
|
|
||||||
|
/// en: 'Full TV Cards'
|
||||||
|
String get tvFullCardLayout => 'Full TV Cards';
|
||||||
|
|
||||||
|
/// en: 'Use image-only TV cards with actor names overlaid'
|
||||||
|
String get tvFullCardLayoutDescription => 'Use image-only TV cards with actor names overlaid';
|
||||||
|
|
||||||
/// en: 'Hide Spoilers for Unwatched Episodes'
|
/// en: 'Hide Spoilers for Unwatched Episodes'
|
||||||
String get hideSpoilers => 'Hide Spoilers for Unwatched Episodes';
|
String get hideSpoilers => 'Hide Spoilers for Unwatched Episodes';
|
||||||
|
|
||||||
@@ -4282,6 +4288,8 @@ extension on Translations {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Show season and episode number on episode cards',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Show season and episode number on episode cards',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Show Season Posters on Tabs',
|
'settings.showSeasonPostersOnTabs' => 'Show Season Posters on Tabs',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Show each season\'s poster above its tab',
|
'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.hideSpoilers' => 'Hide Spoilers for Unwatched Episodes',
|
'settings.hideSpoilers' => 'Hide Spoilers for Unwatched Episodes',
|
||||||
'settings.hideSpoilersDescription' => 'Blur thumbnails and descriptions for unwatched episodes',
|
'settings.hideSpoilersDescription' => 'Blur thumbnails and descriptions for unwatched episodes',
|
||||||
'settings.playerBackend' => 'Player Backend',
|
'settings.playerBackend' => 'Player Backend',
|
||||||
@@ -4676,10 +4684,10 @@ extension on Translations {
|
|||||||
'profiles.active' => 'Active',
|
'profiles.active' => 'Active',
|
||||||
'profiles.manage' => 'Manage',
|
'profiles.manage' => 'Manage',
|
||||||
'profiles.delete' => 'Delete',
|
'profiles.delete' => 'Delete',
|
||||||
'profiles.signOut' => 'Sign out',
|
|
||||||
'profiles.signOutPlexTitle' => 'Sign out of Plex?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Sign out',
|
||||||
|
'profiles.signOutPlexTitle' => 'Sign out of Plex?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Remove ${displayName} and all Plex Home users? Sign back in anytime.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Remove ${displayName} and all Plex Home users? Sign back in anytime.',
|
||||||
'profiles.signedOutPlex' => 'Signed out of Plex.',
|
'profiles.signedOutPlex' => 'Signed out of Plex.',
|
||||||
'profiles.signOutFailed' => 'Sign out failed.',
|
'profiles.signOutFailed' => 'Sign out failed.',
|
||||||
@@ -5190,10 +5198,10 @@ extension on Translations {
|
|||||||
'metadataEdit.originalTitle' => 'Original Title',
|
'metadataEdit.originalTitle' => 'Original Title',
|
||||||
'metadataEdit.releaseDate' => 'Release Date',
|
'metadataEdit.releaseDate' => 'Release Date',
|
||||||
'metadataEdit.contentRating' => 'Content Rating',
|
'metadataEdit.contentRating' => 'Content Rating',
|
||||||
'metadataEdit.studio' => 'Studio',
|
|
||||||
'metadataEdit.tagline' => 'Tagline',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Studio',
|
||||||
|
'metadataEdit.tagline' => 'Tagline',
|
||||||
'metadataEdit.summary' => 'Summary',
|
'metadataEdit.summary' => 'Summary',
|
||||||
'metadataEdit.poster' => 'Poster',
|
'metadataEdit.poster' => 'Poster',
|
||||||
'metadataEdit.background' => 'Background',
|
'metadataEdit.background' => 'Background',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsEs extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Mostrar temporada y episodio en tarjetas de episodio';
|
@override String get showEpisodeNumberOnCardsDescription => 'Mostrar temporada y episodio en tarjetas de episodio';
|
||||||
@override String get showSeasonPostersOnTabs => 'Mostrar pósters de temporada en las pestañas';
|
@override String get showSeasonPostersOnTabs => 'Mostrar pósters de temporada en las pestañas';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Mostrar el póster de cada temporada sobre su pestaña';
|
@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 hideSpoilers => 'Ocultar spoilers de episodios no vistos';
|
@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 hideSpoilersDescription => 'Desenfocar miniaturas y descripciones de episodios no vistos';
|
||||||
@override String get playerBackend => 'Reproductor';
|
@override String get playerBackend => 'Reproductor';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsEs {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Mostrar temporada y episodio en tarjetas de episodio',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Mostrar temporada y episodio en tarjetas de episodio',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Mostrar pósters de temporada en las pestañas',
|
'settings.showSeasonPostersOnTabs' => 'Mostrar pósters de temporada en las pestañas',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Mostrar el póster de cada temporada sobre su pestaña',
|
'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.hideSpoilers' => 'Ocultar spoilers de episodios no vistos',
|
'settings.hideSpoilers' => 'Ocultar spoilers de episodios no vistos',
|
||||||
'settings.hideSpoilersDescription' => 'Desenfocar miniaturas y descripciones de episodios no vistos',
|
'settings.hideSpoilersDescription' => 'Desenfocar miniaturas y descripciones de episodios no vistos',
|
||||||
'settings.playerBackend' => 'Reproductor',
|
'settings.playerBackend' => 'Reproductor',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsEs {
|
|||||||
'profiles.active' => 'Activo',
|
'profiles.active' => 'Activo',
|
||||||
'profiles.manage' => 'Administrar',
|
'profiles.manage' => 'Administrar',
|
||||||
'profiles.delete' => 'Eliminar',
|
'profiles.delete' => 'Eliminar',
|
||||||
'profiles.signOut' => 'Cerrar sesión',
|
|
||||||
'profiles.signOutPlexTitle' => '¿Cerrar sesión de Plex?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Cerrar sesión',
|
||||||
|
'profiles.signOutPlexTitle' => '¿Cerrar sesión de Plex?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => '¿Eliminar ${displayName} y todos los usuarios de Plex Home? Puedes iniciar sesión de nuevo cuando quieras.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => '¿Eliminar ${displayName} y todos los usuarios de Plex Home? Puedes iniciar sesión de nuevo cuando quieras.',
|
||||||
'profiles.signedOutPlex' => 'Sesión de Plex cerrada.',
|
'profiles.signedOutPlex' => 'Sesión de Plex cerrada.',
|
||||||
'profiles.signOutFailed' => 'Error al cerrar sesión.',
|
'profiles.signOutFailed' => 'Error al cerrar sesión.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsEs {
|
|||||||
'metadataEdit.originalTitle' => 'Título original',
|
'metadataEdit.originalTitle' => 'Título original',
|
||||||
'metadataEdit.releaseDate' => 'Fecha de estreno',
|
'metadataEdit.releaseDate' => 'Fecha de estreno',
|
||||||
'metadataEdit.contentRating' => 'Clasificación de contenido',
|
'metadataEdit.contentRating' => 'Clasificación de contenido',
|
||||||
'metadataEdit.studio' => 'Estudio',
|
|
||||||
'metadataEdit.tagline' => 'Eslogan',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Estudio',
|
||||||
|
'metadataEdit.tagline' => 'Eslogan',
|
||||||
'metadataEdit.summary' => 'Resumen',
|
'metadataEdit.summary' => 'Resumen',
|
||||||
'metadataEdit.poster' => 'Póster',
|
'metadataEdit.poster' => 'Póster',
|
||||||
'metadataEdit.background' => 'Fondo',
|
'metadataEdit.background' => 'Fondo',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsFr extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Afficher la saison et l\'épisode sur les cartes d\'épisode';
|
@override String get showEpisodeNumberOnCardsDescription => 'Afficher la saison et l\'épisode sur les cartes d\'épisode';
|
||||||
@override String get showSeasonPostersOnTabs => 'Afficher les posters de saison sur les onglets';
|
@override String get showSeasonPostersOnTabs => 'Afficher les posters de saison sur les onglets';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Afficher l\'affiche de chaque saison au-dessus de son onglet';
|
@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 hideSpoilers => 'Masquer les spoilers des épisodes non vus';
|
@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 hideSpoilersDescription => 'Flouter les miniatures et descriptions des épisodes non vus';
|
||||||
@override String get playerBackend => 'Moteur de lecture';
|
@override String get playerBackend => 'Moteur de lecture';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsFr {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Afficher la saison et l\'épisode sur les cartes d\'épisode',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Afficher la saison et l\'épisode sur les cartes d\'épisode',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Afficher les posters de saison sur les onglets',
|
'settings.showSeasonPostersOnTabs' => 'Afficher les posters de saison sur les onglets',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Afficher l\'affiche de chaque saison au-dessus de son onglet',
|
'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.hideSpoilers' => 'Masquer les spoilers des épisodes non vus',
|
'settings.hideSpoilers' => 'Masquer les spoilers des épisodes non vus',
|
||||||
'settings.hideSpoilersDescription' => 'Flouter les miniatures et descriptions des épisodes non vus',
|
'settings.hideSpoilersDescription' => 'Flouter les miniatures et descriptions des épisodes non vus',
|
||||||
'settings.playerBackend' => 'Moteur de lecture',
|
'settings.playerBackend' => 'Moteur de lecture',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsFr {
|
|||||||
'profiles.active' => 'Actif',
|
'profiles.active' => 'Actif',
|
||||||
'profiles.manage' => 'Gérer',
|
'profiles.manage' => 'Gérer',
|
||||||
'profiles.delete' => 'Supprimer',
|
'profiles.delete' => 'Supprimer',
|
||||||
'profiles.signOut' => 'Se déconnecter',
|
|
||||||
'profiles.signOutPlexTitle' => 'Se déconnecter de Plex ?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Se déconnecter',
|
||||||
|
'profiles.signOutPlexTitle' => 'Se déconnecter de Plex ?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Supprimer ${displayName} et tous les utilisateurs Plex Home ? Reconnexion possible à tout moment.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Supprimer ${displayName} et tous les utilisateurs Plex Home ? Reconnexion possible à tout moment.',
|
||||||
'profiles.signedOutPlex' => 'Déconnecté de Plex.',
|
'profiles.signedOutPlex' => 'Déconnecté de Plex.',
|
||||||
'profiles.signOutFailed' => 'Échec de la déconnexion.',
|
'profiles.signOutFailed' => 'Échec de la déconnexion.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsFr {
|
|||||||
'metadataEdit.originalTitle' => 'Titre original',
|
'metadataEdit.originalTitle' => 'Titre original',
|
||||||
'metadataEdit.releaseDate' => 'Date de sortie',
|
'metadataEdit.releaseDate' => 'Date de sortie',
|
||||||
'metadataEdit.contentRating' => 'Classification',
|
'metadataEdit.contentRating' => 'Classification',
|
||||||
'metadataEdit.studio' => 'Studio',
|
|
||||||
'metadataEdit.tagline' => 'Slogan',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Studio',
|
||||||
|
'metadataEdit.tagline' => 'Slogan',
|
||||||
'metadataEdit.summary' => 'Résumé',
|
'metadataEdit.summary' => 'Résumé',
|
||||||
'metadataEdit.poster' => 'Affiche',
|
'metadataEdit.poster' => 'Affiche',
|
||||||
'metadataEdit.background' => 'Arrière-plan',
|
'metadataEdit.background' => 'Arrière-plan',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsIt extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Mostra stagione ed episodio sulle schede episodio';
|
@override String get showEpisodeNumberOnCardsDescription => 'Mostra stagione ed episodio sulle schede episodio';
|
||||||
@override String get showSeasonPostersOnTabs => 'Mostra poster delle stagioni sulle schede';
|
@override String get showSeasonPostersOnTabs => 'Mostra poster delle stagioni sulle schede';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Mostra il poster di ogni stagione sopra la sua scheda';
|
@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 hideSpoilers => 'Nascondi spoiler per episodi non visti';
|
@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 hideSpoilersDescription => 'Sfoca miniature e descrizioni degli episodi non visti';
|
||||||
@override String get playerBackend => 'Motore di riproduzione';
|
@override String get playerBackend => 'Motore di riproduzione';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsIt {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Mostra stagione ed episodio sulle schede episodio',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Mostra stagione ed episodio sulle schede episodio',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Mostra poster delle stagioni sulle schede',
|
'settings.showSeasonPostersOnTabs' => 'Mostra poster delle stagioni sulle schede',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Mostra il poster di ogni stagione sopra la sua scheda',
|
'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.hideSpoilers' => 'Nascondi spoiler per episodi non visti',
|
'settings.hideSpoilers' => 'Nascondi spoiler per episodi non visti',
|
||||||
'settings.hideSpoilersDescription' => 'Sfoca miniature e descrizioni degli episodi non visti',
|
'settings.hideSpoilersDescription' => 'Sfoca miniature e descrizioni degli episodi non visti',
|
||||||
'settings.playerBackend' => 'Motore di riproduzione',
|
'settings.playerBackend' => 'Motore di riproduzione',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsIt {
|
|||||||
'profiles.active' => 'Attivo',
|
'profiles.active' => 'Attivo',
|
||||||
'profiles.manage' => 'Gestisci',
|
'profiles.manage' => 'Gestisci',
|
||||||
'profiles.delete' => 'Elimina',
|
'profiles.delete' => 'Elimina',
|
||||||
'profiles.signOut' => 'Esci',
|
|
||||||
'profiles.signOutPlexTitle' => 'Uscire da Plex?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Esci',
|
||||||
|
'profiles.signOutPlexTitle' => 'Uscire da Plex?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Rimuovere ${displayName} e tutti gli utenti Plex Home? Puoi accedere di nuovo quando vuoi.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Rimuovere ${displayName} e tutti gli utenti Plex Home? Puoi accedere di nuovo quando vuoi.',
|
||||||
'profiles.signedOutPlex' => 'Uscito da Plex.',
|
'profiles.signedOutPlex' => 'Uscito da Plex.',
|
||||||
'profiles.signOutFailed' => 'Uscita non riuscita.',
|
'profiles.signOutFailed' => 'Uscita non riuscita.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsIt {
|
|||||||
'metadataEdit.originalTitle' => 'Titolo originale',
|
'metadataEdit.originalTitle' => 'Titolo originale',
|
||||||
'metadataEdit.releaseDate' => 'Data di uscita',
|
'metadataEdit.releaseDate' => 'Data di uscita',
|
||||||
'metadataEdit.contentRating' => 'Classificazione',
|
'metadataEdit.contentRating' => 'Classificazione',
|
||||||
'metadataEdit.studio' => 'Studio',
|
|
||||||
'metadataEdit.tagline' => 'Tagline',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Studio',
|
||||||
|
'metadataEdit.tagline' => 'Tagline',
|
||||||
'metadataEdit.summary' => 'Trama',
|
'metadataEdit.summary' => 'Trama',
|
||||||
'metadataEdit.poster' => 'Poster',
|
'metadataEdit.poster' => 'Poster',
|
||||||
'metadataEdit.background' => 'Sfondo',
|
'metadataEdit.background' => 'Sfondo',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsJa extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'エピソードカードにシーズン番号とエピソード番号を表示します';
|
@override String get showEpisodeNumberOnCardsDescription => 'エピソードカードにシーズン番号とエピソード番号を表示します';
|
||||||
@override String get showSeasonPostersOnTabs => 'タブにシーズンポスターを表示';
|
@override String get showSeasonPostersOnTabs => 'タブにシーズンポスターを表示';
|
||||||
@override String get showSeasonPostersOnTabsDescription => '各シーズンのポスターをタブの上に表示します';
|
@override String get showSeasonPostersOnTabsDescription => '各シーズンのポスターをタブの上に表示します';
|
||||||
|
@override String get tvFullCardLayout => 'フルTVカード';
|
||||||
|
@override String get tvFullCardLayoutDescription => 'TVカードを画像のみで表示し、俳優名を重ねて表示します';
|
||||||
@override String get hideSpoilers => '未視聴エピソードのネタバレを非表示';
|
@override String get hideSpoilers => '未視聴エピソードのネタバレを非表示';
|
||||||
@override String get hideSpoilersDescription => '未視聴エピソードのサムネイルと説明をぼかします';
|
@override String get hideSpoilersDescription => '未視聴エピソードのサムネイルと説明をぼかします';
|
||||||
@override String get playerBackend => 'プレーヤーバックエンド';
|
@override String get playerBackend => 'プレーヤーバックエンド';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsJa {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'エピソードカードにシーズン番号とエピソード番号を表示します',
|
'settings.showEpisodeNumberOnCardsDescription' => 'エピソードカードにシーズン番号とエピソード番号を表示します',
|
||||||
'settings.showSeasonPostersOnTabs' => 'タブにシーズンポスターを表示',
|
'settings.showSeasonPostersOnTabs' => 'タブにシーズンポスターを表示',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => '各シーズンのポスターをタブの上に表示します',
|
'settings.showSeasonPostersOnTabsDescription' => '各シーズンのポスターをタブの上に表示します',
|
||||||
|
'settings.tvFullCardLayout' => 'フルTVカード',
|
||||||
|
'settings.tvFullCardLayoutDescription' => 'TVカードを画像のみで表示し、俳優名を重ねて表示します',
|
||||||
'settings.hideSpoilers' => '未視聴エピソードのネタバレを非表示',
|
'settings.hideSpoilers' => '未視聴エピソードのネタバレを非表示',
|
||||||
'settings.hideSpoilersDescription' => '未視聴エピソードのサムネイルと説明をぼかします',
|
'settings.hideSpoilersDescription' => '未視聴エピソードのサムネイルと説明をぼかします',
|
||||||
'settings.playerBackend' => 'プレーヤーバックエンド',
|
'settings.playerBackend' => 'プレーヤーバックエンド',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsJa {
|
|||||||
'profiles.active' => 'アクティブ',
|
'profiles.active' => 'アクティブ',
|
||||||
'profiles.manage' => '管理',
|
'profiles.manage' => '管理',
|
||||||
'profiles.delete' => '削除',
|
'profiles.delete' => '削除',
|
||||||
'profiles.signOut' => 'サインアウト',
|
|
||||||
'profiles.signOutPlexTitle' => 'Plex からサインアウトしますか?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'サインアウト',
|
||||||
|
'profiles.signOutPlexTitle' => 'Plex からサインアウトしますか?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName}とすべてのPlex Homeユーザーを削除しますか?いつでも再サインインできます。',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName}とすべてのPlex Homeユーザーを削除しますか?いつでも再サインインできます。',
|
||||||
'profiles.signedOutPlex' => 'Plex からサインアウトしました。',
|
'profiles.signedOutPlex' => 'Plex からサインアウトしました。',
|
||||||
'profiles.signOutFailed' => 'サインアウトに失敗しました。',
|
'profiles.signOutFailed' => 'サインアウトに失敗しました。',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsJa {
|
|||||||
'metadataEdit.originalTitle' => '原題',
|
'metadataEdit.originalTitle' => '原題',
|
||||||
'metadataEdit.releaseDate' => '公開日',
|
'metadataEdit.releaseDate' => '公開日',
|
||||||
'metadataEdit.contentRating' => 'コンテンツレーティング',
|
'metadataEdit.contentRating' => 'コンテンツレーティング',
|
||||||
'metadataEdit.studio' => 'スタジオ',
|
|
||||||
'metadataEdit.tagline' => 'タグライン',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'スタジオ',
|
||||||
|
'metadataEdit.tagline' => 'タグライン',
|
||||||
'metadataEdit.summary' => 'あらすじ',
|
'metadataEdit.summary' => 'あらすじ',
|
||||||
'metadataEdit.poster' => 'ポスター',
|
'metadataEdit.poster' => 'ポスター',
|
||||||
'metadataEdit.background' => '背景',
|
'metadataEdit.background' => '背景',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsKo extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => '에피소드 카드에 시즌 및 에피소드 번호 표시';
|
@override String get showEpisodeNumberOnCardsDescription => '에피소드 카드에 시즌 및 에피소드 번호 표시';
|
||||||
@override String get showSeasonPostersOnTabs => '탭에 시즌 포스터 표시';
|
@override String get showSeasonPostersOnTabs => '탭에 시즌 포스터 표시';
|
||||||
@override String get showSeasonPostersOnTabsDescription => '각 시즌 포스터를 탭 위에 표시';
|
@override String get showSeasonPostersOnTabsDescription => '각 시즌 포스터를 탭 위에 표시';
|
||||||
|
@override String get tvFullCardLayout => '전체 TV 카드';
|
||||||
|
@override String get tvFullCardLayoutDescription => 'TV 카드에 이미지만 표시하고 배우 이름을 오버레이로 표시';
|
||||||
@override String get hideSpoilers => '미시청 에피소드 스포일러 숨기기';
|
@override String get hideSpoilers => '미시청 에피소드 스포일러 숨기기';
|
||||||
@override String get hideSpoilersDescription => '시청하지 않은 에피소드의 썸네일과 설명을 흐리게 처리';
|
@override String get hideSpoilersDescription => '시청하지 않은 에피소드의 썸네일과 설명을 흐리게 처리';
|
||||||
@override String get playerBackend => '플레이어 백엔드';
|
@override String get playerBackend => '플레이어 백엔드';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsKo {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => '에피소드 카드에 시즌 및 에피소드 번호 표시',
|
'settings.showEpisodeNumberOnCardsDescription' => '에피소드 카드에 시즌 및 에피소드 번호 표시',
|
||||||
'settings.showSeasonPostersOnTabs' => '탭에 시즌 포스터 표시',
|
'settings.showSeasonPostersOnTabs' => '탭에 시즌 포스터 표시',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => '각 시즌 포스터를 탭 위에 표시',
|
'settings.showSeasonPostersOnTabsDescription' => '각 시즌 포스터를 탭 위에 표시',
|
||||||
|
'settings.tvFullCardLayout' => '전체 TV 카드',
|
||||||
|
'settings.tvFullCardLayoutDescription' => 'TV 카드에 이미지만 표시하고 배우 이름을 오버레이로 표시',
|
||||||
'settings.hideSpoilers' => '미시청 에피소드 스포일러 숨기기',
|
'settings.hideSpoilers' => '미시청 에피소드 스포일러 숨기기',
|
||||||
'settings.hideSpoilersDescription' => '시청하지 않은 에피소드의 썸네일과 설명을 흐리게 처리',
|
'settings.hideSpoilersDescription' => '시청하지 않은 에피소드의 썸네일과 설명을 흐리게 처리',
|
||||||
'settings.playerBackend' => '플레이어 백엔드',
|
'settings.playerBackend' => '플레이어 백엔드',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsKo {
|
|||||||
'profiles.active' => '활성',
|
'profiles.active' => '활성',
|
||||||
'profiles.manage' => '관리',
|
'profiles.manage' => '관리',
|
||||||
'profiles.delete' => '삭제',
|
'profiles.delete' => '삭제',
|
||||||
'profiles.signOut' => '로그아웃',
|
|
||||||
'profiles.signOutPlexTitle' => 'Plex에서 로그아웃하시겠습니까?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => '로그아웃',
|
||||||
|
'profiles.signOutPlexTitle' => 'Plex에서 로그아웃하시겠습니까?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} 및 모든 Plex Home 사용자를 제거할까요? 언제든 다시 로그인할 수 있습니다.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} 및 모든 Plex Home 사용자를 제거할까요? 언제든 다시 로그인할 수 있습니다.',
|
||||||
'profiles.signedOutPlex' => 'Plex에서 로그아웃되었습니다.',
|
'profiles.signedOutPlex' => 'Plex에서 로그아웃되었습니다.',
|
||||||
'profiles.signOutFailed' => '로그아웃에 실패했습니다.',
|
'profiles.signOutFailed' => '로그아웃에 실패했습니다.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsKo {
|
|||||||
'metadataEdit.originalTitle' => '원제',
|
'metadataEdit.originalTitle' => '원제',
|
||||||
'metadataEdit.releaseDate' => '출시일',
|
'metadataEdit.releaseDate' => '출시일',
|
||||||
'metadataEdit.contentRating' => '콘텐츠 등급',
|
'metadataEdit.contentRating' => '콘텐츠 등급',
|
||||||
'metadataEdit.studio' => '스튜디오',
|
|
||||||
'metadataEdit.tagline' => '태그라인',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => '스튜디오',
|
||||||
|
'metadataEdit.tagline' => '태그라인',
|
||||||
'metadataEdit.summary' => '줄거리',
|
'metadataEdit.summary' => '줄거리',
|
||||||
'metadataEdit.poster' => '포스터',
|
'metadataEdit.poster' => '포스터',
|
||||||
'metadataEdit.background' => '배경',
|
'metadataEdit.background' => '배경',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsNb extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Vis sesong- og episodenummer på episodekort';
|
@override String get showEpisodeNumberOnCardsDescription => 'Vis sesong- og episodenummer på episodekort';
|
||||||
@override String get showSeasonPostersOnTabs => 'Vis sesongplakater på faner';
|
@override String get showSeasonPostersOnTabs => 'Vis sesongplakater på faner';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Vis hver sesongs plakat over fanen';
|
@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 hideSpoilers => 'Skjul spoilere for usette episoder';
|
@override String get hideSpoilers => 'Skjul spoilere for usette episoder';
|
||||||
@override String get hideSpoilersDescription => 'Slør miniatyrbilder og beskrivelser for usette episoder';
|
@override String get hideSpoilersDescription => 'Slør miniatyrbilder og beskrivelser for usette episoder';
|
||||||
@override String get playerBackend => 'Spillermotor';
|
@override String get playerBackend => 'Spillermotor';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsNb {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Vis sesong- og episodenummer på episodekort',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Vis sesong- og episodenummer på episodekort',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Vis sesongplakater på faner',
|
'settings.showSeasonPostersOnTabs' => 'Vis sesongplakater på faner',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Vis hver sesongs plakat over fanen',
|
'settings.showSeasonPostersOnTabsDescription' => 'Vis hver sesongs plakat over fanen',
|
||||||
|
'settings.tvFullCardLayout' => 'Fulle TV-kort',
|
||||||
|
'settings.tvFullCardLayoutDescription' => 'Bruk bildebaserte TV-kort med skuespillernavn lagt over',
|
||||||
'settings.hideSpoilers' => 'Skjul spoilere for usette episoder',
|
'settings.hideSpoilers' => 'Skjul spoilere for usette episoder',
|
||||||
'settings.hideSpoilersDescription' => 'Slør miniatyrbilder og beskrivelser for usette episoder',
|
'settings.hideSpoilersDescription' => 'Slør miniatyrbilder og beskrivelser for usette episoder',
|
||||||
'settings.playerBackend' => 'Spillermotor',
|
'settings.playerBackend' => 'Spillermotor',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsNb {
|
|||||||
'profiles.active' => 'Aktiv',
|
'profiles.active' => 'Aktiv',
|
||||||
'profiles.manage' => 'Administrer',
|
'profiles.manage' => 'Administrer',
|
||||||
'profiles.delete' => 'Slett',
|
'profiles.delete' => 'Slett',
|
||||||
'profiles.signOut' => 'Logg ut',
|
|
||||||
'profiles.signOutPlexTitle' => 'Logge ut av Plex?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Logg ut',
|
||||||
|
'profiles.signOutPlexTitle' => 'Logge ut av Plex?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Fjerne ${displayName} og alle Plex Home-brukere? Du kan logge inn igjen når som helst.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Fjerne ${displayName} og alle Plex Home-brukere? Du kan logge inn igjen når som helst.',
|
||||||
'profiles.signedOutPlex' => 'Logget ut av Plex.',
|
'profiles.signedOutPlex' => 'Logget ut av Plex.',
|
||||||
'profiles.signOutFailed' => 'Utlogging mislyktes.',
|
'profiles.signOutFailed' => 'Utlogging mislyktes.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsNb {
|
|||||||
'metadataEdit.originalTitle' => 'Originaltittel',
|
'metadataEdit.originalTitle' => 'Originaltittel',
|
||||||
'metadataEdit.releaseDate' => 'Utgivelsesdato',
|
'metadataEdit.releaseDate' => 'Utgivelsesdato',
|
||||||
'metadataEdit.contentRating' => 'Innholdsvurdering',
|
'metadataEdit.contentRating' => 'Innholdsvurdering',
|
||||||
'metadataEdit.studio' => 'Studio',
|
|
||||||
'metadataEdit.tagline' => 'Slagord',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Studio',
|
||||||
|
'metadataEdit.tagline' => 'Slagord',
|
||||||
'metadataEdit.summary' => 'Sammendrag',
|
'metadataEdit.summary' => 'Sammendrag',
|
||||||
'metadataEdit.poster' => 'Plakat',
|
'metadataEdit.poster' => 'Plakat',
|
||||||
'metadataEdit.background' => 'Bakgrunn',
|
'metadataEdit.background' => 'Bakgrunn',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsNl extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Toon seizoen- en afleveringsnummer op afleveringskaarten';
|
@override String get showEpisodeNumberOnCardsDescription => 'Toon seizoen- en afleveringsnummer op afleveringskaarten';
|
||||||
@override String get showSeasonPostersOnTabs => 'Toon seizoensposters op tabbladen';
|
@override String get showSeasonPostersOnTabs => 'Toon seizoensposters op tabbladen';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Toon de poster van elk seizoen boven het tabblad';
|
@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 hideSpoilers => 'Spoilers voor ongekeken afleveringen verbergen';
|
@override String get hideSpoilers => 'Spoilers voor ongekeken afleveringen verbergen';
|
||||||
@override String get hideSpoilersDescription => 'Vervaag miniaturen en beschrijvingen voor niet-bekeken afleveringen';
|
@override String get hideSpoilersDescription => 'Vervaag miniaturen en beschrijvingen voor niet-bekeken afleveringen';
|
||||||
@override String get playerBackend => 'Speler backend';
|
@override String get playerBackend => 'Speler backend';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsNl {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Toon seizoen- en afleveringsnummer op afleveringskaarten',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Toon seizoen- en afleveringsnummer op afleveringskaarten',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Toon seizoensposters op tabbladen',
|
'settings.showSeasonPostersOnTabs' => 'Toon seizoensposters op tabbladen',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Toon de poster van elk seizoen boven het tabblad',
|
'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.hideSpoilers' => 'Spoilers voor ongekeken afleveringen verbergen',
|
'settings.hideSpoilers' => 'Spoilers voor ongekeken afleveringen verbergen',
|
||||||
'settings.hideSpoilersDescription' => 'Vervaag miniaturen en beschrijvingen voor niet-bekeken afleveringen',
|
'settings.hideSpoilersDescription' => 'Vervaag miniaturen en beschrijvingen voor niet-bekeken afleveringen',
|
||||||
'settings.playerBackend' => 'Speler backend',
|
'settings.playerBackend' => 'Speler backend',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsNl {
|
|||||||
'profiles.active' => 'Actief',
|
'profiles.active' => 'Actief',
|
||||||
'profiles.manage' => 'Beheren',
|
'profiles.manage' => 'Beheren',
|
||||||
'profiles.delete' => 'Verwijderen',
|
'profiles.delete' => 'Verwijderen',
|
||||||
'profiles.signOut' => 'Afmelden',
|
|
||||||
'profiles.signOutPlexTitle' => 'Afmelden bij Plex?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Afmelden',
|
||||||
|
'profiles.signOutPlexTitle' => 'Afmelden bij Plex?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} en alle Plex Home-gebruikers verwijderen? Je kunt altijd opnieuw inloggen.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => '${displayName} en alle Plex Home-gebruikers verwijderen? Je kunt altijd opnieuw inloggen.',
|
||||||
'profiles.signedOutPlex' => 'Afgemeld bij Plex.',
|
'profiles.signedOutPlex' => 'Afgemeld bij Plex.',
|
||||||
'profiles.signOutFailed' => 'Afmelden mislukt.',
|
'profiles.signOutFailed' => 'Afmelden mislukt.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsNl {
|
|||||||
'metadataEdit.originalTitle' => 'Oorspronkelijke titel',
|
'metadataEdit.originalTitle' => 'Oorspronkelijke titel',
|
||||||
'metadataEdit.releaseDate' => 'Releasedatum',
|
'metadataEdit.releaseDate' => 'Releasedatum',
|
||||||
'metadataEdit.contentRating' => 'Leeftijdsclassificatie',
|
'metadataEdit.contentRating' => 'Leeftijdsclassificatie',
|
||||||
'metadataEdit.studio' => 'Studio',
|
|
||||||
'metadataEdit.tagline' => 'Tagline',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Studio',
|
||||||
|
'metadataEdit.tagline' => 'Tagline',
|
||||||
'metadataEdit.summary' => 'Samenvatting',
|
'metadataEdit.summary' => 'Samenvatting',
|
||||||
'metadataEdit.poster' => 'Poster',
|
'metadataEdit.poster' => 'Poster',
|
||||||
'metadataEdit.background' => 'Achtergrond',
|
'metadataEdit.background' => 'Achtergrond',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsPl extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Pokazuj numer sezonu i odcinka na kartach odcinków';
|
@override String get showEpisodeNumberOnCardsDescription => 'Pokazuj numer sezonu i odcinka na kartach odcinków';
|
||||||
@override String get showSeasonPostersOnTabs => 'Pokaż plakaty sezonów na zakładkach';
|
@override String get showSeasonPostersOnTabs => 'Pokaż plakaty sezonów na zakładkach';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Pokazuj plakat każdego sezonu nad jego kartą';
|
@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 hideSpoilers => 'Ukryj spoilery nieobejrzanych odcinków';
|
@override String get hideSpoilers => 'Ukryj spoilery nieobejrzanych odcinków';
|
||||||
@override String get hideSpoilersDescription => 'Rozmywaj miniatury i opisy nieobejrzanych odcinków';
|
@override String get hideSpoilersDescription => 'Rozmywaj miniatury i opisy nieobejrzanych odcinków';
|
||||||
@override String get playerBackend => 'Backend odtwarzacza';
|
@override String get playerBackend => 'Backend odtwarzacza';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsPl {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Pokazuj numer sezonu i odcinka na kartach odcinków',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Pokazuj numer sezonu i odcinka na kartach odcinków',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Pokaż plakaty sezonów na zakładkach',
|
'settings.showSeasonPostersOnTabs' => 'Pokaż plakaty sezonów na zakładkach',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Pokazuj plakat każdego sezonu nad jego kartą',
|
'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.hideSpoilers' => 'Ukryj spoilery nieobejrzanych odcinków',
|
'settings.hideSpoilers' => 'Ukryj spoilery nieobejrzanych odcinków',
|
||||||
'settings.hideSpoilersDescription' => 'Rozmywaj miniatury i opisy nieobejrzanych odcinków',
|
'settings.hideSpoilersDescription' => 'Rozmywaj miniatury i opisy nieobejrzanych odcinków',
|
||||||
'settings.playerBackend' => 'Backend odtwarzacza',
|
'settings.playerBackend' => 'Backend odtwarzacza',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsPl {
|
|||||||
'profiles.active' => 'Aktywny',
|
'profiles.active' => 'Aktywny',
|
||||||
'profiles.manage' => 'Zarządzaj',
|
'profiles.manage' => 'Zarządzaj',
|
||||||
'profiles.delete' => 'Usuń',
|
'profiles.delete' => 'Usuń',
|
||||||
'profiles.signOut' => 'Wyloguj się',
|
|
||||||
'profiles.signOutPlexTitle' => 'Wylogować się z Plex?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Wyloguj się',
|
||||||
|
'profiles.signOutPlexTitle' => 'Wylogować się z Plex?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Usunąć ${displayName} i wszystkich użytkowników Plex Home? Możesz zalogować się ponownie w każdej chwili.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Usunąć ${displayName} i wszystkich użytkowników Plex Home? Możesz zalogować się ponownie w każdej chwili.',
|
||||||
'profiles.signedOutPlex' => 'Wylogowano z Plex.',
|
'profiles.signedOutPlex' => 'Wylogowano z Plex.',
|
||||||
'profiles.signOutFailed' => 'Wylogowanie nie powiodło się.',
|
'profiles.signOutFailed' => 'Wylogowanie nie powiodło się.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsPl {
|
|||||||
'metadataEdit.originalTitle' => 'Tytuł oryginalny',
|
'metadataEdit.originalTitle' => 'Tytuł oryginalny',
|
||||||
'metadataEdit.releaseDate' => 'Data premiery',
|
'metadataEdit.releaseDate' => 'Data premiery',
|
||||||
'metadataEdit.contentRating' => 'Klasyfikacja wiekowa',
|
'metadataEdit.contentRating' => 'Klasyfikacja wiekowa',
|
||||||
'metadataEdit.studio' => 'Studio',
|
|
||||||
'metadataEdit.tagline' => 'Tagline',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Studio',
|
||||||
|
'metadataEdit.tagline' => 'Tagline',
|
||||||
'metadataEdit.summary' => 'Opis',
|
'metadataEdit.summary' => 'Opis',
|
||||||
'metadataEdit.poster' => 'Plakat',
|
'metadataEdit.poster' => 'Plakat',
|
||||||
'metadataEdit.background' => 'Tło',
|
'metadataEdit.background' => 'Tło',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsPt extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Mostrar temporada e episódio nos cartões de episódio';
|
@override String get showEpisodeNumberOnCardsDescription => 'Mostrar temporada e episódio nos cartões de episódio';
|
||||||
@override String get showSeasonPostersOnTabs => 'Mostrar Pôsteres de Temporada nas Abas';
|
@override String get showSeasonPostersOnTabs => 'Mostrar Pôsteres de Temporada nas Abas';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Mostrar o pôster de cada temporada acima da aba';
|
@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 hideSpoilers => 'Ocultar Spoilers de Episódios Não Assistidos';
|
@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 hideSpoilersDescription => 'Desfocar miniaturas e descrições de episódios não vistos';
|
||||||
@override String get playerBackend => 'Backend do Player';
|
@override String get playerBackend => 'Backend do Player';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsPt {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Mostrar temporada e episódio nos cartões de episódio',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Mostrar temporada e episódio nos cartões de episódio',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Mostrar Pôsteres de Temporada nas Abas',
|
'settings.showSeasonPostersOnTabs' => 'Mostrar Pôsteres de Temporada nas Abas',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Mostrar o pôster de cada temporada acima da aba',
|
'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.hideSpoilers' => 'Ocultar Spoilers de Episódios Não Assistidos',
|
'settings.hideSpoilers' => 'Ocultar Spoilers de Episódios Não Assistidos',
|
||||||
'settings.hideSpoilersDescription' => 'Desfocar miniaturas e descrições de episódios não vistos',
|
'settings.hideSpoilersDescription' => 'Desfocar miniaturas e descrições de episódios não vistos',
|
||||||
'settings.playerBackend' => 'Backend do Player',
|
'settings.playerBackend' => 'Backend do Player',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsPt {
|
|||||||
'profiles.active' => 'Ativo',
|
'profiles.active' => 'Ativo',
|
||||||
'profiles.manage' => 'Gerenciar',
|
'profiles.manage' => 'Gerenciar',
|
||||||
'profiles.delete' => 'Excluir',
|
'profiles.delete' => 'Excluir',
|
||||||
'profiles.signOut' => 'Sair',
|
|
||||||
'profiles.signOutPlexTitle' => 'Sair do Plex?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Sair',
|
||||||
|
'profiles.signOutPlexTitle' => 'Sair do Plex?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Remover ${displayName} e todos os usuários Plex Home? Você pode entrar novamente quando quiser.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Remover ${displayName} e todos os usuários Plex Home? Você pode entrar novamente quando quiser.',
|
||||||
'profiles.signedOutPlex' => 'Saiu do Plex.',
|
'profiles.signedOutPlex' => 'Saiu do Plex.',
|
||||||
'profiles.signOutFailed' => 'Falha ao sair.',
|
'profiles.signOutFailed' => 'Falha ao sair.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsPt {
|
|||||||
'metadataEdit.originalTitle' => 'Título Original',
|
'metadataEdit.originalTitle' => 'Título Original',
|
||||||
'metadataEdit.releaseDate' => 'Data de Lançamento',
|
'metadataEdit.releaseDate' => 'Data de Lançamento',
|
||||||
'metadataEdit.contentRating' => 'Classificação Indicativa',
|
'metadataEdit.contentRating' => 'Classificação Indicativa',
|
||||||
'metadataEdit.studio' => 'Estúdio',
|
|
||||||
'metadataEdit.tagline' => 'Tagline',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Estúdio',
|
||||||
|
'metadataEdit.tagline' => 'Tagline',
|
||||||
'metadataEdit.summary' => 'Sinopse',
|
'metadataEdit.summary' => 'Sinopse',
|
||||||
'metadataEdit.poster' => 'Poster',
|
'metadataEdit.poster' => 'Poster',
|
||||||
'metadataEdit.background' => 'Plano de Fundo',
|
'metadataEdit.background' => 'Plano de Fundo',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsRu extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Показывать номер сезона и серии на карточках серий';
|
@override String get showEpisodeNumberOnCardsDescription => 'Показывать номер сезона и серии на карточках серий';
|
||||||
@override String get showSeasonPostersOnTabs => 'Показывать постеры сезонов на вкладках';
|
@override String get showSeasonPostersOnTabs => 'Показывать постеры сезонов на вкладках';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Показывать постер каждого сезона над его вкладкой';
|
@override String get showSeasonPostersOnTabsDescription => 'Показывать постер каждого сезона над его вкладкой';
|
||||||
|
@override String get tvFullCardLayout => 'Полные TV-карточки';
|
||||||
|
@override String get tvFullCardLayoutDescription => 'Использовать TV-карточки только с изображением и именами актёров поверх него';
|
||||||
@override String get hideSpoilers => 'Скрыть спойлеры непросмотренных эпизодов';
|
@override String get hideSpoilers => 'Скрыть спойлеры непросмотренных эпизодов';
|
||||||
@override String get hideSpoilersDescription => 'Размывать миниатюры и описания непросмотренных серий';
|
@override String get hideSpoilersDescription => 'Размывать миниатюры и описания непросмотренных серий';
|
||||||
@override String get playerBackend => 'Бэкенд плеера';
|
@override String get playerBackend => 'Бэкенд плеера';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsRu {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Показывать номер сезона и серии на карточках серий',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Показывать номер сезона и серии на карточках серий',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Показывать постеры сезонов на вкладках',
|
'settings.showSeasonPostersOnTabs' => 'Показывать постеры сезонов на вкладках',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Показывать постер каждого сезона над его вкладкой',
|
'settings.showSeasonPostersOnTabsDescription' => 'Показывать постер каждого сезона над его вкладкой',
|
||||||
|
'settings.tvFullCardLayout' => 'Полные TV-карточки',
|
||||||
|
'settings.tvFullCardLayoutDescription' => 'Использовать TV-карточки только с изображением и именами актёров поверх него',
|
||||||
'settings.hideSpoilers' => 'Скрыть спойлеры непросмотренных эпизодов',
|
'settings.hideSpoilers' => 'Скрыть спойлеры непросмотренных эпизодов',
|
||||||
'settings.hideSpoilersDescription' => 'Размывать миниатюры и описания непросмотренных серий',
|
'settings.hideSpoilersDescription' => 'Размывать миниатюры и описания непросмотренных серий',
|
||||||
'settings.playerBackend' => 'Бэкенд плеера',
|
'settings.playerBackend' => 'Бэкенд плеера',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsRu {
|
|||||||
'profiles.active' => 'Активный',
|
'profiles.active' => 'Активный',
|
||||||
'profiles.manage' => 'Управление',
|
'profiles.manage' => 'Управление',
|
||||||
'profiles.delete' => 'Удалить',
|
'profiles.delete' => 'Удалить',
|
||||||
'profiles.signOut' => 'Выйти',
|
|
||||||
'profiles.signOutPlexTitle' => 'Выйти из Plex?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Выйти',
|
||||||
|
'profiles.signOutPlexTitle' => 'Выйти из Plex?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Удалить ${displayName} и всех пользователей Plex Home? Вы сможете войти снова в любое время.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Удалить ${displayName} и всех пользователей Plex Home? Вы сможете войти снова в любое время.',
|
||||||
'profiles.signedOutPlex' => 'Вы вышли из Plex.',
|
'profiles.signedOutPlex' => 'Вы вышли из Plex.',
|
||||||
'profiles.signOutFailed' => 'Не удалось выйти.',
|
'profiles.signOutFailed' => 'Не удалось выйти.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsRu {
|
|||||||
'metadataEdit.originalTitle' => 'Оригинальное название',
|
'metadataEdit.originalTitle' => 'Оригинальное название',
|
||||||
'metadataEdit.releaseDate' => 'Дата выпуска',
|
'metadataEdit.releaseDate' => 'Дата выпуска',
|
||||||
'metadataEdit.contentRating' => 'Возрастной рейтинг',
|
'metadataEdit.contentRating' => 'Возрастной рейтинг',
|
||||||
'metadataEdit.studio' => 'Студия',
|
|
||||||
'metadataEdit.tagline' => 'Слоган',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Студия',
|
||||||
|
'metadataEdit.tagline' => 'Слоган',
|
||||||
'metadataEdit.summary' => 'Описание',
|
'metadataEdit.summary' => 'Описание',
|
||||||
'metadataEdit.poster' => 'Постер',
|
'metadataEdit.poster' => 'Постер',
|
||||||
'metadataEdit.background' => 'Фон',
|
'metadataEdit.background' => 'Фон',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsSv extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => 'Visa säsongs- och avsnittsnummer på avsnittskort';
|
@override String get showEpisodeNumberOnCardsDescription => 'Visa säsongs- och avsnittsnummer på avsnittskort';
|
||||||
@override String get showSeasonPostersOnTabs => 'Visa säsongsaffischer på flikar';
|
@override String get showSeasonPostersOnTabs => 'Visa säsongsaffischer på flikar';
|
||||||
@override String get showSeasonPostersOnTabsDescription => 'Visa varje säsongs affisch ovanför fliken';
|
@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 hideSpoilers => 'Dölj spoilers för osedda avsnitt';
|
@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 hideSpoilersDescription => 'Sudda miniatyrbilder och beskrivningar för osedda avsnitt';
|
||||||
@override String get playerBackend => 'Spelarmotor';
|
@override String get playerBackend => 'Spelarmotor';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsSv {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => 'Visa säsongs- och avsnittsnummer på avsnittskort',
|
'settings.showEpisodeNumberOnCardsDescription' => 'Visa säsongs- och avsnittsnummer på avsnittskort',
|
||||||
'settings.showSeasonPostersOnTabs' => 'Visa säsongsaffischer på flikar',
|
'settings.showSeasonPostersOnTabs' => 'Visa säsongsaffischer på flikar',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => 'Visa varje säsongs affisch ovanför fliken',
|
'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.hideSpoilers' => 'Dölj spoilers för osedda avsnitt',
|
'settings.hideSpoilers' => 'Dölj spoilers för osedda avsnitt',
|
||||||
'settings.hideSpoilersDescription' => 'Sudda miniatyrbilder och beskrivningar för osedda avsnitt',
|
'settings.hideSpoilersDescription' => 'Sudda miniatyrbilder och beskrivningar för osedda avsnitt',
|
||||||
'settings.playerBackend' => 'Spelarmotor',
|
'settings.playerBackend' => 'Spelarmotor',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsSv {
|
|||||||
'profiles.active' => 'Aktiv',
|
'profiles.active' => 'Aktiv',
|
||||||
'profiles.manage' => 'Hantera',
|
'profiles.manage' => 'Hantera',
|
||||||
'profiles.delete' => 'Ta bort',
|
'profiles.delete' => 'Ta bort',
|
||||||
'profiles.signOut' => 'Logga ut',
|
|
||||||
'profiles.signOutPlexTitle' => 'Logga ut från Plex?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => 'Logga ut',
|
||||||
|
'profiles.signOutPlexTitle' => 'Logga ut från Plex?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Ta bort ${displayName} och alla Plex Home-användare? Du kan logga in igen när som helst.',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => 'Ta bort ${displayName} och alla Plex Home-användare? Du kan logga in igen när som helst.',
|
||||||
'profiles.signedOutPlex' => 'Utloggad från Plex.',
|
'profiles.signedOutPlex' => 'Utloggad från Plex.',
|
||||||
'profiles.signOutFailed' => 'Utloggningen misslyckades.',
|
'profiles.signOutFailed' => 'Utloggningen misslyckades.',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsSv {
|
|||||||
'metadataEdit.originalTitle' => 'Originaltitel',
|
'metadataEdit.originalTitle' => 'Originaltitel',
|
||||||
'metadataEdit.releaseDate' => 'Utgivningsdatum',
|
'metadataEdit.releaseDate' => 'Utgivningsdatum',
|
||||||
'metadataEdit.contentRating' => 'Åldersgräns',
|
'metadataEdit.contentRating' => 'Åldersgräns',
|
||||||
'metadataEdit.studio' => 'Studio',
|
|
||||||
'metadataEdit.tagline' => 'Tagline',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => 'Studio',
|
||||||
|
'metadataEdit.tagline' => 'Tagline',
|
||||||
'metadataEdit.summary' => 'Sammanfattning',
|
'metadataEdit.summary' => 'Sammanfattning',
|
||||||
'metadataEdit.poster' => 'Poster',
|
'metadataEdit.poster' => 'Poster',
|
||||||
'metadataEdit.background' => 'Bakgrund',
|
'metadataEdit.background' => 'Bakgrund',
|
||||||
|
|||||||
@@ -254,6 +254,8 @@ class _TranslationsSettingsZh extends TranslationsSettingsEn {
|
|||||||
@override String get showEpisodeNumberOnCardsDescription => '在剧集卡片上显示季和集编号';
|
@override String get showEpisodeNumberOnCardsDescription => '在剧集卡片上显示季和集编号';
|
||||||
@override String get showSeasonPostersOnTabs => '在选项卡上显示季海报';
|
@override String get showSeasonPostersOnTabs => '在选项卡上显示季海报';
|
||||||
@override String get showSeasonPostersOnTabsDescription => '在每季标签上方显示该季海报';
|
@override String get showSeasonPostersOnTabsDescription => '在每季标签上方显示该季海报';
|
||||||
|
@override String get tvFullCardLayout => '完整 TV 卡片';
|
||||||
|
@override String get tvFullCardLayoutDescription => '使用仅显示图片的 TV 卡片,并叠加演员姓名';
|
||||||
@override String get hideSpoilers => '隐藏未看剧集的剧透内容';
|
@override String get hideSpoilers => '隐藏未看剧集的剧透内容';
|
||||||
@override String get hideSpoilersDescription => '模糊未观看剧集的缩略图和描述';
|
@override String get hideSpoilersDescription => '模糊未观看剧集的缩略图和描述';
|
||||||
@override String get playerBackend => '播放器引擎';
|
@override String get playerBackend => '播放器引擎';
|
||||||
@@ -1922,6 +1924,8 @@ extension on TranslationsZh {
|
|||||||
'settings.showEpisodeNumberOnCardsDescription' => '在剧集卡片上显示季和集编号',
|
'settings.showEpisodeNumberOnCardsDescription' => '在剧集卡片上显示季和集编号',
|
||||||
'settings.showSeasonPostersOnTabs' => '在选项卡上显示季海报',
|
'settings.showSeasonPostersOnTabs' => '在选项卡上显示季海报',
|
||||||
'settings.showSeasonPostersOnTabsDescription' => '在每季标签上方显示该季海报',
|
'settings.showSeasonPostersOnTabsDescription' => '在每季标签上方显示该季海报',
|
||||||
|
'settings.tvFullCardLayout' => '完整 TV 卡片',
|
||||||
|
'settings.tvFullCardLayoutDescription' => '使用仅显示图片的 TV 卡片,并叠加演员姓名',
|
||||||
'settings.hideSpoilers' => '隐藏未看剧集的剧透内容',
|
'settings.hideSpoilers' => '隐藏未看剧集的剧透内容',
|
||||||
'settings.hideSpoilersDescription' => '模糊未观看剧集的缩略图和描述',
|
'settings.hideSpoilersDescription' => '模糊未观看剧集的缩略图和描述',
|
||||||
'settings.playerBackend' => '播放器引擎',
|
'settings.playerBackend' => '播放器引擎',
|
||||||
@@ -2316,10 +2320,10 @@ extension on TranslationsZh {
|
|||||||
'profiles.active' => '活跃',
|
'profiles.active' => '活跃',
|
||||||
'profiles.manage' => '管理',
|
'profiles.manage' => '管理',
|
||||||
'profiles.delete' => '删除',
|
'profiles.delete' => '删除',
|
||||||
'profiles.signOut' => '退出登录',
|
|
||||||
'profiles.signOutPlexTitle' => '退出 Plex 登录?',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'profiles.signOut' => '退出登录',
|
||||||
|
'profiles.signOutPlexTitle' => '退出 Plex 登录?',
|
||||||
'profiles.signOutPlexMessage' => ({required Object displayName}) => '要移除 ${displayName} 和所有 Plex Home 用户吗?可随时重新登录。',
|
'profiles.signOutPlexMessage' => ({required Object displayName}) => '要移除 ${displayName} 和所有 Plex Home 用户吗?可随时重新登录。',
|
||||||
'profiles.signedOutPlex' => '已退出 Plex 登录。',
|
'profiles.signedOutPlex' => '已退出 Plex 登录。',
|
||||||
'profiles.signOutFailed' => '退出登录失败。',
|
'profiles.signOutFailed' => '退出登录失败。',
|
||||||
@@ -2830,10 +2834,10 @@ extension on TranslationsZh {
|
|||||||
'metadataEdit.originalTitle' => '原始标题',
|
'metadataEdit.originalTitle' => '原始标题',
|
||||||
'metadataEdit.releaseDate' => '上映日期',
|
'metadataEdit.releaseDate' => '上映日期',
|
||||||
'metadataEdit.contentRating' => '内容分级',
|
'metadataEdit.contentRating' => '内容分级',
|
||||||
'metadataEdit.studio' => '制片厂',
|
|
||||||
'metadataEdit.tagline' => '标语',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'metadataEdit.studio' => '制片厂',
|
||||||
|
'metadataEdit.tagline' => '标语',
|
||||||
'metadataEdit.summary' => '简介',
|
'metadataEdit.summary' => '简介',
|
||||||
'metadataEdit.poster' => '海报',
|
'metadataEdit.poster' => '海报',
|
||||||
'metadataEdit.background' => '背景',
|
'metadataEdit.background' => '背景',
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "Visa säsongs- och avsnittsnummer på avsnittskort",
|
"showEpisodeNumberOnCardsDescription": "Visa säsongs- och avsnittsnummer på avsnittskort",
|
||||||
"showSeasonPostersOnTabs": "Visa säsongsaffischer på flikar",
|
"showSeasonPostersOnTabs": "Visa säsongsaffischer på flikar",
|
||||||
"showSeasonPostersOnTabsDescription": "Visa varje säsongs affisch ovanför fliken",
|
"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å",
|
||||||
"hideSpoilers": "Dölj spoilers för osedda avsnitt",
|
"hideSpoilers": "Dölj spoilers för osedda avsnitt",
|
||||||
"hideSpoilersDescription": "Sudda miniatyrbilder och beskrivningar för osedda avsnitt",
|
"hideSpoilersDescription": "Sudda miniatyrbilder och beskrivningar för osedda avsnitt",
|
||||||
"playerBackend": "Spelarmotor",
|
"playerBackend": "Spelarmotor",
|
||||||
|
|||||||
@@ -126,6 +126,8 @@
|
|||||||
"showEpisodeNumberOnCardsDescription": "在剧集卡片上显示季和集编号",
|
"showEpisodeNumberOnCardsDescription": "在剧集卡片上显示季和集编号",
|
||||||
"showSeasonPostersOnTabs": "在选项卡上显示季海报",
|
"showSeasonPostersOnTabs": "在选项卡上显示季海报",
|
||||||
"showSeasonPostersOnTabsDescription": "在每季标签上方显示该季海报",
|
"showSeasonPostersOnTabsDescription": "在每季标签上方显示该季海报",
|
||||||
|
"tvFullCardLayout": "完整 TV 卡片",
|
||||||
|
"tvFullCardLayoutDescription": "使用仅显示图片的 TV 卡片,并叠加演员姓名",
|
||||||
"hideSpoilers": "隐藏未看剧集的剧透内容",
|
"hideSpoilers": "隐藏未看剧集的剧透内容",
|
||||||
"hideSpoilersDescription": "模糊未观看剧集的缩略图和描述",
|
"hideSpoilersDescription": "模糊未观看剧集的缩略图和描述",
|
||||||
"playerBackend": "播放器引擎",
|
"playerBackend": "播放器引擎",
|
||||||
|
|||||||
@@ -1554,6 +1554,7 @@ class _DiscoverScreenState extends State<DiscoverScreen>
|
|||||||
hubs: browseHubs,
|
hubs: browseHubs,
|
||||||
density: svc.read(SettingsService.libraryDensity),
|
density: svc.read(SettingsService.libraryDensity),
|
||||||
episodePosterMode: svc.read(SettingsService.episodePosterMode),
|
episodePosterMode: svc.read(SettingsService.episodePosterMode),
|
||||||
|
fullCardLayout: svc.read(SettingsService.tvFullCardLayout),
|
||||||
tallPosterScale: TvBrowseRailLayout.compactTallPosterScale,
|
tallPosterScale: TvBrowseRailLayout.compactTallPosterScale,
|
||||||
);
|
);
|
||||||
final spotlightTop = (size.height * 0.075).clamp(64.0 * scale, 120.0 * scale).toDouble();
|
final spotlightTop = (size.height * 0.075).clamp(64.0 * scale, 120.0 * scale).toDouble();
|
||||||
|
|||||||
@@ -317,21 +317,33 @@ class _DownloadsGridContentState extends State<_DownloadsGridContent> {
|
|||||||
// Extra top padding for focus decoration (scale + border extends beyond item bounds)
|
// Extra top padding for focus decoration (scale + border extends beyond item bounds)
|
||||||
const effectivePadding = EdgeInsets.only(left: 8, right: 8, top: 8);
|
const effectivePadding = EdgeInsets.only(left: 8, right: 8, top: 8);
|
||||||
|
|
||||||
return SettingValueBuilder<int>(
|
return SettingsBuilder(
|
||||||
pref: SettingsService.libraryDensity,
|
prefs: const [SettingsService.libraryDensity, SettingsService.tvFullCardLayout],
|
||||||
builder: (context, density, _) {
|
builder: (context) {
|
||||||
|
final settings = SettingsService.instanceOrNull!;
|
||||||
|
final density = settings.read(SettingsService.libraryDensity);
|
||||||
|
final fullCardLayout = PlatformDetector.isTV() && settings.read(SettingsService.tvFullCardLayout);
|
||||||
final maxCrossAxisExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, density);
|
final maxCrossAxisExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, density);
|
||||||
// Use LayoutBuilder to get actual available width (accounting for sidebar)
|
// Use LayoutBuilder to get actual available width (accounting for sidebar)
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final availableWidth = constraints.maxWidth - effectivePadding.left - effectivePadding.right;
|
final availableWidth = constraints.maxWidth - effectivePadding.left - effectivePadding.right;
|
||||||
final columnCount = GridSizeCalculator.getColumnCount(availableWidth, maxCrossAxisExtent);
|
final gridSpacing = MediaGridDelegate.spacingFor(context: context, fullBleedImage: fullCardLayout);
|
||||||
|
final columnCount = GridSizeCalculator.getColumnCount(
|
||||||
|
availableWidth,
|
||||||
|
maxCrossAxisExtent,
|
||||||
|
crossAxisSpacing: gridSpacing,
|
||||||
|
);
|
||||||
|
|
||||||
return GridView.builder(
|
return GridView.builder(
|
||||||
padding: effectivePadding,
|
padding: effectivePadding,
|
||||||
// Allow focus decoration to render outside scroll bounds
|
// Allow focus decoration to render outside scroll bounds
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
gridDelegate: MediaGridDelegate.createDelegate(context: context, density: density),
|
gridDelegate: MediaGridDelegate.createDelegate(
|
||||||
|
context: context,
|
||||||
|
density: density,
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
|
),
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = items[index];
|
final item = items[index];
|
||||||
@@ -342,6 +354,7 @@ class _DownloadsGridContentState extends State<_DownloadsGridContent> {
|
|||||||
focusNode: isFirst ? _firstItemFocusNode : null,
|
focusNode: isFirst ? _firstItemFocusNode : null,
|
||||||
onBack: widget.onBack,
|
onBack: widget.onBack,
|
||||||
isOffline: true, // Downloaded content works without server
|
isOffline: true, // Downloaded content works without server
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
onNavigateLeft: isFirstColumn ? _navigateToSidebar : null,
|
onNavigateLeft: isFirstColumn ? _navigateToSidebar : null,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -169,11 +169,12 @@ mixin FocusableDetailScreenMixin<T extends StatefulWidget> on State<T>, GridFocu
|
|||||||
VoidCallback? onListRefresh,
|
VoidCallback? onListRefresh,
|
||||||
}) {
|
}) {
|
||||||
return SettingsBuilder(
|
return SettingsBuilder(
|
||||||
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity],
|
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity, SettingsService.tvFullCardLayout],
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final svc = SettingsService.instanceOrNull!;
|
final svc = SettingsService.instanceOrNull!;
|
||||||
final isListMode = svc.read(SettingsService.viewMode) == ViewMode.list;
|
final isListMode = svc.read(SettingsService.viewMode) == ViewMode.list;
|
||||||
final libraryDensity = svc.read(SettingsService.libraryDensity);
|
final libraryDensity = svc.read(SettingsService.libraryDensity);
|
||||||
|
final fullCardLayout = PlatformDetector.isTV() && svc.read(SettingsService.tvFullCardLayout);
|
||||||
|
|
||||||
if (isListMode) {
|
if (isListMode) {
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
@@ -206,9 +207,18 @@ mixin FocusableDetailScreenMixin<T extends StatefulWidget> on State<T>, GridFocu
|
|||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
sliver: SliverLayoutBuilder(
|
sliver: SliverLayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final columnCount = GridSizeCalculator.getColumnCount(constraints.crossAxisExtent, maxExtent);
|
final gridSpacing = MediaGridDelegate.spacingFor(context: context, fullBleedImage: fullCardLayout);
|
||||||
|
final columnCount = GridSizeCalculator.getColumnCount(
|
||||||
|
constraints.crossAxisExtent,
|
||||||
|
maxExtent,
|
||||||
|
crossAxisSpacing: gridSpacing,
|
||||||
|
);
|
||||||
return SliverGrid.builder(
|
return SliverGrid.builder(
|
||||||
gridDelegate: MediaGridDelegate.createDelegate(context: context, density: libraryDensity),
|
gridDelegate: MediaGridDelegate.createDelegate(
|
||||||
|
context: context,
|
||||||
|
density: libraryDensity,
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
|
),
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final item = items[index];
|
final item = items[index];
|
||||||
@@ -222,6 +232,7 @@ mixin FocusableDetailScreenMixin<T extends StatefulWidget> on State<T>, GridFocu
|
|||||||
onRefresh: onRefresh,
|
onRefresh: onRefresh,
|
||||||
collectionId: collectionId,
|
collectionId: collectionId,
|
||||||
onListRefresh: onListRefresh,
|
onListRefresh: onListRefresh,
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
onNavigateUp: inFirstRow ? navigateToAppBar : null,
|
onNavigateUp: inFirstRow ? navigateToAppBar : null,
|
||||||
onBack: handleBackFromContent,
|
onBack: handleBackFromContent,
|
||||||
onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus),
|
onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus),
|
||||||
@@ -248,11 +259,12 @@ mixin FocusableDetailScreenMixin<T extends StatefulWidget> on State<T>, GridFocu
|
|||||||
VoidCallback? onListRefresh,
|
VoidCallback? onListRefresh,
|
||||||
}) {
|
}) {
|
||||||
return SettingsBuilder(
|
return SettingsBuilder(
|
||||||
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity],
|
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity, SettingsService.tvFullCardLayout],
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final svc = SettingsService.instanceOrNull!;
|
final svc = SettingsService.instanceOrNull!;
|
||||||
final isListMode = svc.read(SettingsService.viewMode) == ViewMode.list;
|
final isListMode = svc.read(SettingsService.viewMode) == ViewMode.list;
|
||||||
final libraryDensity = svc.read(SettingsService.libraryDensity);
|
final libraryDensity = svc.read(SettingsService.libraryDensity);
|
||||||
|
final fullCardLayout = PlatformDetector.isTV() && svc.read(SettingsService.tvFullCardLayout);
|
||||||
|
|
||||||
Widget buildTile(int index, {required bool inFirstRow, required bool disableScale}) {
|
Widget buildTile(int index, {required bool inFirstRow, required bool disableScale}) {
|
||||||
final item = itemAt(index);
|
final item = itemAt(index);
|
||||||
@@ -269,6 +281,7 @@ mixin FocusableDetailScreenMixin<T extends StatefulWidget> on State<T>, GridFocu
|
|||||||
onRefresh: onRefresh,
|
onRefresh: onRefresh,
|
||||||
collectionId: collectionId,
|
collectionId: collectionId,
|
||||||
onListRefresh: onListRefresh,
|
onListRefresh: onListRefresh,
|
||||||
|
fullBleedImage: fullCardLayout && !disableScale,
|
||||||
onNavigateUp: inFirstRow ? navigateToAppBar : null,
|
onNavigateUp: inFirstRow ? navigateToAppBar : null,
|
||||||
onBack: handleBackFromContent,
|
onBack: handleBackFromContent,
|
||||||
onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus),
|
onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus),
|
||||||
@@ -290,9 +303,18 @@ mixin FocusableDetailScreenMixin<T extends StatefulWidget> on State<T>, GridFocu
|
|||||||
padding: const EdgeInsets.all(8),
|
padding: const EdgeInsets.all(8),
|
||||||
sliver: SliverLayoutBuilder(
|
sliver: SliverLayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final columnCount = GridSizeCalculator.getColumnCount(constraints.crossAxisExtent, maxExtent);
|
final gridSpacing = MediaGridDelegate.spacingFor(context: context, fullBleedImage: fullCardLayout);
|
||||||
|
final columnCount = GridSizeCalculator.getColumnCount(
|
||||||
|
constraints.crossAxisExtent,
|
||||||
|
maxExtent,
|
||||||
|
crossAxisSpacing: gridSpacing,
|
||||||
|
);
|
||||||
return SliverGrid.builder(
|
return SliverGrid.builder(
|
||||||
gridDelegate: MediaGridDelegate.createDelegate(context: context, density: libraryDensity),
|
gridDelegate: MediaGridDelegate.createDelegate(
|
||||||
|
context: context,
|
||||||
|
density: libraryDensity,
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
|
),
|
||||||
itemCount: totalItems,
|
itemCount: totalItems,
|
||||||
itemBuilder: (context, index) => buildTile(
|
itemBuilder: (context, index) => buildTile(
|
||||||
index,
|
index,
|
||||||
|
|||||||
@@ -510,12 +510,14 @@ class _HubDetailScreenState extends State<HubDetailScreen>
|
|||||||
SettingsService.viewMode,
|
SettingsService.viewMode,
|
||||||
SettingsService.episodePosterMode,
|
SettingsService.episodePosterMode,
|
||||||
SettingsService.libraryDensity,
|
SettingsService.libraryDensity,
|
||||||
|
SettingsService.tvFullCardLayout,
|
||||||
],
|
],
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final svc = SettingsService.instanceOrNull!;
|
final svc = SettingsService.instanceOrNull!;
|
||||||
final isListMode = svc.read(SettingsService.viewMode) == ViewMode.list;
|
final isListMode = svc.read(SettingsService.viewMode) == ViewMode.list;
|
||||||
final episodePosterMode = svc.read(SettingsService.episodePosterMode);
|
final episodePosterMode = svc.read(SettingsService.episodePosterMode);
|
||||||
final libraryDensity = svc.read(SettingsService.libraryDensity);
|
final libraryDensity = svc.read(SettingsService.libraryDensity);
|
||||||
|
final fullCardLayout = PlatformDetector.isTV() && svc.read(SettingsService.tvFullCardLayout);
|
||||||
|
|
||||||
// Determine hub content type for layout decisions
|
// Determine hub content type for layout decisions
|
||||||
final hasEpisodes = _filteredItems.any((item) => item.usesWideAspectRatio(episodePosterMode));
|
final hasEpisodes = _filteredItems.any((item) => item.usesWideAspectRatio(episodePosterMode));
|
||||||
@@ -570,9 +572,14 @@ class _HubDetailScreenState extends State<HubDetailScreen>
|
|||||||
libraryDensity,
|
libraryDensity,
|
||||||
16,
|
16,
|
||||||
);
|
);
|
||||||
|
final gridSpacing = MediaGridDelegate.spacingFor(
|
||||||
|
context: context,
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
|
);
|
||||||
final columnCount = GridSizeCalculator.getColumnCount(
|
final columnCount = GridSizeCalculator.getColumnCount(
|
||||||
constraints.crossAxisExtent,
|
constraints.crossAxisExtent,
|
||||||
useWideLayout ? maxExtent * 1.8 : maxExtent,
|
useWideLayout ? maxExtent * 1.8 : maxExtent,
|
||||||
|
crossAxisSpacing: gridSpacing,
|
||||||
);
|
);
|
||||||
|
|
||||||
return SliverGrid(
|
return SliverGrid(
|
||||||
@@ -582,6 +589,7 @@ class _HubDetailScreenState extends State<HubDetailScreen>
|
|||||||
usePaddingAware: true,
|
usePaddingAware: true,
|
||||||
horizontalPadding: 16,
|
horizontalPadding: 16,
|
||||||
useWideAspectRatio: useWideLayout,
|
useWideAspectRatio: useWideLayout,
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
),
|
),
|
||||||
delegate: SliverChildBuilderDelegate((context, index) {
|
delegate: SliverChildBuilderDelegate((context, index) {
|
||||||
final item = _filteredItems[index];
|
final item = _filteredItems[index];
|
||||||
@@ -602,6 +610,7 @@ class _HubDetailScreenState extends State<HubDetailScreen>
|
|||||||
onBack: handleBackFromContent,
|
onBack: handleBackFromContent,
|
||||||
onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus),
|
onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus),
|
||||||
mixedHubContext: isMixedHub,
|
mixedHubContext: isMixedHub,
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
);
|
);
|
||||||
}, childCount: _filteredItems.length),
|
}, childCount: _filteredItems.length),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1511,7 +1511,12 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
SettingsBuilder(
|
SettingsBuilder(
|
||||||
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity, SettingsService.episodePosterMode],
|
prefs: const [
|
||||||
|
SettingsService.viewMode,
|
||||||
|
SettingsService.libraryDensity,
|
||||||
|
SettingsService.episodePosterMode,
|
||||||
|
SettingsService.tvFullCardLayout,
|
||||||
|
],
|
||||||
builder: (context) => _buildItemsSliver(context),
|
builder: (context) => _buildItemsSliver(context),
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
@@ -1568,6 +1573,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
|||||||
final viewMode = svc.read(SettingsService.viewMode);
|
final viewMode = svc.read(SettingsService.viewMode);
|
||||||
final libraryDensity = svc.read(SettingsService.libraryDensity);
|
final libraryDensity = svc.read(SettingsService.libraryDensity);
|
||||||
final episodePosterMode = svc.read(SettingsService.episodePosterMode);
|
final episodePosterMode = svc.read(SettingsService.episodePosterMode);
|
||||||
|
final fullCardLayout = PlatformDetector.isTV() && svc.read(SettingsService.tvFullCardLayout);
|
||||||
final itemCount = totalSize;
|
final itemCount = totalSize;
|
||||||
final isPhone = _isPhone(context);
|
final isPhone = _isPhone(context);
|
||||||
final topPadding = isPhone ? _gridTopPaddingPhone : _gridTopPadding;
|
final topPadding = isPhone ? _gridTopPaddingPhone : _gridTopPadding;
|
||||||
@@ -1611,17 +1617,28 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
|||||||
padding: EdgeInsets.fromLTRB(8, topPadding, rightPadding, 8),
|
padding: EdgeInsets.fromLTRB(8, topPadding, rightPadding, 8),
|
||||||
sliver: SliverLayoutBuilder(
|
sliver: SliverLayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
|
final gridSpacing = MediaGridDelegate.spacingFor(context: context, fullBleedImage: fullCardLayout);
|
||||||
// Compute column count from the width the grid would have without the alpha
|
// Compute column count from the width the grid would have without the alpha
|
||||||
// bar's reservation, so toggling the bar doesn't repack the grid into one
|
// bar's reservation, so toggling the bar doesn't repack the grid into one
|
||||||
// fewer column and blow up poster size.
|
// fewer column and blow up poster size.
|
||||||
final baselineWidth = constraints.crossAxisExtent + (rightPadding - 8.0);
|
final baselineWidth = constraints.crossAxisExtent + (rightPadding - 8.0);
|
||||||
final columnCount = GridSizeCalculator.getColumnCount(baselineWidth, effectiveMaxExtent);
|
final columnCount = GridSizeCalculator.getColumnCount(
|
||||||
|
baselineWidth,
|
||||||
|
effectiveMaxExtent,
|
||||||
|
crossAxisSpacing: gridSpacing,
|
||||||
|
);
|
||||||
// Cache grid metrics for alpha jump bar scroll calculations
|
// Cache grid metrics for alpha jump bar scroll calculations
|
||||||
final itemWidth = constraints.crossAxisExtent / columnCount;
|
final itemWidth = GridSizeCalculator.getCellWidthForColumnCount(
|
||||||
final itemHeight = itemWidth / GridLayoutConstants.posterAspectRatio;
|
constraints.crossAxisExtent,
|
||||||
|
columnCount,
|
||||||
|
crossAxisSpacing: gridSpacing,
|
||||||
|
);
|
||||||
|
final itemHeight =
|
||||||
|
itemWidth /
|
||||||
|
MediaGridDelegate.aspectRatioFor(useWideAspectRatio: useWideRatio, fullBleedImage: fullCardLayout);
|
||||||
_scrollMetrics = LibraryAlphaScrollMetrics(
|
_scrollMetrics = LibraryAlphaScrollMetrics(
|
||||||
columnCount: columnCount,
|
columnCount: columnCount,
|
||||||
rowHeight: itemHeight + GridLayoutConstants.mainAxisSpacing,
|
rowHeight: itemHeight + gridSpacing,
|
||||||
itemWidth: itemWidth,
|
itemWidth: itemWidth,
|
||||||
itemHeight: itemHeight,
|
itemHeight: itemHeight,
|
||||||
);
|
);
|
||||||
@@ -1630,7 +1647,8 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
|||||||
context: context,
|
context: context,
|
||||||
density: libraryDensity,
|
density: libraryDensity,
|
||||||
useWideAspectRatio: useWideRatio,
|
useWideAspectRatio: useWideRatio,
|
||||||
maxCrossAxisExtentOverride: hasAlphaBarReservation ? constraints.crossAxisExtent / columnCount : null,
|
fullBleedImage: fullCardLayout,
|
||||||
|
maxCrossAxisExtentOverride: hasAlphaBarReservation ? itemWidth : null,
|
||||||
),
|
),
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
itemBuilder: (context, index) => _buildMediaCardItem(
|
itemBuilder: (context, index) => _buildMediaCardItem(
|
||||||
@@ -1640,6 +1658,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
|||||||
isLastColumn: (index % columnCount) == (columnCount - 1),
|
isLastColumn: (index % columnCount) == (columnCount - 1),
|
||||||
columnCount: columnCount,
|
columnCount: columnCount,
|
||||||
itemCount: itemCount,
|
itemCount: itemCount,
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1654,6 +1673,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
|||||||
required bool isFirstColumn,
|
required bool isFirstColumn,
|
||||||
bool isLastColumn = false,
|
bool isLastColumn = false,
|
||||||
bool disableScale = false,
|
bool disableScale = false,
|
||||||
|
bool fullBleedImage = false,
|
||||||
int columnCount = 1,
|
int columnCount = 1,
|
||||||
int itemCount = 0,
|
int itemCount = 0,
|
||||||
}) {
|
}) {
|
||||||
@@ -1711,6 +1731,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
|||||||
onBack: widget.onBack,
|
onBack: widget.onBack,
|
||||||
onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus),
|
onFocusChange: (hasFocus) => trackGridItemFocus(index, hasFocus),
|
||||||
onListRefresh: _loadItems,
|
onListRefresh: _loadItems,
|
||||||
|
fullBleedImage: fullBleedImage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../../../utils/grid_size_calculator.dart';
|
|||||||
import '../../../utils/layout_constants.dart';
|
import '../../../utils/layout_constants.dart';
|
||||||
import '../../../utils/library_refresh_notifier.dart';
|
import '../../../utils/library_refresh_notifier.dart';
|
||||||
import '../../../utils/media_server_http_client.dart';
|
import '../../../utils/media_server_http_client.dart';
|
||||||
|
import '../../../utils/platform_detector.dart';
|
||||||
import '../../../widgets/focusable_media_card.dart';
|
import '../../../widgets/focusable_media_card.dart';
|
||||||
import '../../../widgets/media_grid_delegate.dart';
|
import '../../../widgets/media_grid_delegate.dart';
|
||||||
import '../../../widgets/settings_builder.dart';
|
import '../../../widgets/settings_builder.dart';
|
||||||
@@ -106,16 +107,20 @@ class _LibraryCollectionsTabState extends BaseLibraryTabState<MediaItem, Library
|
|||||||
@override
|
@override
|
||||||
Widget buildContent(List<MediaItem> items) {
|
Widget buildContent(List<MediaItem> items) {
|
||||||
return SettingsBuilder(
|
return SettingsBuilder(
|
||||||
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity],
|
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity, SettingsService.tvFullCardLayout],
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final settings = SettingsService.instanceOrNull!;
|
final settings = SettingsService.instanceOrNull!;
|
||||||
final viewMode = settings.read(SettingsService.viewMode);
|
final viewMode = settings.read(SettingsService.viewMode);
|
||||||
final density = settings.read(SettingsService.libraryDensity);
|
final density = settings.read(SettingsService.libraryDensity);
|
||||||
|
final fullCardLayout = PlatformDetector.isTV() && settings.read(SettingsService.tvFullCardLayout);
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
|
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
|
||||||
if (viewMode == ViewMode.list) _buildListSliver(density) else _buildGridSliver(density),
|
if (viewMode == ViewMode.list)
|
||||||
|
_buildListSliver(density)
|
||||||
|
else
|
||||||
|
_buildGridSliver(density, fullCardLayout: fullCardLayout),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -139,25 +144,42 @@ class _LibraryCollectionsTabState extends BaseLibraryTabState<MediaItem, Library
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildGridSliver(int density) {
|
Widget _buildGridSliver(int density, {required bool fullCardLayout}) {
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
padding: _effectivePadding,
|
padding: _effectivePadding,
|
||||||
sliver: SliverLayoutBuilder(
|
sliver: SliverLayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final maxCrossAxisExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, density);
|
final maxCrossAxisExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, density);
|
||||||
final columnCount = GridSizeCalculator.getColumnCount(constraints.crossAxisExtent, maxCrossAxisExtent);
|
final gridSpacing = MediaGridDelegate.spacingFor(context: context, fullBleedImage: fullCardLayout);
|
||||||
|
final columnCount = GridSizeCalculator.getColumnCount(
|
||||||
|
constraints.crossAxisExtent,
|
||||||
|
maxCrossAxisExtent,
|
||||||
|
crossAxisSpacing: gridSpacing,
|
||||||
|
);
|
||||||
return SliverGrid.builder(
|
return SliverGrid.builder(
|
||||||
gridDelegate: MediaGridDelegate.createDelegate(context: context, density: density),
|
gridDelegate: MediaGridDelegate.createDelegate(
|
||||||
|
context: context,
|
||||||
|
density: density,
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
|
),
|
||||||
itemCount: totalSize,
|
itemCount: totalSize,
|
||||||
itemBuilder: (context, index) =>
|
itemBuilder: (context, index) => _buildMediaCardItem(
|
||||||
_buildMediaCardItem(index, isFirstColumn: GridSizeCalculator.isFirstColumn(index, columnCount)),
|
index,
|
||||||
|
isFirstColumn: GridSizeCalculator.isFirstColumn(index, columnCount),
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMediaCardItem(int index, {required bool isFirstColumn, bool disableScale = false}) {
|
Widget _buildMediaCardItem(
|
||||||
|
int index, {
|
||||||
|
required bool isFirstColumn,
|
||||||
|
bool disableScale = false,
|
||||||
|
bool fullBleedImage = false,
|
||||||
|
}) {
|
||||||
final item = loadedItems[index];
|
final item = loadedItems[index];
|
||||||
if (item == null) {
|
if (item == null) {
|
||||||
ensureIndexLoaded(index, pageSize: _pageSize);
|
ensureIndexLoaded(index, pageSize: _pageSize);
|
||||||
@@ -169,6 +191,7 @@ class _LibraryCollectionsTabState extends BaseLibraryTabState<MediaItem, Library
|
|||||||
item: item,
|
item: item,
|
||||||
focusNode: index == 0 ? firstItemFocusNode : null,
|
focusNode: index == 0 ? firstItemFocusNode : null,
|
||||||
disableScale: disableScale,
|
disableScale: disableScale,
|
||||||
|
fullBleedImage: fullBleedImage,
|
||||||
onListRefresh: loadItems,
|
onListRefresh: loadItems,
|
||||||
onBack: widget.onBack,
|
onBack: widget.onBack,
|
||||||
onNavigateLeft: isFirstColumn ? _navigateToSidebar : null,
|
onNavigateLeft: isFirstColumn ? _navigateToSidebar : null,
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import '../../../utils/grid_size_calculator.dart';
|
|||||||
import '../../../utils/layout_constants.dart';
|
import '../../../utils/layout_constants.dart';
|
||||||
import '../../../utils/library_refresh_notifier.dart';
|
import '../../../utils/library_refresh_notifier.dart';
|
||||||
import '../../../utils/media_server_http_client.dart';
|
import '../../../utils/media_server_http_client.dart';
|
||||||
|
import '../../../utils/platform_detector.dart';
|
||||||
import '../../../widgets/focusable_media_card.dart';
|
import '../../../widgets/focusable_media_card.dart';
|
||||||
import '../../../widgets/media_grid_delegate.dart';
|
import '../../../widgets/media_grid_delegate.dart';
|
||||||
import '../../../widgets/settings_builder.dart';
|
import '../../../widgets/settings_builder.dart';
|
||||||
@@ -108,16 +109,20 @@ class _LibraryPlaylistsTabState extends BaseLibraryTabState<MediaPlaylist, Libra
|
|||||||
@override
|
@override
|
||||||
Widget buildContent(List<MediaPlaylist> items) {
|
Widget buildContent(List<MediaPlaylist> items) {
|
||||||
return SettingsBuilder(
|
return SettingsBuilder(
|
||||||
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity],
|
prefs: const [SettingsService.viewMode, SettingsService.libraryDensity, SettingsService.tvFullCardLayout],
|
||||||
builder: (context) {
|
builder: (context) {
|
||||||
final settings = SettingsService.instanceOrNull!;
|
final settings = SettingsService.instanceOrNull!;
|
||||||
final viewMode = settings.read(SettingsService.viewMode);
|
final viewMode = settings.read(SettingsService.viewMode);
|
||||||
final density = settings.read(SettingsService.libraryDensity);
|
final density = settings.read(SettingsService.libraryDensity);
|
||||||
|
final fullCardLayout = PlatformDetector.isTV() && settings.read(SettingsService.tvFullCardLayout);
|
||||||
return CustomScrollView(
|
return CustomScrollView(
|
||||||
clipBehavior: Clip.none,
|
clipBehavior: Clip.none,
|
||||||
slivers: [
|
slivers: [
|
||||||
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
|
SliverOverlapInjector(handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context)),
|
||||||
if (viewMode == ViewMode.list) _buildListSliver(density) else _buildGridSliver(density),
|
if (viewMode == ViewMode.list)
|
||||||
|
_buildListSliver(density)
|
||||||
|
else
|
||||||
|
_buildGridSliver(density, fullCardLayout: fullCardLayout),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -141,25 +146,42 @@ class _LibraryPlaylistsTabState extends BaseLibraryTabState<MediaPlaylist, Libra
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildGridSliver(int density) {
|
Widget _buildGridSliver(int density, {required bool fullCardLayout}) {
|
||||||
return SliverPadding(
|
return SliverPadding(
|
||||||
padding: _effectivePadding,
|
padding: _effectivePadding,
|
||||||
sliver: SliverLayoutBuilder(
|
sliver: SliverLayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final maxCrossAxisExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, density);
|
final maxCrossAxisExtent = GridSizeCalculator.getMaxCrossAxisExtent(context, density);
|
||||||
final columnCount = GridSizeCalculator.getColumnCount(constraints.crossAxisExtent, maxCrossAxisExtent);
|
final gridSpacing = MediaGridDelegate.spacingFor(context: context, fullBleedImage: fullCardLayout);
|
||||||
|
final columnCount = GridSizeCalculator.getColumnCount(
|
||||||
|
constraints.crossAxisExtent,
|
||||||
|
maxCrossAxisExtent,
|
||||||
|
crossAxisSpacing: gridSpacing,
|
||||||
|
);
|
||||||
return SliverGrid.builder(
|
return SliverGrid.builder(
|
||||||
gridDelegate: MediaGridDelegate.createDelegate(context: context, density: density),
|
gridDelegate: MediaGridDelegate.createDelegate(
|
||||||
|
context: context,
|
||||||
|
density: density,
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
|
),
|
||||||
itemCount: totalSize,
|
itemCount: totalSize,
|
||||||
itemBuilder: (context, index) =>
|
itemBuilder: (context, index) => _buildPlaylistCard(
|
||||||
_buildPlaylistCard(index, isFirstColumn: GridSizeCalculator.isFirstColumn(index, columnCount)),
|
index,
|
||||||
|
isFirstColumn: GridSizeCalculator.isFirstColumn(index, columnCount),
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPlaylistCard(int index, {required bool isFirstColumn, bool disableScale = false}) {
|
Widget _buildPlaylistCard(
|
||||||
|
int index, {
|
||||||
|
required bool isFirstColumn,
|
||||||
|
bool disableScale = false,
|
||||||
|
bool fullBleedImage = false,
|
||||||
|
}) {
|
||||||
final playlist = loadedItems[index];
|
final playlist = loadedItems[index];
|
||||||
if (playlist == null) {
|
if (playlist == null) {
|
||||||
ensureIndexLoaded(index, pageSize: _pageSize);
|
ensureIndexLoaded(index, pageSize: _pageSize);
|
||||||
@@ -171,6 +193,7 @@ class _LibraryPlaylistsTabState extends BaseLibraryTabState<MediaPlaylist, Libra
|
|||||||
item: playlist,
|
item: playlist,
|
||||||
focusNode: index == 0 ? firstItemFocusNode : null,
|
focusNode: index == 0 ? firstItemFocusNode : null,
|
||||||
disableScale: disableScale,
|
disableScale: disableScale,
|
||||||
|
fullBleedImage: fullBleedImage,
|
||||||
onListRefresh: loadItems,
|
onListRefresh: loadItems,
|
||||||
onBack: widget.onBack,
|
onBack: widget.onBack,
|
||||||
onNavigateLeft: isFirstColumn ? _navigateToSidebar : null,
|
onNavigateLeft: isFirstColumn ? _navigateToSidebar : null,
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ class _LibraryRecommendedTabState extends BaseLibraryTabState<MediaHub, LibraryR
|
|||||||
hubs: tvHubs,
|
hubs: tvHubs,
|
||||||
density: svc.read(SettingsService.libraryDensity),
|
density: svc.read(SettingsService.libraryDensity),
|
||||||
episodePosterMode: svc.read(SettingsService.episodePosterMode),
|
episodePosterMode: svc.read(SettingsService.episodePosterMode),
|
||||||
|
fullCardLayout: svc.read(SettingsService.tvFullCardLayout),
|
||||||
tallPosterScale: TvBrowseRailLayout.compactTallPosterScale,
|
tallPosterScale: TvBrowseRailLayout.compactTallPosterScale,
|
||||||
);
|
);
|
||||||
final spotlightTop = (size.height * 0.075).clamp(64.0 * scale, 120.0 * scale).toDouble();
|
final spotlightTop = (size.height * 0.075).clamp(64.0 * scale, 120.0 * scale).toDouble();
|
||||||
|
|||||||
@@ -3445,6 +3445,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
|||||||
episodePosterMode: svc.read(SettingsService.episodePosterMode),
|
episodePosterMode: svc.read(SettingsService.episodePosterMode),
|
||||||
episodePosterModeForHub: _tvDetailEpisodePosterModeForHub,
|
episodePosterModeForHub: _tvDetailEpisodePosterModeForHub,
|
||||||
widePosterScaleForHub: _tvDetailWidePosterScaleForHub,
|
widePosterScaleForHub: _tvDetailWidePosterScaleForHub,
|
||||||
|
fullCardLayout: svc.read(SettingsService.tvFullCardLayout),
|
||||||
tallPosterScale: _tvDetailTallPosterScale,
|
tallPosterScale: _tvDetailTallPosterScale,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -3471,6 +3472,7 @@ class _MediaDetailScreenState extends State<MediaDetailScreen>
|
|||||||
availableWidth: availableWidth,
|
availableWidth: availableWidth,
|
||||||
density: svc.read(SettingsService.libraryDensity),
|
density: svc.read(SettingsService.libraryDensity),
|
||||||
episodePosterMode: svc.read(SettingsService.episodePosterMode),
|
episodePosterMode: svc.read(SettingsService.episodePosterMode),
|
||||||
|
fullCardLayout: svc.read(SettingsService.tvFullCardLayout),
|
||||||
scale: scale,
|
scale: scale,
|
||||||
tallPosterScale: _tvDetailTallPosterScale,
|
tallPosterScale: _tvDetailTallPosterScale,
|
||||||
widePosterScale: 1.0,
|
widePosterScale: 1.0,
|
||||||
|
|||||||
@@ -33,6 +33,13 @@ class AppearanceSettingsScreen extends StatelessWidget {
|
|||||||
_densitySelector(),
|
_densitySelector(),
|
||||||
_viewModeSelector(),
|
_viewModeSelector(),
|
||||||
_episodePosterModeSelector(),
|
_episodePosterModeSelector(),
|
||||||
|
if (PlatformDetector.isTV())
|
||||||
|
SettingSwitchTile(
|
||||||
|
pref: SettingsService.tvFullCardLayout,
|
||||||
|
icon: Symbols.image_rounded,
|
||||||
|
title: t.settings.tvFullCardLayout,
|
||||||
|
subtitle: t.settings.tvFullCardLayoutDescription,
|
||||||
|
),
|
||||||
SettingSwitchTile(
|
SettingSwitchTile(
|
||||||
pref: SettingsService.showEpisodeNumberOnCards,
|
pref: SettingsService.showEpisodeNumberOnCards,
|
||||||
icon: Symbols.tag_rounded,
|
icon: Symbols.tag_rounded,
|
||||||
|
|||||||
@@ -278,6 +278,7 @@ class SettingsService extends BaseSharedPreferencesService {
|
|||||||
static const seekTimeLarge = IntPref('seek_time_large', defaultValue: 30);
|
static const seekTimeLarge = IntPref('seek_time_large', defaultValue: 30);
|
||||||
static const rewindOnResume = IntPref('rewind_on_resume');
|
static const rewindOnResume = IntPref('rewind_on_resume');
|
||||||
static const showHeroSection = BoolPref('show_hero_section', defaultValue: true);
|
static const showHeroSection = BoolPref('show_hero_section', defaultValue: true);
|
||||||
|
static const tvFullCardLayout = BoolPref('tv_full_card_layout', defaultValue: false);
|
||||||
static const useGlobalHubs = BoolPref('use_global_hubs', defaultValue: true);
|
static const useGlobalHubs = BoolPref('use_global_hubs', defaultValue: true);
|
||||||
static const showServerNameOnHubs = BoolPref('show_server_name_on_hubs');
|
static const showServerNameOnHubs = BoolPref('show_server_name_on_hubs');
|
||||||
static const groupLibrariesByServer = BoolPref('group_libraries_by_server', defaultValue: true);
|
static const groupLibrariesByServer = BoolPref('group_libraries_by_server', defaultValue: true);
|
||||||
|
|||||||
@@ -53,18 +53,29 @@ class GridSizeCalculator {
|
|||||||
/// [crossAxisExtent] should come from layout constraints (e.g. `SliverLayoutBuilder`
|
/// [crossAxisExtent] should come from layout constraints (e.g. `SliverLayoutBuilder`
|
||||||
/// or `LayoutBuilder`), not from `MediaQuery`, to account for sidebars or other
|
/// or `LayoutBuilder`), not from `MediaQuery`, to account for sidebars or other
|
||||||
/// elements that reduce the grid's actual width.
|
/// elements that reduce the grid's actual width.
|
||||||
static int getColumnCount(double crossAxisExtent, double maxCrossAxisExtent) {
|
static int getColumnCount(
|
||||||
final crossAxisSpacing = GridLayoutConstants.crossAxisSpacing;
|
double crossAxisExtent,
|
||||||
|
double maxCrossAxisExtent, {
|
||||||
|
double crossAxisSpacing = GridLayoutConstants.crossAxisSpacing,
|
||||||
|
}) {
|
||||||
return ((crossAxisExtent + crossAxisSpacing) / (maxCrossAxisExtent + crossAxisSpacing)).ceil().clamp(1, 100);
|
return ((crossAxisExtent + crossAxisSpacing) / (maxCrossAxisExtent + crossAxisSpacing)).ceil().clamp(1, 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static double getCellWidthForColumnCount(
|
||||||
|
double crossAxisExtent,
|
||||||
|
int columnCount, {
|
||||||
|
double crossAxisSpacing = GridLayoutConstants.crossAxisSpacing,
|
||||||
|
}) {
|
||||||
|
return (crossAxisExtent - (crossAxisSpacing * (columnCount - 1))) / columnCount;
|
||||||
|
}
|
||||||
|
|
||||||
/// Computes the actual cell width that a grid with [getMaxCrossAxisExtent] would produce
|
/// Computes the actual cell width that a grid with [getMaxCrossAxisExtent] would produce
|
||||||
/// for the given [availableWidth]. This matches SliverGridDelegateWithMaxCrossAxisExtent's
|
/// for the given [availableWidth]. This matches SliverGridDelegateWithMaxCrossAxisExtent's
|
||||||
/// internal calculation, so horizontal scroll lists can use the same width as grids.
|
/// internal calculation, so horizontal scroll lists can use the same width as grids.
|
||||||
static double getCellWidth(double availableWidth, BuildContext context, int density) {
|
static double getCellWidth(double availableWidth, BuildContext context, int density) {
|
||||||
final maxExtent = getMaxCrossAxisExtent(context, density);
|
final maxExtent = getMaxCrossAxisExtent(context, density);
|
||||||
final columns = getColumnCount(availableWidth, maxExtent);
|
final columns = getColumnCount(availableWidth, maxExtent);
|
||||||
return availableWidth / columns;
|
return getCellWidthForColumnCount(availableWidth, columns);
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool isFirstRow(int index, int columnCount) {
|
static bool isFirstRow(int index, int columnCount) {
|
||||||
|
|||||||
@@ -41,6 +41,8 @@ class AppDurations {
|
|||||||
class GridLayoutConstants {
|
class GridLayoutConstants {
|
||||||
static const double posterAspectRatio = 2 / 3.3;
|
static const double posterAspectRatio = 2 / 3.3;
|
||||||
|
|
||||||
|
static const double fullCardPosterAspectRatio = 2 / 3;
|
||||||
|
|
||||||
static const double episodeThumbnailAspectRatio = 16 / 9;
|
static const double episodeThumbnailAspectRatio = 16 / 9;
|
||||||
|
|
||||||
static const double episodeGridCellAspectRatio = 1.4;
|
static const double episodeGridCellAspectRatio = 1.4;
|
||||||
@@ -48,6 +50,8 @@ class GridLayoutConstants {
|
|||||||
static const double crossAxisSpacing = 0;
|
static const double crossAxisSpacing = 0;
|
||||||
static const double mainAxisSpacing = 0;
|
static const double mainAxisSpacing = 0;
|
||||||
|
|
||||||
|
static double fullCardGridSpacingForScale(double scale) => (12 * scale).clamp(8, 18).toDouble();
|
||||||
|
|
||||||
/// Standard grid padding
|
/// Standard grid padding
|
||||||
static EdgeInsets get gridPadding => const EdgeInsets.only(left: 2, right: 2, bottom: 2);
|
static EdgeInsets get gridPadding => const EdgeInsets.only(left: 2, right: 2, bottom: 2);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,6 +73,10 @@ class FocusBuilders {
|
|||||||
VoidCallback? onTap,
|
VoidCallback? onTap,
|
||||||
VoidCallback? onLongPress,
|
VoidCallback? onLongPress,
|
||||||
double borderRadius = FocusTheme.defaultBorderRadius,
|
double borderRadius = FocusTheme.defaultBorderRadius,
|
||||||
|
double focusScale = FocusTheme.focusScale,
|
||||||
|
double focusBorderStrokeAlign = BorderSide.strokeAlignInside,
|
||||||
|
bool useFocusGlow = false,
|
||||||
|
bool useForegroundFocusDecoration = false,
|
||||||
required Widget child,
|
required Widget child,
|
||||||
}) {
|
}) {
|
||||||
final isKeyboardMode = InputModeTracker.isKeyboardMode(context);
|
final isKeyboardMode = InputModeTracker.isKeyboardMode(context);
|
||||||
@@ -93,15 +97,25 @@ class FocusBuilders {
|
|||||||
|
|
||||||
final duration = FocusTheme.getAnimationDuration(context);
|
final duration = FocusTheme.getAnimationDuration(context);
|
||||||
final showFocus = isFocused && isKeyboardMode;
|
final showFocus = isFocused && isKeyboardMode;
|
||||||
|
final focusDecoration = FocusTheme.focusDecoration(
|
||||||
|
context,
|
||||||
|
isFocused: showFocus,
|
||||||
|
borderRadius: borderRadius,
|
||||||
|
borderStrokeAlign: focusBorderStrokeAlign,
|
||||||
|
);
|
||||||
|
final glowDecoration = useFocusGlow
|
||||||
|
? FocusTheme.focusGlowDecoration(context, isFocused: showFocus, borderRadius: borderRadius)
|
||||||
|
: null;
|
||||||
|
|
||||||
final focusedWidget = AnimatedScale(
|
final focusedWidget = AnimatedScale(
|
||||||
scale: showFocus ? FocusTheme.focusScale : 1.0,
|
scale: showFocus ? focusScale : 1.0,
|
||||||
duration: duration,
|
duration: duration,
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
child: AnimatedContainer(
|
child: AnimatedContainer(
|
||||||
duration: duration,
|
duration: duration,
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
decoration: FocusTheme.focusDecoration(context, isFocused: showFocus, borderRadius: borderRadius),
|
decoration: useForegroundFocusDecoration ? glowDecoration : focusDecoration,
|
||||||
|
foregroundDecoration: useForegroundFocusDecoration ? focusDecoration : null,
|
||||||
child: child,
|
child: child,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -138,6 +152,10 @@ class FocusBuilders {
|
|||||||
VoidCallback? onTap,
|
VoidCallback? onTap,
|
||||||
VoidCallback? onLongPress,
|
VoidCallback? onLongPress,
|
||||||
double borderRadius = FocusTheme.defaultBorderRadius,
|
double borderRadius = FocusTheme.defaultBorderRadius,
|
||||||
|
double focusScale = FocusTheme.focusScale,
|
||||||
|
double focusBorderStrokeAlign = BorderSide.strokeAlignInside,
|
||||||
|
bool useFocusGlow = false,
|
||||||
|
bool useForegroundFocusDecoration = false,
|
||||||
required Widget child,
|
required Widget child,
|
||||||
}) {
|
}) {
|
||||||
return buildFocusableCard(
|
return buildFocusableCard(
|
||||||
@@ -148,6 +166,10 @@ class FocusBuilders {
|
|||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
onLongPress: onLongPress,
|
onLongPress: onLongPress,
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
|
focusScale: focusScale,
|
||||||
|
focusBorderStrokeAlign: focusBorderStrokeAlign,
|
||||||
|
useFocusGlow: useFocusGlow,
|
||||||
|
useForegroundFocusDecoration: useForegroundFocusDecoration,
|
||||||
child: child,
|
child: child,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../focus/focus_theme.dart';
|
||||||
import '../focus/focusable_wrapper.dart';
|
import '../focus/focusable_wrapper.dart';
|
||||||
import '../utils/platform_detector.dart';
|
import '../utils/platform_detector.dart';
|
||||||
import 'media_card.dart';
|
import 'media_card.dart';
|
||||||
@@ -30,6 +31,9 @@ class FocusableMediaCard extends StatefulWidget {
|
|||||||
/// True when in a hub with mixed content (movies + episodes)
|
/// True when in a hub with mixed content (movies + episodes)
|
||||||
final bool mixedHubContext;
|
final bool mixedHubContext;
|
||||||
|
|
||||||
|
/// Render grid cards as image-only full-bleed cards.
|
||||||
|
final bool fullBleedImage;
|
||||||
|
|
||||||
/// Show server name in list view (multi-server)
|
/// Show server name in list view (multi-server)
|
||||||
final bool showServerName;
|
final bool showServerName;
|
||||||
|
|
||||||
@@ -79,6 +83,7 @@ class FocusableMediaCard extends StatefulWidget {
|
|||||||
this.collectionId,
|
this.collectionId,
|
||||||
this.isOffline = false,
|
this.isOffline = false,
|
||||||
this.mixedHubContext = false,
|
this.mixedHubContext = false,
|
||||||
|
this.fullBleedImage = false,
|
||||||
this.showServerName = false,
|
this.showServerName = false,
|
||||||
this.disableScale = false,
|
this.disableScale = false,
|
||||||
this.focusNode,
|
this.focusNode,
|
||||||
@@ -111,6 +116,10 @@ class _FocusableMediaCardState extends State<FocusableMediaCard> {
|
|||||||
onFocusChange: widget.onFocusChange,
|
onFocusChange: widget.onFocusChange,
|
||||||
enableLongPress: true,
|
enableLongPress: true,
|
||||||
disableScale: widget.disableScale,
|
disableScale: widget.disableScale,
|
||||||
|
focusScale: widget.fullBleedImage ? FocusTheme.fullCardFocusScale : FocusTheme.focusScale,
|
||||||
|
focusBorderStrokeAlign: widget.fullBleedImage ? BorderSide.strokeAlignOutside : BorderSide.strokeAlignInside,
|
||||||
|
useFocusGlow: widget.fullBleedImage,
|
||||||
|
useForegroundFocusDecoration: widget.fullBleedImage,
|
||||||
useComfortableZone: !PlatformDetector.isTV(), // Always center on TV
|
useComfortableZone: !PlatformDetector.isTV(), // Always center on TV
|
||||||
scrollAlignment: 0.5,
|
scrollAlignment: 0.5,
|
||||||
child: MediaCard(
|
child: MediaCard(
|
||||||
@@ -127,6 +136,7 @@ class _FocusableMediaCardState extends State<FocusableMediaCard> {
|
|||||||
collectionId: widget.collectionId,
|
collectionId: widget.collectionId,
|
||||||
isOffline: widget.isOffline,
|
isOffline: widget.isOffline,
|
||||||
mixedHubContext: widget.mixedHubContext,
|
mixedHubContext: widget.mixedHubContext,
|
||||||
|
fullBleedImage: widget.fullBleedImage,
|
||||||
showServerName: widget.showServerName,
|
showServerName: widget.showServerName,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ class MediaCard extends StatefulWidget {
|
|||||||
final bool mixedHubContext; // True when in a hub with mixed content (movies + episodes)
|
final bool mixedHubContext; // True when in a hub with mixed content (movies + episodes)
|
||||||
final bool showServerName; // Show server name in list view (multi-server)
|
final bool showServerName; // Show server name in list view (multi-server)
|
||||||
final EpisodePosterMode? episodePosterModeOverride;
|
final EpisodePosterMode? episodePosterModeOverride;
|
||||||
|
final bool fullBleedImage;
|
||||||
|
|
||||||
const MediaCard({
|
const MediaCard({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -77,6 +78,7 @@ class MediaCard extends StatefulWidget {
|
|||||||
this.mixedHubContext = false,
|
this.mixedHubContext = false,
|
||||||
this.showServerName = false,
|
this.showServerName = false,
|
||||||
this.episodePosterModeOverride,
|
this.episodePosterModeOverride,
|
||||||
|
this.fullBleedImage = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -264,8 +266,65 @@ class MediaCardState extends State<MediaCard> with ContextMenuTapMixin<MediaCard
|
|||||||
/// Grid layout — inlined from former _MediaCardGrid, _PosterOverlay, and
|
/// Grid layout — inlined from former _MediaCardGrid, _PosterOverlay, and
|
||||||
/// flattened Column. Semantics removed (InkWell provides button semantics).
|
/// flattened Column. Semantics removed (InkWell provides button semantics).
|
||||||
Widget _buildGridCard(BuildContext context, Object item, String? localPosterPath) {
|
Widget _buildGridCard(BuildContext context, Object item, String? localPosterPath) {
|
||||||
|
if (widget.fullBleedImage) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final cardWidth = widget.width ?? (constraints.hasBoundedWidth ? constraints.maxWidth : null);
|
||||||
|
final cardHeight = widget.height ?? (constraints.hasBoundedHeight ? constraints.maxHeight : null);
|
||||||
|
if (cardHeight == null) return _buildStandardGridCard(context, item, localPosterPath);
|
||||||
|
return _buildFullBleedGridCard(context, item, localPosterPath, width: cardWidth, height: cardHeight);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _buildStandardGridCard(context, item, localPosterPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildFullBleedGridCard(
|
||||||
|
BuildContext context,
|
||||||
|
Object item,
|
||||||
|
String? localPosterPath, {
|
||||||
|
required double? width,
|
||||||
|
required double height,
|
||||||
|
}) {
|
||||||
|
return SizedBox(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
child: InkWell(
|
||||||
|
mouseCursor: SystemMouseCursors.click,
|
||||||
|
canRequestFocus: false,
|
||||||
|
onTap: () => _handleTap(context, item),
|
||||||
|
onTapDown: storeTapPosition,
|
||||||
|
onLongPress: showContextMenuFromTap,
|
||||||
|
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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildStandardGridCard(BuildContext context, Object item, String? localPosterPath) {
|
||||||
// Compute actual poster dimensions from card dimensions
|
// Compute actual poster dimensions from card dimensions
|
||||||
final posterWidth = widget.width != null ? widget.width! - 6 : null; // 3px padding each side
|
final posterWidth = widget.width != null ? widget.width! - 6 : null;
|
||||||
final posterHeight = widget.height;
|
final posterHeight = widget.height;
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
|
|||||||
@@ -3,13 +3,14 @@ import '../utils/grid_size_calculator.dart';
|
|||||||
import '../utils/layout_constants.dart';
|
import '../utils/layout_constants.dart';
|
||||||
|
|
||||||
/// Shared grid delegate configuration for media item grids
|
/// Shared grid delegate configuration for media item grids
|
||||||
/// Maintains consistent spacing (2/3.3 aspect ratio, 0 spacing) across all media grids
|
/// Maintains consistent aspect ratio and spacing across all media grids.
|
||||||
class MediaGridDelegate {
|
class MediaGridDelegate {
|
||||||
/// Creates a standard grid delegate for media items
|
/// Creates a standard grid delegate for media items
|
||||||
///
|
///
|
||||||
/// Uses [GridSizeCalculator.getMaxCrossAxisExtent] by default.
|
/// Uses [GridSizeCalculator.getMaxCrossAxisExtent] by default.
|
||||||
/// Set [usePaddingAware] to true to use [GridSizeCalculator.getMaxCrossAxisExtentWithPadding] instead.
|
/// Set [usePaddingAware] to true to use [GridSizeCalculator.getMaxCrossAxisExtentWithPadding] instead.
|
||||||
/// Set [useWideAspectRatio] to true to use 16:9 aspect ratio for episode thumbnails.
|
/// Set [useWideAspectRatio] to true to use 16:9 aspect ratio for episode thumbnails.
|
||||||
|
/// Set [fullBleedImage] to true when the card is image-only and should not reserve text height.
|
||||||
/// Pass [maxCrossAxisExtentOverride] to bypass the calculator and the wide-aspect multiplier —
|
/// Pass [maxCrossAxisExtentOverride] to bypass the calculator and the wide-aspect multiplier —
|
||||||
/// the caller is then responsible for providing a fully-resolved per-cell width.
|
/// the caller is then responsible for providing a fully-resolved per-cell width.
|
||||||
static SliverGridDelegateWithMaxCrossAxisExtent createDelegate({
|
static SliverGridDelegateWithMaxCrossAxisExtent createDelegate({
|
||||||
@@ -18,11 +19,11 @@ class MediaGridDelegate {
|
|||||||
bool usePaddingAware = false,
|
bool usePaddingAware = false,
|
||||||
double horizontalPadding = 16,
|
double horizontalPadding = 16,
|
||||||
bool useWideAspectRatio = false,
|
bool useWideAspectRatio = false,
|
||||||
|
bool fullBleedImage = false,
|
||||||
double? maxCrossAxisExtentOverride,
|
double? maxCrossAxisExtentOverride,
|
||||||
}) {
|
}) {
|
||||||
final aspectRatio = useWideAspectRatio
|
final aspectRatio = aspectRatioFor(useWideAspectRatio: useWideAspectRatio, fullBleedImage: fullBleedImage);
|
||||||
? GridLayoutConstants.episodeGridCellAspectRatio
|
final spacing = spacingFor(context: context, fullBleedImage: fullBleedImage);
|
||||||
: GridLayoutConstants.posterAspectRatio;
|
|
||||||
|
|
||||||
double maxCrossAxisExtent;
|
double maxCrossAxisExtent;
|
||||||
if (maxCrossAxisExtentOverride != null) {
|
if (maxCrossAxisExtentOverride != null) {
|
||||||
@@ -42,8 +43,23 @@ class MediaGridDelegate {
|
|||||||
return SliverGridDelegateWithMaxCrossAxisExtent(
|
return SliverGridDelegateWithMaxCrossAxisExtent(
|
||||||
maxCrossAxisExtent: maxCrossAxisExtent,
|
maxCrossAxisExtent: maxCrossAxisExtent,
|
||||||
childAspectRatio: aspectRatio,
|
childAspectRatio: aspectRatio,
|
||||||
crossAxisSpacing: GridLayoutConstants.crossAxisSpacing,
|
crossAxisSpacing: spacing,
|
||||||
mainAxisSpacing: GridLayoutConstants.mainAxisSpacing,
|
mainAxisSpacing: spacing,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static double spacingFor({required BuildContext context, bool fullBleedImage = false}) {
|
||||||
|
if (!fullBleedImage) return GridLayoutConstants.crossAxisSpacing;
|
||||||
|
return GridLayoutConstants.fullCardGridSpacingForScale(TvLayoutConstants.scaleOf(context));
|
||||||
|
}
|
||||||
|
|
||||||
|
static double aspectRatioFor({bool useWideAspectRatio = false, bool fullBleedImage = false}) {
|
||||||
|
if (fullBleedImage) {
|
||||||
|
return useWideAspectRatio
|
||||||
|
? GridLayoutConstants.episodeThumbnailAspectRatio
|
||||||
|
: GridLayoutConstants.fullCardPosterAspectRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
return useWideAspectRatio ? GridLayoutConstants.episodeGridCellAspectRatio : GridLayoutConstants.posterAspectRatio;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+262
-77
@@ -20,6 +20,7 @@ import '../utils/media_navigation_helper.dart';
|
|||||||
import '../utils/provider_extensions.dart';
|
import '../utils/provider_extensions.dart';
|
||||||
import '../utils/layout_constants.dart';
|
import '../utils/layout_constants.dart';
|
||||||
import 'app_icon.dart';
|
import 'app_icon.dart';
|
||||||
|
import 'clickable_cursor.dart';
|
||||||
import 'focus_builders.dart';
|
import 'focus_builders.dart';
|
||||||
import 'horizontal_scroll_with_arrows.dart';
|
import 'horizontal_scroll_with_arrows.dart';
|
||||||
import 'media_card.dart';
|
import 'media_card.dart';
|
||||||
@@ -57,6 +58,7 @@ class TvBrowseRailLayoutMetrics {
|
|||||||
class TvBrowseRailLayout {
|
class TvBrowseRailLayout {
|
||||||
static const double compactTallPosterScale = 0.80;
|
static const double compactTallPosterScale = 0.80;
|
||||||
static const double compactEpisodeThumbnailScale = compactTallPosterScale;
|
static const double compactEpisodeThumbnailScale = compactTallPosterScale;
|
||||||
|
static const double fullCardFocusScale = FocusTheme.fullCardFocusScale;
|
||||||
|
|
||||||
static double scaleForSize(Size size) => TvLayoutConstants.scaleForSize(size);
|
static double scaleForSize(Size size) => TvLayoutConstants.scaleForSize(size);
|
||||||
|
|
||||||
@@ -70,6 +72,21 @@ class TvBrowseRailLayout {
|
|||||||
|
|
||||||
static double itemGapForScale(double _) => 0;
|
static double itemGapForScale(double _) => 0;
|
||||||
|
|
||||||
|
static double fullCardItemGapForScale(double scale) => (12 * scale).clamp(8, 18).toDouble();
|
||||||
|
|
||||||
|
static double viewAllItemWidthForScale(double scale) => (104 * scale).clamp(88, 132).toDouble();
|
||||||
|
|
||||||
|
static double viewAllPillHeightForScale(double scale) => (44 * scale).clamp(36, 54).toDouble();
|
||||||
|
|
||||||
|
static double fullCardFocusPaintOverflowForScale(double scale) {
|
||||||
|
return (FocusTheme.focusGlowOuterBlurRadius +
|
||||||
|
FocusTheme.focusGlowSpreadRadius +
|
||||||
|
FocusTheme.focusBorderWidth +
|
||||||
|
(10 * scale))
|
||||||
|
.clamp(42, 64)
|
||||||
|
.toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
static double hubStripHeightForScale(double scale) => 36 * scale;
|
static double hubStripHeightForScale(double scale) => 36 * scale;
|
||||||
|
|
||||||
static double hubStripGapForScale(double _) => 0;
|
static double hubStripGapForScale(double _) => 0;
|
||||||
@@ -111,12 +128,13 @@ class TvBrowseRailLayout {
|
|||||||
required int density,
|
required int density,
|
||||||
required EpisodePosterMode episodePosterMode,
|
required EpisodePosterMode episodePosterMode,
|
||||||
required double scale,
|
required double scale,
|
||||||
|
bool fullCardLayout = false,
|
||||||
double tallPosterScale = 1.0,
|
double tallPosterScale = 1.0,
|
||||||
double widePosterScale = 1.0,
|
double widePosterScale = 1.0,
|
||||||
}) {
|
}) {
|
||||||
final focusExtra = FocusTheme.focusBorderWidth * 2 * scale;
|
final focusExtra = FocusTheme.focusBorderWidth * 2 * scale;
|
||||||
final railEdgePadding = focusExtra + (12 * scale);
|
final railEdgePadding = focusExtra + (12 * scale);
|
||||||
final itemGap = itemGapForScale(scale);
|
final itemGap = fullCardLayout ? fullCardItemGapForScale(scale) : itemGapForScale(scale);
|
||||||
final isPersonHub = TvBrowseRailLayout.isPersonHub(hub);
|
final isPersonHub = TvBrowseRailLayout.isPersonHub(hub);
|
||||||
final hasWide = !isPersonHub && hub.items.any((item) => item.usesWideAspectRatio(episodePosterMode));
|
final hasWide = !isPersonHub && hub.items.any((item) => item.usesWideAspectRatio(episodePosterMode));
|
||||||
final hasTall = !isPersonHub && hub.items.any((item) => !item.usesWideAspectRatio(episodePosterMode));
|
final hasTall = !isPersonHub && hub.items.any((item) => !item.usesWideAspectRatio(episodePosterMode));
|
||||||
@@ -131,9 +149,10 @@ class TvBrowseRailLayout {
|
|||||||
itemGap: itemGap,
|
itemGap: itemGap,
|
||||||
);
|
);
|
||||||
final cardWidth = baseCardWidth * (useWideLayout ? widePosterScale : tallPosterScale);
|
final cardWidth = baseCardWidth * (useWideLayout ? widePosterScale : tallPosterScale);
|
||||||
final posterWidth = cardWidth - (6 * scale);
|
final posterWidth = fullCardLayout ? cardWidth : cardWidth - (6 * scale);
|
||||||
final posterHeight = isPersonHub ? posterWidth : (useWideLayout ? posterWidth * 9 / 16 : posterWidth * 1.5);
|
final posterHeight = isPersonHub ? posterWidth : (useWideLayout ? posterWidth * 9 / 16 : posterWidth * 1.5);
|
||||||
final containerHeight = (posterHeight + ((isPersonHub ? 58 : 42) * scale)).ceilToDouble();
|
final labelHeight = fullCardLayout ? 0.0 : ((isPersonHub ? 58 : 42) * scale);
|
||||||
|
final containerHeight = (posterHeight + labelHeight).ceilToDouble();
|
||||||
final height = containerHeight + focusExtra + (14 * scale);
|
final height = containerHeight + focusExtra + (14 * scale);
|
||||||
|
|
||||||
return TvBrowseRailLayoutMetrics(
|
return TvBrowseRailLayoutMetrics(
|
||||||
@@ -159,6 +178,7 @@ class TvBrowseRailLayout {
|
|||||||
EpisodePosterMode Function(MediaHub hub)? episodePosterModeForHub,
|
EpisodePosterMode Function(MediaHub hub)? episodePosterModeForHub,
|
||||||
double Function(MediaHub hub)? widePosterScaleForHub,
|
double Function(MediaHub hub)? widePosterScaleForHub,
|
||||||
required double scale,
|
required double scale,
|
||||||
|
bool fullCardLayout = false,
|
||||||
double tallPosterScale = 1.0,
|
double tallPosterScale = 1.0,
|
||||||
double widePosterScale = 1.0,
|
double widePosterScale = 1.0,
|
||||||
}) {
|
}) {
|
||||||
@@ -170,6 +190,7 @@ class TvBrowseRailLayout {
|
|||||||
density: density,
|
density: density,
|
||||||
episodePosterMode: episodePosterModeForHub?.call(hub) ?? episodePosterMode,
|
episodePosterMode: episodePosterModeForHub?.call(hub) ?? episodePosterMode,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
|
fullCardLayout: fullCardLayout,
|
||||||
tallPosterScale: tallPosterScale,
|
tallPosterScale: tallPosterScale,
|
||||||
widePosterScale: widePosterScaleForHub?.call(hub) ?? widePosterScale,
|
widePosterScale: widePosterScaleForHub?.call(hub) ?? widePosterScale,
|
||||||
);
|
);
|
||||||
@@ -185,7 +206,7 @@ class TvBrowseRailLayout {
|
|||||||
required double scale,
|
required double scale,
|
||||||
}) {
|
}) {
|
||||||
final itemContentWidth = hub.items.length * (metrics.cardWidth + metrics.itemGap);
|
final itemContentWidth = hub.items.length * (metrics.cardWidth + metrics.itemGap);
|
||||||
final moreContentWidth = hub.more ? (132 * scale) + metrics.itemGap : 0.0;
|
final moreContentWidth = hub.more ? viewAllItemWidthForScale(scale) + metrics.itemGap : 0.0;
|
||||||
final contentWidth = (metrics.railEdgePadding * 2) + itemContentWidth + moreContentWidth;
|
final contentWidth = (metrics.railEdgePadding * 2) + itemContentWidth + moreContentWidth;
|
||||||
return (contentWidth - viewportWidth).clamp(0.0, double.infinity).toDouble();
|
return (contentWidth - viewportWidth).clamp(0.0, double.infinity).toDouble();
|
||||||
}
|
}
|
||||||
@@ -196,7 +217,7 @@ class TvBrowseRailLayout {
|
|||||||
required TvBrowseRailLayoutMetrics metrics,
|
required TvBrowseRailLayoutMetrics metrics,
|
||||||
required double scale,
|
required double scale,
|
||||||
}) {
|
}) {
|
||||||
if (index == hub.items.length && hub.more) return (132 * scale) + metrics.itemGap;
|
if (index == hub.items.length && hub.more) return viewAllItemWidthForScale(scale) + metrics.itemGap;
|
||||||
return metrics.cardWidth + metrics.itemGap;
|
return metrics.cardWidth + metrics.itemGap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,6 +248,7 @@ class TvBrowseRailLayout {
|
|||||||
required EpisodePosterMode episodePosterMode,
|
required EpisodePosterMode episodePosterMode,
|
||||||
EpisodePosterMode Function(MediaHub hub)? episodePosterModeForHub,
|
EpisodePosterMode Function(MediaHub hub)? episodePosterModeForHub,
|
||||||
double Function(MediaHub hub)? widePosterScaleForHub,
|
double Function(MediaHub hub)? widePosterScaleForHub,
|
||||||
|
bool fullCardLayout = false,
|
||||||
double tallPosterScale = 1.0,
|
double tallPosterScale = 1.0,
|
||||||
double widePosterScale = 1.0,
|
double widePosterScale = 1.0,
|
||||||
}) {
|
}) {
|
||||||
@@ -244,6 +266,7 @@ class TvBrowseRailLayout {
|
|||||||
episodePosterModeForHub: episodePosterModeForHub,
|
episodePosterModeForHub: episodePosterModeForHub,
|
||||||
widePosterScaleForHub: widePosterScaleForHub,
|
widePosterScaleForHub: widePosterScaleForHub,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
|
fullCardLayout: fullCardLayout,
|
||||||
tallPosterScale: tallPosterScale,
|
tallPosterScale: tallPosterScale,
|
||||||
widePosterScale: widePosterScale,
|
widePosterScale: widePosterScale,
|
||||||
);
|
);
|
||||||
@@ -784,7 +807,11 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
if (_activeHub == null) return const SizedBox.shrink();
|
if (_activeHub == null) return const SizedBox.shrink();
|
||||||
return SettingsBuilder(
|
return SettingsBuilder(
|
||||||
prefs: const [SettingsService.libraryDensity, SettingsService.episodePosterMode],
|
prefs: const [
|
||||||
|
SettingsService.libraryDensity,
|
||||||
|
SettingsService.episodePosterMode,
|
||||||
|
SettingsService.tvFullCardLayout,
|
||||||
|
],
|
||||||
builder: (context) => LayoutBuilder(
|
builder: (context) => LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final svc = SettingsService.instanceOrNull!;
|
final svc = SettingsService.instanceOrNull!;
|
||||||
@@ -800,6 +827,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
final railViewportWidth = (availableWidth + interactionExpansion).clamp(1.0, double.infinity).toDouble();
|
final railViewportWidth = (availableWidth + interactionExpansion).clamp(1.0, double.infinity).toDouble();
|
||||||
final density = svc.read(SettingsService.libraryDensity);
|
final density = svc.read(SettingsService.libraryDensity);
|
||||||
final episodePosterMode = svc.read(SettingsService.episodePosterMode);
|
final episodePosterMode = svc.read(SettingsService.episodePosterMode);
|
||||||
|
final fullCardLayout = svc.read(SettingsService.tvFullCardLayout);
|
||||||
final modes = [for (final hub in widget.hubs) widget.episodePosterModeForHub?.call(hub) ?? episodePosterMode];
|
final modes = [for (final hub in widget.hubs) widget.episodePosterModeForHub?.call(hub) ?? episodePosterMode];
|
||||||
final wideScales = [
|
final wideScales = [
|
||||||
for (final hub in widget.hubs) widget.widePosterScaleForHub?.call(hub) ?? widget.widePosterScale,
|
for (final hub in widget.hubs) widget.widePosterScaleForHub?.call(hub) ?? widget.widePosterScale,
|
||||||
@@ -812,6 +840,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
density: density,
|
density: density,
|
||||||
episodePosterMode: modes[i],
|
episodePosterMode: modes[i],
|
||||||
scale: scale,
|
scale: scale,
|
||||||
|
fullCardLayout: fullCardLayout,
|
||||||
tallPosterScale: widget.tallPosterScale,
|
tallPosterScale: widget.tallPosterScale,
|
||||||
widePosterScale: wideScales[i],
|
widePosterScale: wideScales[i],
|
||||||
),
|
),
|
||||||
@@ -845,7 +874,9 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
TvBrowseRailLayout.railTopPaddingForScale(scale) +
|
TvBrowseRailLayout.railTopPaddingForScale(scale) +
|
||||||
viewportHeight +
|
viewportHeight +
|
||||||
TvBrowseRailLayout.railBottomPaddingForScale(scale);
|
TvBrowseRailLayout.railBottomPaddingForScale(scale);
|
||||||
|
final paintOverflow = fullCardLayout && hasFocus
|
||||||
|
? TvBrowseRailLayout.fullCardFocusPaintOverflowForScale(scale)
|
||||||
|
: 0.0;
|
||||||
return Focus(
|
return Focus(
|
||||||
focusNode: _focusNode,
|
focusNode: _focusNode,
|
||||||
onKeyEvent: _handleKeyEvent,
|
onKeyEvent: _handleKeyEvent,
|
||||||
@@ -874,7 +905,12 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
duration: FocusTheme.getAnimationDuration(context),
|
duration: FocusTheme.getAnimationDuration(context),
|
||||||
curve: Curves.easeOutCubic,
|
curve: Curves.easeOutCubic,
|
||||||
child: ClipRect(
|
child: ClipRect(
|
||||||
clipper: _RailClipper(leftOverflow: horizontalInset, rightOverflow: 0, verticalOverflow: 0),
|
clipper: _RailClipper(
|
||||||
|
leftOverflow: horizontalInset,
|
||||||
|
rightOverflow: paintOverflow,
|
||||||
|
topOverflow: 0,
|
||||||
|
bottomOverflow: paintOverflow,
|
||||||
|
),
|
||||||
child: SizedBox(
|
child: SizedBox(
|
||||||
height: viewportHeight,
|
height: viewportHeight,
|
||||||
child: _buildHubSectionList(
|
child: _buildHubSectionList(
|
||||||
@@ -883,6 +919,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
metricsByHub: metricsByHub,
|
metricsByHub: metricsByHub,
|
||||||
sectionHeights: sectionHeights,
|
sectionHeights: sectionHeights,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
|
fullCardLayout: fullCardLayout,
|
||||||
leftOverflow: horizontalInset,
|
leftOverflow: horizontalInset,
|
||||||
interactionExpansion: interactionExpansion,
|
interactionExpansion: interactionExpansion,
|
||||||
railViewportWidth: railViewportWidth,
|
railViewportWidth: railViewportWidth,
|
||||||
@@ -908,6 +945,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
required List<TvBrowseRailLayoutMetrics> metricsByHub,
|
required List<TvBrowseRailLayoutMetrics> metricsByHub,
|
||||||
required List<double> sectionHeights,
|
required List<double> sectionHeights,
|
||||||
required double scale,
|
required double scale,
|
||||||
|
required bool fullCardLayout,
|
||||||
required double leftOverflow,
|
required double leftOverflow,
|
||||||
required double interactionExpansion,
|
required double interactionExpansion,
|
||||||
required double railViewportWidth,
|
required double railViewportWidth,
|
||||||
@@ -946,6 +984,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
episodePosterMode: modes[hubIndex],
|
episodePosterMode: modes[hubIndex],
|
||||||
metrics: metrics,
|
metrics: metrics,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
|
fullCardLayout: fullCardLayout,
|
||||||
leftOverflow: leftOverflow,
|
leftOverflow: leftOverflow,
|
||||||
interactionExpansion: interactionExpansion,
|
interactionExpansion: interactionExpansion,
|
||||||
railViewportWidth: railViewportWidth,
|
railViewportWidth: railViewportWidth,
|
||||||
@@ -1010,6 +1049,7 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
required EpisodePosterMode episodePosterMode,
|
required EpisodePosterMode episodePosterMode,
|
||||||
required TvBrowseRailLayoutMetrics metrics,
|
required TvBrowseRailLayoutMetrics metrics,
|
||||||
required double scale,
|
required double scale,
|
||||||
|
required bool fullCardLayout,
|
||||||
required double leftOverflow,
|
required double leftOverflow,
|
||||||
required double interactionExpansion,
|
required double interactionExpansion,
|
||||||
required double railViewportWidth,
|
required double railViewportWidth,
|
||||||
@@ -1019,6 +1059,9 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
final inactiveIndex = HubFocusMemory.getForHubOnly(hub.id, totalCount);
|
final inactiveIndex = HubFocusMemory.getForHubOnly(hub.id, totalCount);
|
||||||
final focusedIndex = isActiveHub ? _itemIndex : inactiveIndex;
|
final focusedIndex = isActiveHub ? _itemIndex : inactiveIndex;
|
||||||
final scrollController = _scrollControllerForHub(hub, metrics, railViewportWidth, scale, focusedIndex);
|
final scrollController = _scrollControllerForHub(hub, metrics, railViewportWidth, scale, focusedIndex);
|
||||||
|
final paintOverflow = fullCardLayout && hasFocus && isActiveHub
|
||||||
|
? TvBrowseRailLayout.fullCardFocusPaintOverflowForScale(scale)
|
||||||
|
: 0.0;
|
||||||
_metricsByHub[hub.id] = metrics;
|
_metricsByHub[hub.id] = metrics;
|
||||||
_scaleByHub[hub.id] = scale;
|
_scaleByHub[hub.id] = scale;
|
||||||
|
|
||||||
@@ -1030,8 +1073,8 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
child: ClipRect(
|
child: ClipRect(
|
||||||
clipper: _RailClipper(
|
clipper: _RailClipper(
|
||||||
leftOverflow: leftOverflow,
|
leftOverflow: leftOverflow,
|
||||||
rightOverflow: metrics.railEdgePadding + metrics.cardWidth + metrics.itemGap,
|
rightOverflow: metrics.railEdgePadding + metrics.cardWidth + metrics.itemGap + paintOverflow,
|
||||||
verticalOverflow: metrics.focusExtra,
|
verticalOverflow: fullCardLayout ? math.max(metrics.focusExtra, paintOverflow) : metrics.focusExtra,
|
||||||
),
|
),
|
||||||
child: HorizontalScrollWithArrows(
|
child: HorizontalScrollWithArrows(
|
||||||
controller: scrollController,
|
controller: scrollController,
|
||||||
@@ -1048,79 +1091,69 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
if (itemIndex == hub.items.length) {
|
if (itemIndex == hub.items.length) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(right: metrics.itemGap),
|
padding: EdgeInsets.only(right: metrics.itemGap),
|
||||||
child: FocusBuilders.buildLockedFocusWrapper(
|
child: Align(
|
||||||
context: context,
|
alignment: Alignment.centerLeft,
|
||||||
isFocused: isFocused,
|
child: _buildViewAllButton(
|
||||||
onTap: () {
|
context,
|
||||||
_selectHubItem(hub, hubIndex, itemIndex);
|
isFocused: isFocused,
|
||||||
_navigateToHubDetail(hub);
|
scale: scale,
|
||||||
},
|
onTap: () {
|
||||||
child: SizedBox(
|
_selectHubItem(hub, hubIndex, itemIndex);
|
||||||
width: 132 * scale,
|
_navigateToHubDetail(hub);
|
||||||
height: metrics.containerHeight - metrics.itemGap,
|
},
|
||||||
child: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
AppIcon(
|
|
||||||
Symbols.arrow_forward_rounded,
|
|
||||||
fill: 1,
|
|
||||||
size: 42 * scale,
|
|
||||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.72),
|
|
||||||
),
|
|
||||||
SizedBox(height: 6 * scale),
|
|
||||||
Text(
|
|
||||||
t.common.viewAll,
|
|
||||||
style: TextStyle(
|
|
||||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha: 0.72),
|
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
final item = hub.items[itemIndex];
|
final item = hub.items[itemIndex];
|
||||||
|
final focusableCard = FocusBuilders.buildLockedFocusWrapper(
|
||||||
|
context: context,
|
||||||
|
isFocused: isFocused,
|
||||||
|
borderRadius: tokens(context).radiusSm,
|
||||||
|
focusScale: fullCardLayout ? TvBrowseRailLayout.fullCardFocusScale : FocusTheme.focusScale,
|
||||||
|
focusBorderStrokeAlign: fullCardLayout ? BorderSide.strokeAlignOutside : BorderSide.strokeAlignInside,
|
||||||
|
useFocusGlow: fullCardLayout,
|
||||||
|
useForegroundFocusDecoration: fullCardLayout,
|
||||||
|
onTap: () {
|
||||||
|
_selectHubItem(hub, hubIndex, itemIndex);
|
||||||
|
unawaited(_activateCurrentItem());
|
||||||
|
},
|
||||||
|
onLongPress: metrics.isPersonHub
|
||||||
|
? null
|
||||||
|
: () {
|
||||||
|
_selectHubItem(hub, hubIndex, itemIndex);
|
||||||
|
_cardKeyFor(hub, itemIndex).currentState?.showContextMenu();
|
||||||
|
},
|
||||||
|
child: metrics.isPersonHub
|
||||||
|
? _buildPersonCard(
|
||||||
|
context,
|
||||||
|
item,
|
||||||
|
cardWidth: metrics.cardWidth,
|
||||||
|
imageSize: metrics.posterHeight,
|
||||||
|
scale: scale,
|
||||||
|
fullCardLayout: fullCardLayout,
|
||||||
|
)
|
||||||
|
: MediaCard(
|
||||||
|
key: _cardKeyFor(hub, itemIndex),
|
||||||
|
item: item,
|
||||||
|
width: metrics.cardWidth,
|
||||||
|
height: metrics.posterHeight,
|
||||||
|
onRefresh: widget.onRefresh,
|
||||||
|
onRemoveFromContinueWatching: widget.onRemoveFromContinueWatching,
|
||||||
|
forceGridMode: true,
|
||||||
|
fullBleedImage: fullCardLayout,
|
||||||
|
isInContinueWatching: widget.isContinueWatchingHub?.call(hub) ?? false,
|
||||||
|
mixedHubContext: metrics.isMixedHub,
|
||||||
|
episodePosterModeOverride: episodePosterMode,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.only(right: metrics.itemGap),
|
padding: EdgeInsets.only(right: metrics.itemGap),
|
||||||
child: MouseRegion(
|
child: MouseRegion(
|
||||||
onEnter: (_) => _setHoveredItem(hub, itemIndex),
|
onEnter: (_) => _setHoveredItem(hub, itemIndex),
|
||||||
child: FocusBuilders.buildLockedFocusWrapper(
|
child: Align(alignment: Alignment.topLeft, child: focusableCard),
|
||||||
context: context,
|
|
||||||
isFocused: isFocused,
|
|
||||||
onTap: () {
|
|
||||||
_selectHubItem(hub, hubIndex, itemIndex);
|
|
||||||
unawaited(_activateCurrentItem());
|
|
||||||
},
|
|
||||||
onLongPress: metrics.isPersonHub
|
|
||||||
? null
|
|
||||||
: () {
|
|
||||||
_selectHubItem(hub, hubIndex, itemIndex);
|
|
||||||
_cardKeyFor(hub, itemIndex).currentState?.showContextMenu();
|
|
||||||
},
|
|
||||||
child: metrics.isPersonHub
|
|
||||||
? _buildPersonCard(
|
|
||||||
context,
|
|
||||||
item,
|
|
||||||
cardWidth: metrics.cardWidth,
|
|
||||||
imageSize: metrics.posterHeight,
|
|
||||||
scale: scale,
|
|
||||||
)
|
|
||||||
: MediaCard(
|
|
||||||
key: _cardKeyFor(hub, itemIndex),
|
|
||||||
item: item,
|
|
||||||
width: metrics.cardWidth,
|
|
||||||
height: metrics.posterHeight,
|
|
||||||
onRefresh: widget.onRefresh,
|
|
||||||
onRemoveFromContinueWatching: widget.onRemoveFromContinueWatching,
|
|
||||||
forceGridMode: true,
|
|
||||||
isInContinueWatching: widget.isContinueWatchingHub?.call(hub) ?? false,
|
|
||||||
mixedHubContext: metrics.isMixedHub,
|
|
||||||
episodePosterModeOverride: episodePosterMode,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -1137,10 +1170,81 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
required double cardWidth,
|
required double cardWidth,
|
||||||
required double imageSize,
|
required double imageSize,
|
||||||
required double scale,
|
required double scale,
|
||||||
|
required bool fullCardLayout,
|
||||||
}) {
|
}) {
|
||||||
final theme = Theme.of(context);
|
final theme = Theme.of(context);
|
||||||
final characterName = item.parentTitle;
|
final characterName = item.parentTitle;
|
||||||
|
|
||||||
|
if (fullCardLayout) {
|
||||||
|
return SizedBox(
|
||||||
|
width: cardWidth,
|
||||||
|
height: imageSize,
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(tokens(context).radiusSm),
|
||||||
|
child: Stack(
|
||||||
|
fit: StackFit.expand,
|
||||||
|
children: [
|
||||||
|
OptimizedMediaImage(
|
||||||
|
client: context.tryGetMediaClientWithFallback(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: MainAxisSize.min,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
item.displayTitle,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 13 * scale,
|
||||||
|
height: 1.1,
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (characterName != null && characterName.isNotEmpty) ...[
|
||||||
|
SizedBox(height: 2 * scale),
|
||||||
|
Text(
|
||||||
|
characterName,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white.withValues(alpha: 0.82),
|
||||||
|
fontSize: 11 * scale,
|
||||||
|
height: 1.1,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
width: cardWidth,
|
width: cardWidth,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
@@ -1191,6 +1295,78 @@ class TvBrowseRailState extends State<TvBrowseRail> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildViewAllButton(
|
||||||
|
BuildContext context, {
|
||||||
|
required bool isFocused,
|
||||||
|
required double scale,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final duration = FocusTheme.getAnimationDuration(context);
|
||||||
|
final width = TvBrowseRailLayout.viewAllItemWidthForScale(scale);
|
||||||
|
final height = TvBrowseRailLayout.viewAllPillHeightForScale(scale);
|
||||||
|
final foreground = isFocused ? theme.colorScheme.primary : theme.colorScheme.onSurface.withValues(alpha: 0.78);
|
||||||
|
final background = isFocused
|
||||||
|
? theme.colorScheme.primary.withValues(alpha: 0.20)
|
||||||
|
: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.42);
|
||||||
|
|
||||||
|
return ClickableCursor(
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: AnimatedScale(
|
||||||
|
scale: isFocused ? 1.04 : 1.0,
|
||||||
|
duration: duration,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
child: AnimatedContainer(
|
||||||
|
duration: duration,
|
||||||
|
curve: Curves.easeOutCubic,
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: (12 * scale).clamp(10, 16).toDouble()),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: background,
|
||||||
|
borderRadius: BorderRadius.circular(height / 2),
|
||||||
|
boxShadow: isFocused
|
||||||
|
? [
|
||||||
|
BoxShadow(
|
||||||
|
color: theme.colorScheme.primary.withValues(alpha: 0.20),
|
||||||
|
blurRadius: 18,
|
||||||
|
spreadRadius: 1,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: null,
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
t.common.viewAll,
|
||||||
|
maxLines: 1,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
style: TextStyle(
|
||||||
|
color: foreground,
|
||||||
|
fontSize: (13 * scale).clamp(12, 16).toDouble(),
|
||||||
|
fontWeight: FontWeight.w800,
|
||||||
|
letterSpacing: 0.1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: (5 * scale).clamp(4, 7).toDouble()),
|
||||||
|
AppIcon(
|
||||||
|
Symbols.arrow_forward_rounded,
|
||||||
|
fill: 1,
|
||||||
|
size: (18 * scale).clamp(16, 22).toDouble(),
|
||||||
|
color: foreground,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _RailBackgroundBleed extends StatelessWidget {
|
class _RailBackgroundBleed extends StatelessWidget {
|
||||||
@@ -1226,18 +1402,27 @@ class _RailBackgroundBleed extends StatelessWidget {
|
|||||||
class _RailClipper extends CustomClipper<Rect> {
|
class _RailClipper extends CustomClipper<Rect> {
|
||||||
final double leftOverflow;
|
final double leftOverflow;
|
||||||
final double rightOverflow;
|
final double rightOverflow;
|
||||||
final double verticalOverflow;
|
final double topOverflow;
|
||||||
|
final double bottomOverflow;
|
||||||
|
|
||||||
const _RailClipper({this.leftOverflow = 0, required this.rightOverflow, required this.verticalOverflow});
|
const _RailClipper({
|
||||||
|
this.leftOverflow = 0,
|
||||||
|
required this.rightOverflow,
|
||||||
|
double verticalOverflow = 0,
|
||||||
|
double? topOverflow,
|
||||||
|
double? bottomOverflow,
|
||||||
|
}) : topOverflow = topOverflow ?? verticalOverflow,
|
||||||
|
bottomOverflow = bottomOverflow ?? verticalOverflow;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Rect getClip(Size size) =>
|
Rect getClip(Size size) =>
|
||||||
Rect.fromLTRB(-leftOverflow, -verticalOverflow, size.width + rightOverflow, size.height + verticalOverflow);
|
Rect.fromLTRB(-leftOverflow, -topOverflow, size.width + rightOverflow, size.height + bottomOverflow);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool shouldReclip(covariant _RailClipper oldClipper) {
|
bool shouldReclip(covariant _RailClipper oldClipper) {
|
||||||
return oldClipper.leftOverflow != leftOverflow ||
|
return oldClipper.leftOverflow != leftOverflow ||
|
||||||
oldClipper.rightOverflow != rightOverflow ||
|
oldClipper.rightOverflow != rightOverflow ||
|
||||||
oldClipper.verticalOverflow != verticalOverflow;
|
oldClipper.topOverflow != topOverflow ||
|
||||||
|
oldClipper.bottomOverflow != bottomOverflow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ void main() {
|
|||||||
hubs: [hub],
|
hubs: [hub],
|
||||||
density: LibraryDensity.max,
|
density: LibraryDensity.max,
|
||||||
episodePosterMode: settings.read(SettingsService.episodePosterMode),
|
episodePosterMode: settings.read(SettingsService.episodePosterMode),
|
||||||
|
fullCardLayout: settings.read(SettingsService.tvFullCardLayout),
|
||||||
tallPosterScale: TvBrowseRailLayout.compactTallPosterScale,
|
tallPosterScale: TvBrowseRailLayout.compactTallPosterScale,
|
||||||
);
|
);
|
||||||
final minimumSpotlightBottom = railHeight + (8 * scale);
|
final minimumSpotlightBottom = railHeight + (8 * scale);
|
||||||
|
|||||||
@@ -74,6 +74,14 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('SettingsService TV card defaults', () {
|
||||||
|
test('full card layout starts disabled', () async {
|
||||||
|
final settings = await SettingsService.getInstance();
|
||||||
|
|
||||||
|
expect(settings.read(SettingsService.tvFullCardLayout), isFalse);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
group('SettingsService companion remote prefs', () {
|
group('SettingsService companion remote prefs', () {
|
||||||
test('last manual host address trims whitespace and drops blanks', () async {
|
test('last manual host address trims whitespace and drops blanks', () async {
|
||||||
final settings = await SettingsService.getInstance();
|
final settings = await SettingsService.getInstance();
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:plezy/focus/input_mode_tracker.dart';
|
||||||
|
import 'package:plezy/media/media_backend.dart';
|
||||||
|
import 'package:plezy/media/media_item.dart';
|
||||||
|
import 'package:plezy/media/media_kind.dart';
|
||||||
|
import 'package:plezy/services/settings_service.dart';
|
||||||
|
import 'package:plezy/theme/mono_theme.dart';
|
||||||
|
import 'package:plezy/utils/layout_constants.dart';
|
||||||
|
import 'package:plezy/utils/platform_detector.dart';
|
||||||
|
import 'package:plezy/widgets/focusable_media_card.dart';
|
||||||
|
import 'package:plezy/widgets/media_card.dart';
|
||||||
|
import 'package:plezy/widgets/media_grid_delegate.dart';
|
||||||
|
|
||||||
|
import '../test_helpers/prefs.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
TestWidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
setUp(() async {
|
||||||
|
resetSharedPreferencesForTest();
|
||||||
|
SettingsService.resetForTesting();
|
||||||
|
await SettingsService.getInstance();
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('full bleed grid delegates use image aspect ratios', () {
|
||||||
|
expect(MediaGridDelegate.aspectRatioFor(fullBleedImage: true), GridLayoutConstants.fullCardPosterAspectRatio);
|
||||||
|
expect(
|
||||||
|
MediaGridDelegate.aspectRatioFor(useWideAspectRatio: true, fullBleedImage: true),
|
||||||
|
GridLayoutConstants.episodeThumbnailAspectRatio,
|
||||||
|
);
|
||||||
|
expect(MediaGridDelegate.aspectRatioFor(useWideAspectRatio: true), GridLayoutConstants.episodeGridCellAspectRatio);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('full bleed grid delegates use scaled gutters', (tester) async {
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(true);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
tester.view.physicalSize = const Size(1280, 720);
|
||||||
|
addTearDown(() {
|
||||||
|
tester.view.resetDevicePixelRatio();
|
||||||
|
tester.view.resetPhysicalSize();
|
||||||
|
});
|
||||||
|
|
||||||
|
late SliverGridDelegateWithMaxCrossAxisExtent delegate;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_TestApp(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
delegate = MediaGridDelegate.createDelegate(
|
||||||
|
context: context,
|
||||||
|
density: LibraryDensity.defaultValue,
|
||||||
|
fullBleedImage: true,
|
||||||
|
);
|
||||||
|
return const SizedBox.shrink();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(delegate.crossAxisSpacing, greaterThan(0));
|
||||||
|
expect(delegate.mainAxisSpacing, delegate.crossAxisSpacing);
|
||||||
|
expect(delegate.crossAxisSpacing, GridLayoutConstants.fullCardGridSpacingForScale(0.85));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('full bleed grid media cards hide text when constrained by a grid cell', (tester) async {
|
||||||
|
final item = MediaItem(
|
||||||
|
id: 'movie_1',
|
||||||
|
backend: MediaBackend.plex,
|
||||||
|
kind: MediaKind.movie,
|
||||||
|
title: 'Hidden Movie',
|
||||||
|
year: 2024,
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_TestApp(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 200,
|
||||||
|
height: 300,
|
||||||
|
child: MediaCard(item: item, forceGridMode: true, fullBleedImage: true, isOffline: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Hidden Movie'), findsNothing);
|
||||||
|
expect(find.text('2024'), findsNothing);
|
||||||
|
expect(tester.getSize(find.byType(InkWell)), const Size(200, 300));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('standard grid media cards still show text', (tester) async {
|
||||||
|
final item = MediaItem(id: 'movie_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Visible Movie');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_TestApp(
|
||||||
|
child: SizedBox(width: 200, height: 330, child: MediaCard(item: item, forceGridMode: true, isOffline: true)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Visible Movie'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('full bleed flag does not hide list media card text', (tester) async {
|
||||||
|
final item = MediaItem(id: 'movie_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'List Movie');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
_TestApp(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 420,
|
||||||
|
height: 160,
|
||||||
|
child: MediaCard(item: item, forceListMode: true, fullBleedImage: true, isOffline: true),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('List Movie'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('full bleed focusable media card uses outside ring and local glow', (tester) async {
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(true);
|
||||||
|
final focusNode = FocusNode(debugLabel: 'full_bleed_card');
|
||||||
|
addTearDown(focusNode.dispose);
|
||||||
|
final item = MediaItem(id: 'movie_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Focused Movie');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
InputModeTracker(
|
||||||
|
child: _TestApp(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 200,
|
||||||
|
height: 300,
|
||||||
|
child: FocusableMediaCard(
|
||||||
|
item: item,
|
||||||
|
forceGridMode: true,
|
||||||
|
fullBleedImage: true,
|
||||||
|
focusNode: focusNode,
|
||||||
|
isOffline: true,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
focusNode.requestFocus();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final focusDecoration = find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is AnimatedContainer &&
|
||||||
|
widget.decoration is BoxDecoration &&
|
||||||
|
widget.foregroundDecoration is BoxDecoration,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(focusDecoration, findsOneWidget);
|
||||||
|
final focusedContainer = tester.widget<AnimatedContainer>(focusDecoration);
|
||||||
|
final glowDecoration = focusedContainer.decoration as BoxDecoration;
|
||||||
|
final foregroundDecoration = focusedContainer.foregroundDecoration as BoxDecoration;
|
||||||
|
final border = foregroundDecoration.border as Border;
|
||||||
|
|
||||||
|
expect(glowDecoration.boxShadow, hasLength(2));
|
||||||
|
expect(glowDecoration.boxShadow!.first.color, isNot(Colors.transparent));
|
||||||
|
expect(border.top.strokeAlign, BorderSide.strokeAlignOutside);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TestApp extends StatelessWidget {
|
||||||
|
final Widget child;
|
||||||
|
|
||||||
|
const _TestApp({required this.child});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
theme: monoTheme(dark: true),
|
||||||
|
home: Scaffold(body: Center(child: child)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:plezy/focus/dpad_navigator.dart';
|
import 'package:plezy/focus/dpad_navigator.dart';
|
||||||
|
import 'package:plezy/focus/input_mode_tracker.dart';
|
||||||
import 'package:plezy/focus/locked_hub_controller.dart';
|
import 'package:plezy/focus/locked_hub_controller.dart';
|
||||||
import 'package:plezy/media/media_backend.dart';
|
import 'package:plezy/media/media_backend.dart';
|
||||||
import 'package:plezy/media/media_hub.dart';
|
import 'package:plezy/media/media_hub.dart';
|
||||||
@@ -12,6 +13,7 @@ import 'package:plezy/services/data_aggregation_service.dart';
|
|||||||
import 'package:plezy/services/multi_server_manager.dart';
|
import 'package:plezy/services/multi_server_manager.dart';
|
||||||
import 'package:plezy/services/settings_service.dart';
|
import 'package:plezy/services/settings_service.dart';
|
||||||
import 'package:plezy/theme/mono_theme.dart';
|
import 'package:plezy/theme/mono_theme.dart';
|
||||||
|
import 'package:plezy/utils/platform_detector.dart';
|
||||||
import 'package:plezy/widgets/side_navigation_rail.dart';
|
import 'package:plezy/widgets/side_navigation_rail.dart';
|
||||||
import 'package:plezy/widgets/tv_browse_rail.dart';
|
import 'package:plezy/widgets/tv_browse_rail.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
@@ -176,6 +178,40 @@ void main() {
|
|||||||
expect(compactHeight, lessThan(defaultHeight));
|
expect(compactHeight, lessThan(defaultHeight));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('full card layout removes label reserve and preserves episode poster mode', () {
|
||||||
|
final episode = MediaItem(
|
||||||
|
id: 'episode_1',
|
||||||
|
backend: MediaBackend.plex,
|
||||||
|
kind: MediaKind.episode,
|
||||||
|
title: 'Episode 1',
|
||||||
|
thumbPath: '/episode-thumb',
|
||||||
|
grandparentThumbPath: '/show-poster',
|
||||||
|
);
|
||||||
|
final hub = MediaHub(id: 'episodes', title: 'Episodes', type: 'episode', items: [episode], size: 1);
|
||||||
|
|
||||||
|
final detailed = TvBrowseRailLayout.metricsForHub(
|
||||||
|
hub: hub,
|
||||||
|
availableWidth: 1040,
|
||||||
|
density: LibraryDensity.defaultValue,
|
||||||
|
episodePosterMode: EpisodePosterMode.episodeThumbnail,
|
||||||
|
scale: 0.85,
|
||||||
|
);
|
||||||
|
final full = TvBrowseRailLayout.metricsForHub(
|
||||||
|
hub: hub,
|
||||||
|
availableWidth: 1040,
|
||||||
|
density: LibraryDensity.defaultValue,
|
||||||
|
episodePosterMode: EpisodePosterMode.episodeThumbnail,
|
||||||
|
scale: 0.85,
|
||||||
|
fullCardLayout: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(full.height, lessThan(detailed.height));
|
||||||
|
expect(full.useWideLayout, isTrue);
|
||||||
|
expect(detailed.itemGap, 0);
|
||||||
|
expect(full.itemGap, closeTo(12 * 0.85, 0.001));
|
||||||
|
expect(full.posterHeight, closeTo(full.posterWidth * 9 / 16, 0.001));
|
||||||
|
});
|
||||||
|
|
||||||
test('compact wide poster scale makes clips match compact episode thumbnails', () {
|
test('compact wide poster scale makes clips match compact episode thumbnails', () {
|
||||||
final episode = MediaItem(
|
final episode = MediaItem(
|
||||||
id: 'episode_1',
|
id: 'episode_1',
|
||||||
@@ -275,6 +311,326 @@ void main() {
|
|||||||
expect(headerText.style?.color, theme.colorScheme.onSurface);
|
expect(headerText.style?.color, theme.colorScheme.onSurface);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('full card layout hides media text and overlays actor text when enabled', (tester) async {
|
||||||
|
await SettingsService.instanceOrNull!.write(SettingsService.tvFullCardLayout, true);
|
||||||
|
|
||||||
|
final serverManager = MultiServerManager();
|
||||||
|
final movie = MediaItem(id: 'movie_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Hidden Movie');
|
||||||
|
final actor = MediaItem(
|
||||||
|
id: 'actor_1',
|
||||||
|
backend: MediaBackend.plex,
|
||||||
|
kind: MediaKind.unknown,
|
||||||
|
title: 'Actor Name',
|
||||||
|
parentTitle: 'Character Name',
|
||||||
|
);
|
||||||
|
final movieHub = MediaHub(id: 'movies', title: 'Movies', type: 'movie', items: [movie], size: 1);
|
||||||
|
final actorHub = MediaHub(id: 'actors', title: 'Cast', type: 'person', items: [actor], size: 1);
|
||||||
|
|
||||||
|
Widget rail(MediaHub hub) {
|
||||||
|
return ChangeNotifierProvider<MultiServerProvider>(
|
||||||
|
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: monoTheme(dark: true),
|
||||||
|
home: Scaffold(
|
||||||
|
body: SizedBox(
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
child: TvBrowseRail(hubs: [hub], iconForHub: (_, _) => Icons.movie_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpWidget(rail(movieHub));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Hidden Movie'), findsNothing);
|
||||||
|
|
||||||
|
await tester.pumpWidget(rail(actorHub));
|
||||||
|
await tester.pump();
|
||||||
|
expect(find.text('Actor Name'), findsOneWidget);
|
||||||
|
expect(find.text('Character Name'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('full card focus adds outside ring, local glow, and card image scale', (tester) async {
|
||||||
|
await SettingsService.instanceOrNull!.write(SettingsService.tvFullCardLayout, true);
|
||||||
|
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(true);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
tester.view.physicalSize = const Size(1280, 720);
|
||||||
|
addTearDown(() {
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(null);
|
||||||
|
tester.view.resetDevicePixelRatio();
|
||||||
|
tester.view.resetPhysicalSize();
|
||||||
|
});
|
||||||
|
|
||||||
|
final serverManager = MultiServerManager();
|
||||||
|
final movie = MediaItem(id: 'movie_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Movie');
|
||||||
|
final hub = MediaHub(id: 'movies', title: 'Movies', type: 'movie', items: [movie], size: 1);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ChangeNotifierProvider<MultiServerProvider>(
|
||||||
|
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||||
|
child: InputModeTracker(
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: monoTheme(dark: true),
|
||||||
|
home: Scaffold(
|
||||||
|
body: SizedBox(
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
child: TvBrowseRail(hubs: [hub], autofocus: true, iconForHub: (_, _) => Icons.movie_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final focusDecoration = find.byWidgetPredicate(
|
||||||
|
(widget) =>
|
||||||
|
widget is AnimatedContainer &&
|
||||||
|
widget.decoration is BoxDecoration &&
|
||||||
|
widget.foregroundDecoration is BoxDecoration,
|
||||||
|
);
|
||||||
|
final scale = TvBrowseRailLayout.scaleForSize(tester.view.physicalSize / tester.view.devicePixelRatio);
|
||||||
|
final metrics = TvBrowseRailLayout.metricsForHub(
|
||||||
|
hub: hub,
|
||||||
|
availableWidth: 1280 - TvBrowseRailLayout.horizontalInsetForScale(scale),
|
||||||
|
density: LibraryDensity.defaultValue,
|
||||||
|
episodePosterMode: EpisodePosterMode.seriesPoster,
|
||||||
|
scale: scale,
|
||||||
|
fullCardLayout: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(focusDecoration, findsOneWidget);
|
||||||
|
final focusDecorationWidget = tester.widget<AnimatedContainer>(focusDecoration);
|
||||||
|
final glowDecoration = focusDecorationWidget.decoration as BoxDecoration;
|
||||||
|
final foregroundDecoration = focusDecorationWidget.foregroundDecoration as BoxDecoration;
|
||||||
|
final border = foregroundDecoration.border as Border;
|
||||||
|
final focusDecorationSize = tester.getSize(focusDecoration);
|
||||||
|
final focusScale = tester.widget<AnimatedScale>(
|
||||||
|
find.ancestor(of: focusDecoration, matching: find.byType(AnimatedScale)).first,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(border.top.strokeAlign, BorderSide.strokeAlignOutside);
|
||||||
|
expect(find.byType(ShaderMask), findsNothing);
|
||||||
|
expect(find.byType(CompositedTransformFollower), findsNothing);
|
||||||
|
expect(find.byType(CompositedTransformTarget), findsNothing);
|
||||||
|
expect(glowDecoration.boxShadow, hasLength(2));
|
||||||
|
expect(glowDecoration.boxShadow!.first.color, isNot(Colors.transparent));
|
||||||
|
expect(focusScale.scale, closeTo(1.03, 0.0001));
|
||||||
|
expect(focusDecorationSize.width, closeTo(metrics.cardWidth, 0.001));
|
||||||
|
expect(focusDecorationSize.height, closeTo(metrics.posterHeight, 0.001));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('vertical hub viewport keeps top clipping while switching hubs', (tester) async {
|
||||||
|
await SettingsService.instanceOrNull!.write(SettingsService.tvFullCardLayout, true);
|
||||||
|
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(true);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
tester.view.physicalSize = const Size(1280, 720);
|
||||||
|
addTearDown(() {
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(null);
|
||||||
|
tester.view.resetDevicePixelRatio();
|
||||||
|
tester.view.resetPhysicalSize();
|
||||||
|
});
|
||||||
|
|
||||||
|
final serverManager = MultiServerManager();
|
||||||
|
final firstMovie = MediaItem(id: 'movie_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Movie 1');
|
||||||
|
final secondMovie = MediaItem(id: 'movie_2', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Movie 2');
|
||||||
|
final firstHub = MediaHub(id: 'movies_1', title: 'Movies 1', type: 'movie', items: [firstMovie], size: 1);
|
||||||
|
final secondHub = MediaHub(id: 'movies_2', title: 'Movies 2', type: 'movie', items: [secondMovie], size: 1);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ChangeNotifierProvider<MultiServerProvider>(
|
||||||
|
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||||
|
child: InputModeTracker(
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: monoTheme(dark: true),
|
||||||
|
home: Scaffold(
|
||||||
|
body: SizedBox(
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
child: TvBrowseRail(
|
||||||
|
hubs: [firstHub, secondHub],
|
||||||
|
autofocus: true,
|
||||||
|
iconForHub: (_, _) => Icons.movie_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowDown);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 16));
|
||||||
|
|
||||||
|
final scale = TvBrowseRailLayout.scaleForSize(tester.view.physicalSize / tester.view.devicePixelRatio);
|
||||||
|
final expectedPaintOverflow = TvBrowseRailLayout.fullCardFocusPaintOverflowForScale(scale);
|
||||||
|
final expectedLeftOverflow = TvBrowseRailLayout.horizontalInsetForScale(scale);
|
||||||
|
final verticalViewportClip = tester
|
||||||
|
.widgetList<ClipRect>(
|
||||||
|
find.ancestor(of: find.byKey(const ValueKey('tv_browse_rail_vertical')), matching: find.byType(ClipRect)),
|
||||||
|
)
|
||||||
|
.singleWhere((widget) => widget.clipper != null);
|
||||||
|
final clipRectSize = tester.getSize(find.byWidget(verticalViewportClip));
|
||||||
|
final clip = verticalViewportClip.clipper!.getClip(clipRectSize);
|
||||||
|
|
||||||
|
expect(clip.left, closeTo(-expectedLeftOverflow, 0.001));
|
||||||
|
expect(clip.left, greaterThan(-expectedPaintOverflow));
|
||||||
|
expect(clip.top, 0);
|
||||||
|
expect(clip.bottom, greaterThanOrEqualTo(clipRectSize.height + expectedPaintOverflow));
|
||||||
|
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowDown);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byType(CompositedTransformFollower), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('detailed card layout can still show media text', (tester) async {
|
||||||
|
await SettingsService.instanceOrNull!.write(SettingsService.tvFullCardLayout, false);
|
||||||
|
|
||||||
|
final serverManager = MultiServerManager();
|
||||||
|
final movie = MediaItem(id: 'movie_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Visible Movie');
|
||||||
|
final hub = MediaHub(id: 'movies', title: 'Movies', type: 'movie', items: [movie], size: 1);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ChangeNotifierProvider<MultiServerProvider>(
|
||||||
|
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: monoTheme(dark: true),
|
||||||
|
home: Scaffold(
|
||||||
|
body: SizedBox(
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
child: TvBrowseRail(hubs: [hub], iconForHub: (_, _) => Icons.movie_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(find.text('Visible Movie'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('detailed card focus ring wraps card content height', (tester) async {
|
||||||
|
await SettingsService.instanceOrNull!.write(SettingsService.tvFullCardLayout, false);
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(true);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
tester.view.physicalSize = const Size(1280, 720);
|
||||||
|
addTearDown(() {
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(null);
|
||||||
|
tester.view.resetDevicePixelRatio();
|
||||||
|
tester.view.resetPhysicalSize();
|
||||||
|
});
|
||||||
|
|
||||||
|
final serverManager = MultiServerManager();
|
||||||
|
final movie = MediaItem(
|
||||||
|
id: 'movie_1',
|
||||||
|
backend: MediaBackend.plex,
|
||||||
|
kind: MediaKind.movie,
|
||||||
|
title: 'Visible Movie',
|
||||||
|
year: 2024,
|
||||||
|
);
|
||||||
|
final hub = MediaHub(id: 'movies', title: 'Movies', type: 'movie', items: [movie], size: 1);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ChangeNotifierProvider<MultiServerProvider>(
|
||||||
|
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||||
|
child: InputModeTracker(
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: monoTheme(dark: true),
|
||||||
|
home: Scaffold(
|
||||||
|
body: SizedBox(
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
child: TvBrowseRail(hubs: [hub], autofocus: true, iconForHub: (_, _) => Icons.movie_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final focusDecoration = find.ancestor(
|
||||||
|
of: find.text('Visible Movie'),
|
||||||
|
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;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(focusDecoration, findsOneWidget);
|
||||||
|
expect(find.text('2024'), findsOneWidget);
|
||||||
|
|
||||||
|
final focusRect = tester.getRect(focusDecoration);
|
||||||
|
final subtitleRect = tester.getRect(find.text('2024'));
|
||||||
|
expect(focusRect.bottom - subtitleRect.bottom, lessThan(8));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('view all item uses compact pill focus style', (tester) async {
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(true);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
tester.view.physicalSize = const Size(1280, 720);
|
||||||
|
addTearDown(() {
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(null);
|
||||||
|
tester.view.resetDevicePixelRatio();
|
||||||
|
tester.view.resetPhysicalSize();
|
||||||
|
});
|
||||||
|
|
||||||
|
final serverManager = MultiServerManager();
|
||||||
|
final movie = MediaItem(id: 'movie_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Movie');
|
||||||
|
final hub = MediaHub(id: 'movies', title: 'Movies', type: 'movie', items: [movie], size: 2, more: true);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ChangeNotifierProvider<MultiServerProvider>(
|
||||||
|
create: (_) => MultiServerProvider(serverManager, DataAggregationService(serverManager)),
|
||||||
|
child: InputModeTracker(
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: monoTheme(dark: true),
|
||||||
|
home: Scaffold(
|
||||||
|
body: SizedBox(
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
child: TvBrowseRail(hubs: [hub], autofocus: true, iconForHub: (_, _) => Icons.movie_rounded),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
tester.state<TvBrowseRailState>(find.byType(TvBrowseRail)).requestFocus();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.sendKeyDownEvent(LogicalKeyboardKey.arrowRight);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.sendKeyUpEvent(LogicalKeyboardKey.arrowRight);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final viewAllText = find.text('View All');
|
||||||
|
final pill = find.ancestor(of: viewAllText, matching: find.byType(AnimatedContainer));
|
||||||
|
final scale = TvBrowseRailLayout.scaleForSize(tester.view.physicalSize / tester.view.devicePixelRatio);
|
||||||
|
|
||||||
|
expect(viewAllText, findsOneWidget);
|
||||||
|
expect(pill, findsOneWidget);
|
||||||
|
final pillWidget = tester.widget<AnimatedContainer>(pill);
|
||||||
|
final decoration = pillWidget.decoration as BoxDecoration;
|
||||||
|
final pillSize = tester.getSize(pill);
|
||||||
|
|
||||||
|
expect(decoration.border, isNull);
|
||||||
|
expect(decoration.boxShadow, isNotNull);
|
||||||
|
expect(pillSize.width, closeTo(TvBrowseRailLayout.viewAllItemWidthForScale(scale), 0.001));
|
||||||
|
expect(pillSize.width, lessThan(132 * scale));
|
||||||
|
expect(pillSize.height, closeTo(TvBrowseRailLayout.viewAllPillHeightForScale(scale), 0.001));
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('inactive hub contents render at reduced opacity', (tester) async {
|
testWidgets('inactive hub contents render at reduced opacity', (tester) async {
|
||||||
final serverManager = MultiServerManager();
|
final serverManager = MultiServerManager();
|
||||||
final firstItem = MediaItem(id: 'movie_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Movie 1');
|
final firstItem = MediaItem(id: 'movie_1', backend: MediaBackend.plex, kind: MediaKind.movie, title: 'Movie 1');
|
||||||
@@ -500,6 +856,7 @@ void main() {
|
|||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
final scale = TvBrowseRailLayout.scaleForSize(tester.view.physicalSize / tester.view.devicePixelRatio);
|
final scale = TvBrowseRailLayout.scaleForSize(tester.view.physicalSize / tester.view.devicePixelRatio);
|
||||||
|
final fullCardLayout = SettingsService.instanceOrNull!.read(SettingsService.tvFullCardLayout);
|
||||||
final availableWidth = 700 - TvBrowseRailLayout.horizontalInsetForScale(scale);
|
final availableWidth = 700 - TvBrowseRailLayout.horizontalInsetForScale(scale);
|
||||||
final movieMetrics = TvBrowseRailLayout.metricsForHub(
|
final movieMetrics = TvBrowseRailLayout.metricsForHub(
|
||||||
hub: movieHub,
|
hub: movieHub,
|
||||||
@@ -507,6 +864,7 @@ void main() {
|
|||||||
density: LibraryDensity.defaultValue,
|
density: LibraryDensity.defaultValue,
|
||||||
episodePosterMode: EpisodePosterMode.episodeThumbnail,
|
episodePosterMode: EpisodePosterMode.episodeThumbnail,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
|
fullCardLayout: fullCardLayout,
|
||||||
);
|
);
|
||||||
final expectedVerticalOffset = TvBrowseRailLayout.hubSectionHeightFor(
|
final expectedVerticalOffset = TvBrowseRailLayout.hubSectionHeightFor(
|
||||||
scale: scale,
|
scale: scale,
|
||||||
@@ -533,6 +891,7 @@ void main() {
|
|||||||
density: LibraryDensity.defaultValue,
|
density: LibraryDensity.defaultValue,
|
||||||
episodePosterMode: EpisodePosterMode.episodeThumbnail,
|
episodePosterMode: EpisodePosterMode.episodeThumbnail,
|
||||||
scale: scale,
|
scale: scale,
|
||||||
|
fullCardLayout: fullCardLayout,
|
||||||
);
|
);
|
||||||
final expectedOffset = TvBrowseRailLayout.scrollOffsetForIndex(
|
final expectedOffset = TvBrowseRailLayout.scrollOffsetForIndex(
|
||||||
hub: episodeHub,
|
hub: episodeHub,
|
||||||
@@ -620,6 +979,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('keeps late episode thumbnails visible in long TV rows', (tester) async {
|
testWidgets('keeps late episode thumbnails visible in long TV rows', (tester) async {
|
||||||
|
await SettingsService.instanceOrNull!.write(SettingsService.tvFullCardLayout, false);
|
||||||
tester.view.devicePixelRatio = 1.0;
|
tester.view.devicePixelRatio = 1.0;
|
||||||
tester.view.physicalSize = const Size(1280, 720);
|
tester.view.physicalSize = const Size(1280, 720);
|
||||||
addTearDown(() {
|
addTearDown(() {
|
||||||
@@ -707,6 +1067,7 @@ void main() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('keeps late episode thumbnails visible during rapid key repeat', (tester) async {
|
testWidgets('keeps late episode thumbnails visible during rapid key repeat', (tester) async {
|
||||||
|
await SettingsService.instanceOrNull!.write(SettingsService.tvFullCardLayout, false);
|
||||||
tester.view.devicePixelRatio = 1.0;
|
tester.view.devicePixelRatio = 1.0;
|
||||||
tester.view.physicalSize = const Size(1280, 720);
|
tester.view.physicalSize = const Size(1280, 720);
|
||||||
addTearDown(() {
|
addTearDown(() {
|
||||||
|
|||||||
Reference in New Issue
Block a user