fix(sheets): size sheets to their content instead of 75% of the window

Sheets rendered at the host's maximum height regardless of content, so a
one-item player queue or a two-track picker filled ~75% of a desktop window
with empty space.

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

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

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

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

Add videoControls.noAudioDevicesAvailable so the audio output page shows a
placeholder instead of a bare header while devices load.
This commit is contained in:
edde746
2026-08-08 00:37:17 +02:00
parent e3703892b3
commit 24a041977b
71 changed files with 1210 additions and 207 deletions
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Hissələr əlçatan deyil",
"queue": "Növbə",
"noQueueItems": "Növbədə element yoxdur",
"noAudioDevicesAvailable": "Səs cihazları əlçatan deyil",
"searchSubtitles": "Altyazı axtar",
"language": "Dil",
"noSubtitlesFound": "Altyazı tapılmadı",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Няма налични глави",
"queue": "Опашка",
"noQueueItems": "Няма елементи в опашката",
"noAudioDevicesAvailable": "Няма налични аудио устройства",
"searchSubtitles": "Търсене на субтитри",
"language": "Език",
"noSubtitlesFound": "Не са намерени субтитри",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Ingen kapitler tilgængelige",
"queue": "Kø",
"noQueueItems": "Ingen elementer i køen",
"noAudioDevicesAvailable": "Ingen lydenheder tilgængelige",
"searchSubtitles": "Søg undertekster",
"language": "Sprog",
"noSubtitlesFound": "Ingen undertekster fundet",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Keine Kapitel verfügbar",
"queue": "Warteschlange",
"noQueueItems": "Keine Elemente in der Warteschlange",
"noAudioDevicesAvailable": "Keine Audiogeräte verfügbar",
"searchSubtitles": "Untertitel suchen",
"language": "Sprache",
"noSubtitlesFound": "Keine Untertitel gefunden",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "No chapters available",
"queue": "Queue",
"noQueueItems": "No items in queue",
"noAudioDevicesAvailable": "No audio devices available",
"searchSubtitles": "Search Subtitles",
"language": "Language",
"noSubtitlesFound": "No subtitles found",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "No hay capítulos disponibles",
"queue": "Cola",
"noQueueItems": "No hay elementos en la cola",
"noAudioDevicesAvailable": "No hay dispositivos de audio disponibles",
"searchSubtitles": "Buscar subtítulos",
"language": "Idioma",
"noSubtitlesFound": "No se encontraron subtítulos",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Aucun chapitre disponible",
"queue": "File d'attente",
"noQueueItems": "Aucun élément dans la file d'attente",
"noAudioDevicesAvailable": "Aucun appareil audio disponible",
"searchSubtitles": "Rechercher des sous-titres",
"language": "Langue",
"noSubtitlesFound": "Aucun sous-titre trouvé",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Nincsenek elérhető fejezetek",
"queue": "Lejátszási sor",
"noQueueItems": "Nincsenek elemek a sorban",
"noAudioDevicesAvailable": "Nincsenek elérhető audioeszközök",
"searchSubtitles": "Feliratok keresése",
"language": "Nyelv",
"noSubtitlesFound": "Nem találhatók feliratok",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Nessun capitolo disponibile",
"queue": "Coda",
"noQueueItems": "Nessun elemento in coda",
"noAudioDevicesAvailable": "Nessun dispositivo audio disponibile",
"searchSubtitles": "Cerca sottotitoli",
"language": "Lingua",
"noSubtitlesFound": "Nessun sottotitolo trovato",
+1
View File
@@ -678,6 +678,7 @@
"noChaptersAvailable": "チャプターがありません",
"queue": "キュー",
"noQueueItems": "キューにアイテムがありません",
"noAudioDevicesAvailable": "オーディオデバイスがありません",
"searchSubtitles": "字幕を検索",
"language": "言語",
"noSubtitlesFound": "字幕が見つかりません",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Бөлімдер қолжетімсіз",
"queue": "Кезек",
"noQueueItems": "Кезекте элементтер жоқ",
"noAudioDevicesAvailable": "Қолжетімді аудио құрылғылар жоқ",
"searchSubtitles": "Субтитр іздеу",
"language": "Тіл",
"noSubtitlesFound": "Субтитр табылмады",
+1
View File
@@ -678,6 +678,7 @@
"noChaptersAvailable": "사용 가능한 챕터가 없습니다",
"queue": "재생 대기열",
"noQueueItems": "대기열에 항목이 없습니다",
"noAudioDevicesAvailable": "사용 가능한 오디오 기기가 없습니다",
"searchSubtitles": "자막 검색",
"language": "언어",
"noSubtitlesFound": "자막을 찾을 수 없습니다",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Ingen kapitler tilgjengelig",
"queue": "Kø",
"noQueueItems": "Ingen elementer i kø",
"noAudioDevicesAvailable": "Ingen lydenheter tilgjengelig",
"searchSubtitles": "Søk etter undertekster",
"language": "Språk",
"noSubtitlesFound": "Ingen undertekster funnet",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Geen hoofdstukken beschikbaar",
"queue": "Wachtrij",
"noQueueItems": "Geen items in de wachtrij",
"noAudioDevicesAvailable": "Geen audioapparaten beschikbaar",
"searchSubtitles": "Ondertitels zoeken",
"language": "Taal",
"noSubtitlesFound": "Geen ondertitels gevonden",
+1
View File
@@ -687,6 +687,7 @@
"noChaptersAvailable": "Brak dostępnych rozdziałów",
"queue": "Kolejka",
"noQueueItems": "Brak elementów w kolejce",
"noAudioDevicesAvailable": "Brak dostępnych urządzeń audio",
"searchSubtitles": "Szukaj napisów",
"language": "Język",
"noSubtitlesFound": "Nie znaleziono napisów",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Nenhum capítulo disponível",
"queue": "Fila",
"noQueueItems": "Nenhum item na fila",
"noAudioDevicesAvailable": "Nenhum dispositivo de áudio disponível",
"searchSubtitles": "Pesquisar legendas",
"language": "Idioma",
"noSubtitlesFound": "Nenhuma legenda encontrada",
+1
View File
@@ -687,6 +687,7 @@
"noChaptersAvailable": "Главы недоступны",
"queue": "Очередь",
"noQueueItems": "В очереди нет элементов",
"noAudioDevicesAvailable": "Нет доступных аудиоустройств",
"searchSubtitles": "Поиск субтитров",
"language": "Язык",
"noSubtitlesFound": "Субтитры не найдены",
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang`
///
/// Locales: 22
/// Strings: 38852 (1766 per locale)
/// Strings: 38874 (1767 per locale)
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$az extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Hissələr əlçatan deyil';
@override String get queue => 'Növbə';
@override String get noQueueItems => 'Növbədə element yoxdur';
@override String get noAudioDevicesAvailable => 'Səs cihazları əlçatan deyil';
@override String get searchSubtitles => 'Altyazı axtar';
@override String get language => 'Dil';
@override String get noSubtitlesFound => 'Altyazı tapılmadı';
@@ -3233,6 +3234,7 @@ extension on TranslationsAz {
'videoControls.noChaptersAvailable' => 'Hissələr əlçatan deyil',
'videoControls.queue' => 'Növbə',
'videoControls.noQueueItems' => 'Növbədə element yoxdur',
'videoControls.noAudioDevicesAvailable' => 'Səs cihazları əlçatan deyil',
'videoControls.searchSubtitles' => 'Altyazı axtar',
'videoControls.language' => 'Dil',
'videoControls.noSubtitlesFound' => 'Altyazı tapılmadı',
@@ -3618,9 +3620,9 @@ extension on TranslationsAz {
'explore.badge.requested' => 'Sorğu göndərildi',
'explore.badge.pendingApproval' => 'Təsdiq gözlənilir',
'explore.badge.processing' => 'Emal edilir',
'explore.badge.declined' => 'Rədd edildi',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Rədd edildi',
'explore.badge.requestFailed' => 'Sorğu uğursuz oldu',
'explore.badge.requested4k' => '4K sorğu göndərildi',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} mövsüm',
@@ -4132,9 +4134,9 @@ extension on TranslationsAz {
'performanceOverlay.dropped' => 'İtirilmiş kadrlar',
'performanceOverlay.dvRpus' => 'DV RPU-ları',
'performanceOverlay.dvRpuAverage' => 'DV RPU Ort.',
'performanceOverlay.dvSampleAverage' => 'DV Nümunə Ort.',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV Nümunə Ort.',
'performanceOverlay.maxLuma' => 'Maks Luma',
'performanceOverlay.minLuma' => 'Min Luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$bg extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Няма налични глави';
@override String get queue => 'Опашка';
@override String get noQueueItems => 'Няма елементи в опашката';
@override String get noAudioDevicesAvailable => 'Няма налични аудио устройства';
@override String get searchSubtitles => 'Търсене на субтитри';
@override String get language => 'Език';
@override String get noSubtitlesFound => 'Не са намерени субтитри';
@@ -3233,6 +3234,7 @@ extension on TranslationsBg {
'videoControls.noChaptersAvailable' => 'Няма налични глави',
'videoControls.queue' => 'Опашка',
'videoControls.noQueueItems' => 'Няма елементи в опашката',
'videoControls.noAudioDevicesAvailable' => 'Няма налични аудио устройства',
'videoControls.searchSubtitles' => 'Търсене на субтитри',
'videoControls.language' => 'Език',
'videoControls.noSubtitlesFound' => 'Не са намерени субтитри',
@@ -3618,9 +3620,9 @@ extension on TranslationsBg {
'explore.badge.requested' => 'Заявен',
'explore.badge.pendingApproval' => 'В очакване на одобрение',
'explore.badge.processing' => 'Обработва се',
'explore.badge.declined' => 'Отхвърлен',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Отхвърлен',
'explore.badge.requestFailed' => 'Заявката се провали',
'explore.badge.requested4k' => 'Заявен в 4K',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} сезона',
@@ -4132,9 +4134,9 @@ extension on TranslationsBg {
'performanceOverlay.dropped' => 'Пропуснати кадри',
'performanceOverlay.dvRpus' => 'DV RPU',
'performanceOverlay.dvRpuAverage' => 'Средно DV RPU',
'performanceOverlay.dvSampleAverage' => 'Средно DV семпл',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'Средно DV семпл',
'performanceOverlay.maxLuma' => 'Макс. яркост',
'performanceOverlay.minLuma' => 'Мин. яркост',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$da extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Ingen kapitler tilgængelige';
@override String get queue => '';
@override String get noQueueItems => 'Ingen elementer i køen';
@override String get noAudioDevicesAvailable => 'Ingen lydenheder tilgængelige';
@override String get searchSubtitles => 'Søg undertekster';
@override String get language => 'Sprog';
@override String get noSubtitlesFound => 'Ingen undertekster fundet';
@@ -3233,6 +3234,7 @@ extension on TranslationsDa {
'videoControls.noChaptersAvailable' => 'Ingen kapitler tilgængelige',
'videoControls.queue' => '',
'videoControls.noQueueItems' => 'Ingen elementer i køen',
'videoControls.noAudioDevicesAvailable' => 'Ingen lydenheder tilgængelige',
'videoControls.searchSubtitles' => 'Søg undertekster',
'videoControls.language' => 'Sprog',
'videoControls.noSubtitlesFound' => 'Ingen undertekster fundet',
@@ -3618,9 +3620,9 @@ extension on TranslationsDa {
'explore.badge.requested' => 'Anmodet',
'explore.badge.pendingApproval' => 'Afventer godkendelse',
'explore.badge.processing' => 'Behandles',
'explore.badge.declined' => 'Afvist',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Afvist',
'explore.badge.requestFailed' => 'Anmodningen mislykkedes',
'explore.badge.requested4k' => '4K anmodet',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} sæsoner',
@@ -4132,9 +4134,9 @@ extension on TranslationsDa {
'performanceOverlay.dropped' => 'Tabte',
'performanceOverlay.dvRpus' => 'DV RPUer',
'performanceOverlay.dvRpuAverage' => 'DV RPU gns.',
'performanceOverlay.dvSampleAverage' => 'DV-sample gns.',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV-sample gns.',
'performanceOverlay.maxLuma' => 'Maks. luma',
'performanceOverlay.minLuma' => 'Min. luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$de extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Keine Kapitel verfügbar';
@override String get queue => 'Warteschlange';
@override String get noQueueItems => 'Keine Elemente in der Warteschlange';
@override String get noAudioDevicesAvailable => 'Keine Audiogeräte verfügbar';
@override String get searchSubtitles => 'Untertitel suchen';
@override String get language => 'Sprache';
@override String get noSubtitlesFound => 'Keine Untertitel gefunden';
@@ -3233,6 +3234,7 @@ extension on TranslationsDe {
'videoControls.noChaptersAvailable' => 'Keine Kapitel verfügbar',
'videoControls.queue' => 'Warteschlange',
'videoControls.noQueueItems' => 'Keine Elemente in der Warteschlange',
'videoControls.noAudioDevicesAvailable' => 'Keine Audiogeräte verfügbar',
'videoControls.searchSubtitles' => 'Untertitel suchen',
'videoControls.language' => 'Sprache',
'videoControls.noSubtitlesFound' => 'Keine Untertitel gefunden',
@@ -3618,9 +3620,9 @@ extension on TranslationsDe {
'explore.badge.requested' => 'Angefragt',
'explore.badge.pendingApproval' => 'Genehmigung ausstehend',
'explore.badge.processing' => 'Wird verarbeitet',
'explore.badge.declined' => 'Abgelehnt',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Abgelehnt',
'explore.badge.requestFailed' => 'Anfrage fehlgeschlagen',
'explore.badge.requested4k' => '4K angefragt',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} Staffeln',
@@ -4132,9 +4134,9 @@ extension on TranslationsDe {
'performanceOverlay.dropped' => 'Verworfen',
'performanceOverlay.dvRpus' => 'DV-RPUs',
'performanceOverlay.dvRpuAverage' => 'DV-RPU Ø',
'performanceOverlay.dvSampleAverage' => 'DV-Sample Ø',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV-Sample Ø',
'performanceOverlay.maxLuma' => 'Max. Luma',
'performanceOverlay.minLuma' => 'Min. Luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+6 -2
View File
@@ -2026,6 +2026,9 @@ class Translations$videoControls$en {
/// en: 'No items in queue'
String get noQueueItems => 'No items in queue';
/// en: 'No audio devices available'
String get noAudioDevicesAvailable => 'No audio devices available';
/// en: 'Search Subtitles'
String get searchSubtitles => 'Search Subtitles';
@@ -6758,6 +6761,7 @@ extension on Translations {
'videoControls.noChaptersAvailable' => 'No chapters available',
'videoControls.queue' => 'Queue',
'videoControls.noQueueItems' => 'No items in queue',
'videoControls.noAudioDevicesAvailable' => 'No audio devices available',
'videoControls.searchSubtitles' => 'Search Subtitles',
'videoControls.language' => 'Language',
'videoControls.noSubtitlesFound' => 'No subtitles found',
@@ -7143,9 +7147,9 @@ extension on Translations {
'explore.badge.requested' => 'Requested',
'explore.badge.pendingApproval' => 'Pending approval',
'explore.badge.processing' => 'Processing',
'explore.badge.declined' => 'Declined',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Declined',
'explore.badge.requestFailed' => 'Request failed',
'explore.badge.requested4k' => '4K requested',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} seasons',
@@ -7657,9 +7661,9 @@ extension on Translations {
'performanceOverlay.dropped' => 'Dropped',
'performanceOverlay.dvRpus' => 'DV RPUs',
'performanceOverlay.dvRpuAverage' => 'DV RPU Avg',
'performanceOverlay.dvSampleAverage' => 'DV Sample Avg',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV Sample Avg',
'performanceOverlay.maxLuma' => 'Max Luma',
'performanceOverlay.minLuma' => 'Min Luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$es extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'No hay capítulos disponibles';
@override String get queue => 'Cola';
@override String get noQueueItems => 'No hay elementos en la cola';
@override String get noAudioDevicesAvailable => 'No hay dispositivos de audio disponibles';
@override String get searchSubtitles => 'Buscar subtítulos';
@override String get language => 'Idioma';
@override String get noSubtitlesFound => 'No se encontraron subtítulos';
@@ -3233,6 +3234,7 @@ extension on TranslationsEs {
'videoControls.noChaptersAvailable' => 'No hay capítulos disponibles',
'videoControls.queue' => 'Cola',
'videoControls.noQueueItems' => 'No hay elementos en la cola',
'videoControls.noAudioDevicesAvailable' => 'No hay dispositivos de audio disponibles',
'videoControls.searchSubtitles' => 'Buscar subtítulos',
'videoControls.language' => 'Idioma',
'videoControls.noSubtitlesFound' => 'No se encontraron subtítulos',
@@ -3618,9 +3620,9 @@ extension on TranslationsEs {
'explore.badge.requested' => 'Solicitado',
'explore.badge.pendingApproval' => 'Pendiente de aprobación',
'explore.badge.processing' => 'Procesando',
'explore.badge.declined' => 'Rechazado',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Rechazado',
'explore.badge.requestFailed' => 'La solicitud falló',
'explore.badge.requested4k' => 'Solicitado en 4K',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} temporadas',
@@ -4132,9 +4134,9 @@ extension on TranslationsEs {
'performanceOverlay.dropped' => 'Descartados',
'performanceOverlay.dvRpus' => 'DV RPUs',
'performanceOverlay.dvRpuAverage' => 'Prom. DV RPU',
'performanceOverlay.dvSampleAverage' => 'Prom. muestra DV',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'Prom. muestra DV',
'performanceOverlay.maxLuma' => 'Luma máx.',
'performanceOverlay.minLuma' => 'Luma mín.',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$fr extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Aucun chapitre disponible';
@override String get queue => 'File d\'attente';
@override String get noQueueItems => 'Aucun élément dans la file d\'attente';
@override String get noAudioDevicesAvailable => 'Aucun appareil audio disponible';
@override String get searchSubtitles => 'Rechercher des sous-titres';
@override String get language => 'Langue';
@override String get noSubtitlesFound => 'Aucun sous-titre trouvé';
@@ -3233,6 +3234,7 @@ extension on TranslationsFr {
'videoControls.noChaptersAvailable' => 'Aucun chapitre disponible',
'videoControls.queue' => 'File d\'attente',
'videoControls.noQueueItems' => 'Aucun élément dans la file d\'attente',
'videoControls.noAudioDevicesAvailable' => 'Aucun appareil audio disponible',
'videoControls.searchSubtitles' => 'Rechercher des sous-titres',
'videoControls.language' => 'Langue',
'videoControls.noSubtitlesFound' => 'Aucun sous-titre trouvé',
@@ -3618,9 +3620,9 @@ extension on TranslationsFr {
'explore.badge.requested' => 'Demandé',
'explore.badge.pendingApproval' => 'En attente d\'approbation',
'explore.badge.processing' => 'En cours de traitement',
'explore.badge.declined' => 'Refusé',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Refusé',
'explore.badge.requestFailed' => 'Échec de la demande',
'explore.badge.requested4k' => '4K demandé',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} saisons',
@@ -4132,9 +4134,9 @@ extension on TranslationsFr {
'performanceOverlay.dropped' => 'Perdues',
'performanceOverlay.dvRpus' => 'DV RPU',
'performanceOverlay.dvRpuAverage' => 'Moy. DV RPU',
'performanceOverlay.dvSampleAverage' => 'Moy. échant. DV',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'Moy. échant. DV',
'performanceOverlay.maxLuma' => 'Luma max.',
'performanceOverlay.minLuma' => 'Luma min.',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$hu extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Nincsenek elérhető fejezetek';
@override String get queue => 'Lejátszási sor';
@override String get noQueueItems => 'Nincsenek elemek a sorban';
@override String get noAudioDevicesAvailable => 'Nincsenek elérhető audioeszközök';
@override String get searchSubtitles => 'Feliratok keresése';
@override String get language => 'Nyelv';
@override String get noSubtitlesFound => 'Nem találhatók feliratok';
@@ -3233,6 +3234,7 @@ extension on TranslationsHu {
'videoControls.noChaptersAvailable' => 'Nincsenek elérhető fejezetek',
'videoControls.queue' => 'Lejátszási sor',
'videoControls.noQueueItems' => 'Nincsenek elemek a sorban',
'videoControls.noAudioDevicesAvailable' => 'Nincsenek elérhető audioeszközök',
'videoControls.searchSubtitles' => 'Feliratok keresése',
'videoControls.language' => 'Nyelv',
'videoControls.noSubtitlesFound' => 'Nem találhatók feliratok',
@@ -3618,9 +3620,9 @@ extension on TranslationsHu {
'explore.badge.requested' => 'Kérve',
'explore.badge.pendingApproval' => 'Jóváhagyásra vár',
'explore.badge.processing' => 'Feldolgozás alatt',
'explore.badge.declined' => 'Elutasítva',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Elutasítva',
'explore.badge.requestFailed' => 'A kérés nem sikerült',
'explore.badge.requested4k' => '4K kérve',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} évad',
@@ -4132,9 +4134,9 @@ extension on TranslationsHu {
'performanceOverlay.dropped' => 'Eldobva',
'performanceOverlay.dvRpus' => 'DV RPU-k',
'performanceOverlay.dvRpuAverage' => 'DV RPU-átlag',
'performanceOverlay.dvSampleAverage' => 'DV-mintaátlag',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV-mintaátlag',
'performanceOverlay.maxLuma' => 'Maximális luma',
'performanceOverlay.minLuma' => 'Minimális luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$it extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Nessun capitolo disponibile';
@override String get queue => 'Coda';
@override String get noQueueItems => 'Nessun elemento in coda';
@override String get noAudioDevicesAvailable => 'Nessun dispositivo audio disponibile';
@override String get searchSubtitles => 'Cerca sottotitoli';
@override String get language => 'Lingua';
@override String get noSubtitlesFound => 'Nessun sottotitolo trovato';
@@ -3233,6 +3234,7 @@ extension on TranslationsIt {
'videoControls.noChaptersAvailable' => 'Nessun capitolo disponibile',
'videoControls.queue' => 'Coda',
'videoControls.noQueueItems' => 'Nessun elemento in coda',
'videoControls.noAudioDevicesAvailable' => 'Nessun dispositivo audio disponibile',
'videoControls.searchSubtitles' => 'Cerca sottotitoli',
'videoControls.language' => 'Lingua',
'videoControls.noSubtitlesFound' => 'Nessun sottotitolo trovato',
@@ -3618,9 +3620,9 @@ extension on TranslationsIt {
'explore.badge.requested' => 'Richiesto',
'explore.badge.pendingApproval' => 'In attesa di approvazione',
'explore.badge.processing' => 'In elaborazione',
'explore.badge.declined' => 'Rifiutato',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Rifiutato',
'explore.badge.requestFailed' => 'Richiesta non riuscita',
'explore.badge.requested4k' => 'Richiesto in 4K',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} stagioni',
@@ -4132,9 +4134,9 @@ extension on TranslationsIt {
'performanceOverlay.dropped' => 'Scartati',
'performanceOverlay.dvRpus' => 'DV RPU',
'performanceOverlay.dvRpuAverage' => 'Media DV RPU',
'performanceOverlay.dvSampleAverage' => 'Media camp. DV',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'Media camp. DV',
'performanceOverlay.maxLuma' => 'Luma max',
'performanceOverlay.minLuma' => 'Luma min',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -828,6 +828,7 @@ class _Translations$videoControls$ja extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'チャプターがありません';
@override String get queue => 'キュー';
@override String get noQueueItems => 'キューにアイテムがありません';
@override String get noAudioDevicesAvailable => 'オーディオデバイスがありません';
@override String get searchSubtitles => '字幕を検索';
@override String get language => '言語';
@override String get noSubtitlesFound => '字幕が見つかりません';
@@ -3224,6 +3225,7 @@ extension on TranslationsJa {
'videoControls.noChaptersAvailable' => 'チャプターがありません',
'videoControls.queue' => 'キュー',
'videoControls.noQueueItems' => 'キューにアイテムがありません',
'videoControls.noAudioDevicesAvailable' => 'オーディオデバイスがありません',
'videoControls.searchSubtitles' => '字幕を検索',
'videoControls.language' => '言語',
'videoControls.noSubtitlesFound' => '字幕が見つかりません',
@@ -3609,9 +3611,9 @@ extension on TranslationsJa {
'explore.badge.requested' => 'リクエスト済み',
'explore.badge.pendingApproval' => '承認待ち',
'explore.badge.processing' => '処理中',
'explore.badge.declined' => '却下',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => '却下',
'explore.badge.requestFailed' => 'リクエスト失敗',
'explore.badge.requested4k' => '4Kリクエスト済み',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => 'シーズン ${available}/${total}',
@@ -4123,9 +4125,9 @@ extension on TranslationsJa {
'performanceOverlay.dropped' => 'ドロップ',
'performanceOverlay.dvRpus' => 'DV RPU',
'performanceOverlay.dvRpuAverage' => 'DV RPU 平均',
'performanceOverlay.dvSampleAverage' => 'DV サンプル平均',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV サンプル平均',
'performanceOverlay.maxLuma' => '最大輝度',
'performanceOverlay.minLuma' => '最小輝度',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$kk extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Бөлімдер қолжетімсіз';
@override String get queue => 'Кезек';
@override String get noQueueItems => 'Кезекте элементтер жоқ';
@override String get noAudioDevicesAvailable => 'Қолжетімді аудио құрылғылар жоқ';
@override String get searchSubtitles => 'Субтитр іздеу';
@override String get language => 'Тіл';
@override String get noSubtitlesFound => 'Субтитр табылмады';
@@ -3233,6 +3234,7 @@ extension on TranslationsKk {
'videoControls.noChaptersAvailable' => 'Бөлімдер қолжетімсіз',
'videoControls.queue' => 'Кезек',
'videoControls.noQueueItems' => 'Кезекте элементтер жоқ',
'videoControls.noAudioDevicesAvailable' => 'Қолжетімді аудио құрылғылар жоқ',
'videoControls.searchSubtitles' => 'Субтитр іздеу',
'videoControls.language' => 'Тіл',
'videoControls.noSubtitlesFound' => 'Субтитр табылмады',
@@ -3618,9 +3620,9 @@ extension on TranslationsKk {
'explore.badge.requested' => 'Сұралған',
'explore.badge.pendingApproval' => 'Растау күтілуде',
'explore.badge.processing' => 'Өңделуде',
'explore.badge.declined' => 'Қабылданбады',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Қабылданбады',
'explore.badge.requestFailed' => 'Сұрау сәтсіз аяқталды',
'explore.badge.requested4k' => '4K сұралған',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} маусым',
@@ -4132,9 +4134,9 @@ extension on TranslationsKk {
'performanceOverlay.dropped' => 'Өткізілген кадрлар',
'performanceOverlay.dvRpus' => 'DV RPU-лар',
'performanceOverlay.dvRpuAverage' => 'DV RPU Орт.',
'performanceOverlay.dvSampleAverage' => 'DV Үлгі Орт.',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV Үлгі Орт.',
'performanceOverlay.maxLuma' => 'Макс Luma',
'performanceOverlay.minLuma' => 'Мин Luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -828,6 +828,7 @@ class _Translations$videoControls$ko extends Translations$videoControls$en {
@override String get noChaptersAvailable => '사용 가능한 챕터가 없습니다';
@override String get queue => '재생 대기열';
@override String get noQueueItems => '대기열에 항목이 없습니다';
@override String get noAudioDevicesAvailable => '사용 가능한 오디오 기기가 없습니다';
@override String get searchSubtitles => '자막 검색';
@override String get language => '언어';
@override String get noSubtitlesFound => '자막을 찾을 수 없습니다';
@@ -3224,6 +3225,7 @@ extension on TranslationsKo {
'videoControls.noChaptersAvailable' => '사용 가능한 챕터가 없습니다',
'videoControls.queue' => '재생 대기열',
'videoControls.noQueueItems' => '대기열에 항목이 없습니다',
'videoControls.noAudioDevicesAvailable' => '사용 가능한 오디오 기기가 없습니다',
'videoControls.searchSubtitles' => '자막 검색',
'videoControls.language' => '언어',
'videoControls.noSubtitlesFound' => '자막을 찾을 수 없습니다',
@@ -3609,9 +3611,9 @@ extension on TranslationsKo {
'explore.badge.requested' => '요청됨',
'explore.badge.pendingApproval' => '승인 대기 중',
'explore.badge.processing' => '처리 중',
'explore.badge.declined' => '거절됨',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => '거절됨',
'explore.badge.requestFailed' => '요청 실패',
'explore.badge.requested4k' => '4K 요청됨',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '시즌 ${available}/${total}',
@@ -4123,9 +4125,9 @@ extension on TranslationsKo {
'performanceOverlay.dropped' => '드롭됨',
'performanceOverlay.dvRpus' => 'DV RPU',
'performanceOverlay.dvRpuAverage' => 'DV RPU 평균',
'performanceOverlay.dvSampleAverage' => 'DV 샘플 평균',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV 샘플 평균',
'performanceOverlay.maxLuma' => '최대 휘도',
'performanceOverlay.minLuma' => '최소 휘도',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$nb extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Ingen kapitler tilgjengelig';
@override String get queue => '';
@override String get noQueueItems => 'Ingen elementer i kø';
@override String get noAudioDevicesAvailable => 'Ingen lydenheter tilgjengelig';
@override String get searchSubtitles => 'Søk etter undertekster';
@override String get language => 'Språk';
@override String get noSubtitlesFound => 'Ingen undertekster funnet';
@@ -3233,6 +3234,7 @@ extension on TranslationsNb {
'videoControls.noChaptersAvailable' => 'Ingen kapitler tilgjengelig',
'videoControls.queue' => '',
'videoControls.noQueueItems' => 'Ingen elementer i kø',
'videoControls.noAudioDevicesAvailable' => 'Ingen lydenheter tilgjengelig',
'videoControls.searchSubtitles' => 'Søk etter undertekster',
'videoControls.language' => 'Språk',
'videoControls.noSubtitlesFound' => 'Ingen undertekster funnet',
@@ -3618,9 +3620,9 @@ extension on TranslationsNb {
'explore.badge.requested' => 'Forespurt',
'explore.badge.pendingApproval' => 'Venter på godkjenning',
'explore.badge.processing' => 'Behandler',
'explore.badge.declined' => 'Avslått',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Avslått',
'explore.badge.requestFailed' => 'Forespørsel mislyktes',
'explore.badge.requested4k' => '4K forespurt',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} sesonger',
@@ -4132,9 +4134,9 @@ extension on TranslationsNb {
'performanceOverlay.dropped' => 'Tapte',
'performanceOverlay.dvRpus' => 'DV RPU-er',
'performanceOverlay.dvRpuAverage' => 'DV RPU snitt',
'performanceOverlay.dvSampleAverage' => 'DV-sample snitt',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV-sample snitt',
'performanceOverlay.maxLuma' => 'Maks luma',
'performanceOverlay.minLuma' => 'Min luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$nl extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Geen hoofdstukken beschikbaar';
@override String get queue => 'Wachtrij';
@override String get noQueueItems => 'Geen items in de wachtrij';
@override String get noAudioDevicesAvailable => 'Geen audioapparaten beschikbaar';
@override String get searchSubtitles => 'Ondertitels zoeken';
@override String get language => 'Taal';
@override String get noSubtitlesFound => 'Geen ondertitels gevonden';
@@ -3233,6 +3234,7 @@ extension on TranslationsNl {
'videoControls.noChaptersAvailable' => 'Geen hoofdstukken beschikbaar',
'videoControls.queue' => 'Wachtrij',
'videoControls.noQueueItems' => 'Geen items in de wachtrij',
'videoControls.noAudioDevicesAvailable' => 'Geen audioapparaten beschikbaar',
'videoControls.searchSubtitles' => 'Ondertitels zoeken',
'videoControls.language' => 'Taal',
'videoControls.noSubtitlesFound' => 'Geen ondertitels gevonden',
@@ -3618,9 +3620,9 @@ extension on TranslationsNl {
'explore.badge.requested' => 'Aangevraagd',
'explore.badge.pendingApproval' => 'In afwachting van goedkeuring',
'explore.badge.processing' => 'Wordt verwerkt',
'explore.badge.declined' => 'Afgewezen',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Afgewezen',
'explore.badge.requestFailed' => 'Aanvraag mislukt',
'explore.badge.requested4k' => '4K aangevraagd',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} seizoenen',
@@ -4132,9 +4134,9 @@ extension on TranslationsNl {
'performanceOverlay.dropped' => 'Gedropt',
'performanceOverlay.dvRpus' => 'DV RPUs',
'performanceOverlay.dvRpuAverage' => 'DV RPU gem.',
'performanceOverlay.dvSampleAverage' => 'DV-sample gem.',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV-sample gem.',
'performanceOverlay.maxLuma' => 'Max luma',
'performanceOverlay.minLuma' => 'Min luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -837,6 +837,7 @@ class _Translations$videoControls$pl extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Brak dostępnych rozdziałów';
@override String get queue => 'Kolejka';
@override String get noQueueItems => 'Brak elementów w kolejce';
@override String get noAudioDevicesAvailable => 'Brak dostępnych urządzeń audio';
@override String get searchSubtitles => 'Szukaj napisów';
@override String get language => 'Język';
@override String get noSubtitlesFound => 'Nie znaleziono napisów';
@@ -3251,6 +3252,7 @@ extension on TranslationsPl {
'videoControls.noChaptersAvailable' => 'Brak dostępnych rozdziałów',
'videoControls.queue' => 'Kolejka',
'videoControls.noQueueItems' => 'Brak elementów w kolejce',
'videoControls.noAudioDevicesAvailable' => 'Brak dostępnych urządzeń audio',
'videoControls.searchSubtitles' => 'Szukaj napisów',
'videoControls.language' => 'Język',
'videoControls.noSubtitlesFound' => 'Nie znaleziono napisów',
@@ -3636,9 +3638,9 @@ extension on TranslationsPl {
'explore.badge.requested' => 'Zamówiono',
'explore.badge.pendingApproval' => 'Oczekuje na zatwierdzenie',
'explore.badge.processing' => 'Przetwarzanie',
'explore.badge.declined' => 'Odrzucono',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Odrzucono',
'explore.badge.requestFailed' => 'Żądanie nie powiodło się',
'explore.badge.requested4k' => 'Zamówiono w 4K',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} sezonów',
@@ -4150,9 +4152,9 @@ extension on TranslationsPl {
'performanceOverlay.dropped' => 'Pominięte',
'performanceOverlay.dvRpus' => 'DV RPU',
'performanceOverlay.dvRpuAverage' => 'Śr. DV RPU',
'performanceOverlay.dvSampleAverage' => 'Śr. próbki DV',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'Śr. próbki DV',
'performanceOverlay.maxLuma' => 'Maks. luma',
'performanceOverlay.minLuma' => 'Min. luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$pt extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Nenhum capítulo disponível';
@override String get queue => 'Fila';
@override String get noQueueItems => 'Nenhum item na fila';
@override String get noAudioDevicesAvailable => 'Nenhum dispositivo de áudio disponível';
@override String get searchSubtitles => 'Pesquisar legendas';
@override String get language => 'Idioma';
@override String get noSubtitlesFound => 'Nenhuma legenda encontrada';
@@ -3233,6 +3234,7 @@ extension on TranslationsPt {
'videoControls.noChaptersAvailable' => 'Nenhum capítulo disponível',
'videoControls.queue' => 'Fila',
'videoControls.noQueueItems' => 'Nenhum item na fila',
'videoControls.noAudioDevicesAvailable' => 'Nenhum dispositivo de áudio disponível',
'videoControls.searchSubtitles' => 'Pesquisar legendas',
'videoControls.language' => 'Idioma',
'videoControls.noSubtitlesFound' => 'Nenhuma legenda encontrada',
@@ -3618,9 +3620,9 @@ extension on TranslationsPt {
'explore.badge.requested' => 'Solicitado',
'explore.badge.pendingApproval' => 'Aguardando aprovação',
'explore.badge.processing' => 'Processando',
'explore.badge.declined' => 'Recusado',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Recusado',
'explore.badge.requestFailed' => 'Falha na solicitação',
'explore.badge.requested4k' => '4K solicitado',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} temporadas',
@@ -4132,9 +4134,9 @@ extension on TranslationsPt {
'performanceOverlay.dropped' => 'Descartados',
'performanceOverlay.dvRpus' => 'DV RPUs',
'performanceOverlay.dvRpuAverage' => 'Média DV RPU',
'performanceOverlay.dvSampleAverage' => 'Média amostra DV',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'Média amostra DV',
'performanceOverlay.maxLuma' => 'Luma máx.',
'performanceOverlay.minLuma' => 'Luma mín.',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -837,6 +837,7 @@ class _Translations$videoControls$ru extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Главы недоступны';
@override String get queue => 'Очередь';
@override String get noQueueItems => 'В очереди нет элементов';
@override String get noAudioDevicesAvailable => 'Нет доступных аудиоустройств';
@override String get searchSubtitles => 'Поиск субтитров';
@override String get language => 'Язык';
@override String get noSubtitlesFound => 'Субтитры не найдены';
@@ -3251,6 +3252,7 @@ extension on TranslationsRu {
'videoControls.noChaptersAvailable' => 'Главы недоступны',
'videoControls.queue' => 'Очередь',
'videoControls.noQueueItems' => 'В очереди нет элементов',
'videoControls.noAudioDevicesAvailable' => 'Нет доступных аудиоустройств',
'videoControls.searchSubtitles' => 'Поиск субтитров',
'videoControls.language' => 'Язык',
'videoControls.noSubtitlesFound' => 'Субтитры не найдены',
@@ -3636,9 +3638,9 @@ extension on TranslationsRu {
'explore.badge.requested' => 'Запрошено',
'explore.badge.pendingApproval' => 'Ожидает одобрения',
'explore.badge.processing' => 'В обработке',
'explore.badge.declined' => 'Отклонено',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Отклонено',
'explore.badge.requestFailed' => 'Запрос не удался',
'explore.badge.requested4k' => 'Запрошено в 4K',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} сезонов',
@@ -4150,9 +4152,9 @@ extension on TranslationsRu {
'performanceOverlay.dropped' => 'Пропущено',
'performanceOverlay.dvRpus' => 'DV RPU',
'performanceOverlay.dvRpuAverage' => 'Сред. DV RPU',
'performanceOverlay.dvSampleAverage' => 'Сред. сэмпл DV',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'Сред. сэмпл DV',
'performanceOverlay.maxLuma' => 'Макс. яркость',
'performanceOverlay.minLuma' => 'Мин. яркость',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$sv extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Inga kapitel tillgängliga';
@override String get queue => '';
@override String get noQueueItems => 'Inga objekt i kön';
@override String get noAudioDevicesAvailable => 'Inga ljudenheter tillgängliga';
@override String get searchSubtitles => 'Sök undertexter';
@override String get language => 'Språk';
@override String get noSubtitlesFound => 'Inga undertexter hittades';
@@ -3233,6 +3234,7 @@ extension on TranslationsSv {
'videoControls.noChaptersAvailable' => 'Inga kapitel tillgängliga',
'videoControls.queue' => '',
'videoControls.noQueueItems' => 'Inga objekt i kön',
'videoControls.noAudioDevicesAvailable' => 'Inga ljudenheter tillgängliga',
'videoControls.searchSubtitles' => 'Sök undertexter',
'videoControls.language' => 'Språk',
'videoControls.noSubtitlesFound' => 'Inga undertexter hittades',
@@ -3618,9 +3620,9 @@ extension on TranslationsSv {
'explore.badge.requested' => 'Begärd',
'explore.badge.pendingApproval' => 'Väntar på godkännande',
'explore.badge.processing' => 'Bearbetas',
'explore.badge.declined' => 'Avvisad',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Avvisad',
'explore.badge.requestFailed' => 'Begäran misslyckades',
'explore.badge.requested4k' => '4K begärd',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} säsonger',
@@ -4132,9 +4134,9 @@ extension on TranslationsSv {
'performanceOverlay.dropped' => 'Tappade bildrutor',
'performanceOverlay.dvRpus' => 'DV-RPU:er',
'performanceOverlay.dvRpuAverage' => 'DV-RPU, genomsnitt',
'performanceOverlay.dvSampleAverage' => 'DV-sampling, genomsnitt',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV-sampling, genomsnitt',
'performanceOverlay.maxLuma' => 'Max luma',
'performanceOverlay.minLuma' => 'Min luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$tr extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Kısım bulunmuyor';
@override String get queue => 'Kuyruk';
@override String get noQueueItems => 'Kuyrukta öge yok';
@override String get noAudioDevicesAvailable => 'Kullanılabilir ses cihazı yok';
@override String get searchSubtitles => 'Altyazı Ara';
@override String get language => 'Dil';
@override String get noSubtitlesFound => 'Altyazı bulunamadı';
@@ -3233,6 +3234,7 @@ extension on TranslationsTr {
'videoControls.noChaptersAvailable' => 'Kısım bulunmuyor',
'videoControls.queue' => 'Kuyruk',
'videoControls.noQueueItems' => 'Kuyrukta öge yok',
'videoControls.noAudioDevicesAvailable' => 'Kullanılabilir ses cihazı yok',
'videoControls.searchSubtitles' => 'Altyazı Ara',
'videoControls.language' => 'Dil',
'videoControls.noSubtitlesFound' => 'Altyazı bulunamadı',
@@ -3618,9 +3620,9 @@ extension on TranslationsTr {
'explore.badge.requested' => 'İstendi',
'explore.badge.pendingApproval' => 'Onay bekliyor',
'explore.badge.processing' => 'İşleniyor',
'explore.badge.declined' => 'Reddedildi',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Reddedildi',
'explore.badge.requestFailed' => 'İstek başarısız oldu',
'explore.badge.requested4k' => '4K istendi',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} sezon',
@@ -4132,9 +4134,9 @@ extension on TranslationsTr {
'performanceOverlay.dropped' => 'Kare Kaybı',
'performanceOverlay.dvRpus' => 'DV RPU\'ları',
'performanceOverlay.dvRpuAverage' => 'DV RPU Ort.',
'performanceOverlay.dvSampleAverage' => 'DV Örnek Ort.',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV Örnek Ort.',
'performanceOverlay.maxLuma' => 'Maks Luma',
'performanceOverlay.minLuma' => 'Min Luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -831,6 +831,7 @@ class _Translations$videoControls$uz extends Translations$videoControls$en {
@override String get noChaptersAvailable => 'Boʻlimlar mavjud emas';
@override String get queue => 'Navbat';
@override String get noQueueItems => 'Navbatda elementlar yoʻq';
@override String get noAudioDevicesAvailable => 'Mavjud audio qurilmalar yoʻq';
@override String get searchSubtitles => 'Subtitr qidirish';
@override String get language => 'Til';
@override String get noSubtitlesFound => 'Subtitr topilmadi';
@@ -3233,6 +3234,7 @@ extension on TranslationsUz {
'videoControls.noChaptersAvailable' => 'Boʻlimlar mavjud emas',
'videoControls.queue' => 'Navbat',
'videoControls.noQueueItems' => 'Navbatda elementlar yoʻq',
'videoControls.noAudioDevicesAvailable' => 'Mavjud audio qurilmalar yoʻq',
'videoControls.searchSubtitles' => 'Subtitr qidirish',
'videoControls.language' => 'Til',
'videoControls.noSubtitlesFound' => 'Subtitr topilmadi',
@@ -3618,9 +3620,9 @@ extension on TranslationsUz {
'explore.badge.requested' => 'Soʻralgan',
'explore.badge.pendingApproval' => 'Tasdiq kutilmoqda',
'explore.badge.processing' => 'Ishlanmoqda',
'explore.badge.declined' => 'Rad etilgan',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => 'Rad etilgan',
'explore.badge.requestFailed' => 'Soʻrov amalga oshmadi',
'explore.badge.requested4k' => '4K soʻralgan',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} mavsum',
@@ -4132,9 +4134,9 @@ extension on TranslationsUz {
'performanceOverlay.dropped' => 'Tushirib qoldirilgan kadrlar',
'performanceOverlay.dvRpus' => 'DV RPU-lar',
'performanceOverlay.dvRpuAverage' => 'DV RPU Oʻrt.',
'performanceOverlay.dvSampleAverage' => 'DV Namuna Oʻrt.',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV Namuna Oʻrt.',
'performanceOverlay.maxLuma' => 'Maks Luma',
'performanceOverlay.minLuma' => 'Min Luma',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -828,6 +828,7 @@ class Translations$videoControls$zh extends Translations$videoControls$en {
@override String get noChaptersAvailable => '没有可用的章节';
@override String get queue => '播放队列';
@override String get noQueueItems => '队列中没有项目';
@override String get noAudioDevicesAvailable => '没有可用的音频设备';
@override String get searchSubtitles => '搜索字幕';
@override String get language => '语言';
@override String get noSubtitlesFound => '未找到字幕';
@@ -3224,6 +3225,7 @@ extension on TranslationsZh {
'videoControls.noChaptersAvailable' => '没有可用的章节',
'videoControls.queue' => '播放队列',
'videoControls.noQueueItems' => '队列中没有项目',
'videoControls.noAudioDevicesAvailable' => '没有可用的音频设备',
'videoControls.searchSubtitles' => '搜索字幕',
'videoControls.language' => '语言',
'videoControls.noSubtitlesFound' => '未找到字幕',
@@ -3609,9 +3611,9 @@ extension on TranslationsZh {
'explore.badge.requested' => '已请求',
'explore.badge.pendingApproval' => '待批准',
'explore.badge.processing' => '处理中',
'explore.badge.declined' => '已拒绝',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => '已拒绝',
'explore.badge.requestFailed' => '请求失败',
'explore.badge.requested4k' => '已请求 4K',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '已有 ${available}/${total}',
@@ -4123,9 +4125,9 @@ extension on TranslationsZh {
'performanceOverlay.dropped' => '丢帧',
'performanceOverlay.dvRpus' => 'DV RPU',
'performanceOverlay.dvRpuAverage' => 'DV RPU 平均',
'performanceOverlay.dvSampleAverage' => 'DV 采样平均',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV 采样平均',
'performanceOverlay.maxLuma' => '最大亮度',
'performanceOverlay.minLuma' => '最小亮度',
'performanceOverlay.maxCll' => 'MaxCLL',
+4 -2
View File
@@ -829,6 +829,7 @@ class _Translations$videoControls$zh_Hant extends Translations$videoControls$zh
@override String get noChaptersAvailable => '沒有可用的章節';
@override String get queue => '播放佇列';
@override String get noQueueItems => '佇列中沒有項目';
@override String get noAudioDevicesAvailable => '沒有可用的音訊裝置';
@override String get searchSubtitles => '搜尋字幕';
@override String get language => '語言';
@override String get noSubtitlesFound => '找不到字幕';
@@ -3225,6 +3226,7 @@ extension on TranslationsZhHant {
'videoControls.noChaptersAvailable' => '沒有可用的章節',
'videoControls.queue' => '播放佇列',
'videoControls.noQueueItems' => '佇列中沒有項目',
'videoControls.noAudioDevicesAvailable' => '沒有可用的音訊裝置',
'videoControls.searchSubtitles' => '搜尋字幕',
'videoControls.language' => '語言',
'videoControls.noSubtitlesFound' => '找不到字幕',
@@ -3610,9 +3612,9 @@ extension on TranslationsZhHant {
'explore.badge.requested' => '已提出請求',
'explore.badge.pendingApproval' => '等待核准',
'explore.badge.processing' => '處理中',
'explore.badge.declined' => '已拒絕',
_ => null,
} ?? switch (path) {
'explore.badge.declined' => '已拒絕',
'explore.badge.requestFailed' => '請求失敗',
'explore.badge.requested4k' => '已請求 4K',
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total}',
@@ -4124,9 +4126,9 @@ extension on TranslationsZhHant {
'performanceOverlay.dropped' => '丟格數(Dropped',
'performanceOverlay.dvRpus' => 'DV RPU 數',
'performanceOverlay.dvRpuAverage' => 'DV RPU 平均',
'performanceOverlay.dvSampleAverage' => 'DV 取樣平均',
_ => null,
} ?? switch (path) {
'performanceOverlay.dvSampleAverage' => 'DV 取樣平均',
'performanceOverlay.maxLuma' => '最大亮度',
'performanceOverlay.minLuma' => '最小亮度',
'performanceOverlay.maxCll' => 'MaxCLL',
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Inga kapitel tillgängliga",
"queue": "Kö",
"noQueueItems": "Inga objekt i kön",
"noAudioDevicesAvailable": "Inga ljudenheter tillgängliga",
"searchSubtitles": "Sök undertexter",
"language": "Språk",
"noSubtitlesFound": "Inga undertexter hittades",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Kısım bulunmuyor",
"queue": "Kuyruk",
"noQueueItems": "Kuyrukta öge yok",
"noAudioDevicesAvailable": "Kullanılabilir ses cihazı yok",
"searchSubtitles": "Altyazı Ara",
"language": "Dil",
"noSubtitlesFound": "Altyazı bulunamadı",
+1
View File
@@ -681,6 +681,7 @@
"noChaptersAvailable": "Boʻlimlar mavjud emas",
"queue": "Navbat",
"noQueueItems": "Navbatda elementlar yoʻq",
"noAudioDevicesAvailable": "Mavjud audio qurilmalar yoʻq",
"searchSubtitles": "Subtitr qidirish",
"language": "Til",
"noSubtitlesFound": "Subtitr topilmadi",
+1
View File
@@ -678,6 +678,7 @@
"noChaptersAvailable": "沒有可用的章節",
"queue": "播放佇列",
"noQueueItems": "佇列中沒有項目",
"noAudioDevicesAvailable": "沒有可用的音訊裝置",
"searchSubtitles": "搜尋字幕",
"language": "語言",
"noSubtitlesFound": "找不到字幕",
+1
View File
@@ -678,6 +678,7 @@
"noChaptersAvailable": "没有可用的章节",
"queue": "播放队列",
"noQueueItems": "队列中没有项目",
"noAudioDevicesAvailable": "没有可用的音频设备",
"searchSubtitles": "搜索字幕",
"language": "语言",
"noSubtitlesFound": "未找到字幕",
@@ -51,6 +51,8 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
bool _isLoadingValues = false;
String? _filterValuesError;
int _filterValuesLoadGeneration = 0;
final _contentKey = GlobalKey();
double? _transitionMinHeight;
final Map<String, String> _tempSelectedFilters = {};
static final Map<String, String> _filterDisplayNames = {}; // Cache for display names
static const int _maxCachedDisplayNames = 1000;
@@ -116,6 +118,11 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
final libraryKey = widget.libraryKey;
final cachedValues = widget.cachedValues;
final loader = widget.loadFilterValues;
// Drilling in is a setState page swap inside one sheet, and sheets are
// bottom-anchored, so any height change during the load drags the header
// and its Back button. Hold the outgoing page's height for the transient
// spinner; the settled states below are free to hug again.
_transitionMinHeight = _contentHeight();
setState(() {
_currentFilter = filter;
_filterValues = [];
@@ -235,26 +242,52 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
),
)
: null,
child: currentFilter != null ? _buildFilterValuesView(currentFilter) : _buildFiltersView(),
child: KeyedSubtree(
key: _contentKey,
child: currentFilter != null ? _buildFilterValuesView(currentFilter) : _buildFiltersView(),
),
);
}
/// Height the content area currently occupies, used to hold the sheet steady
/// across a page swap. Null before first layout.
double? _contentHeight() {
final box = _contentKey.currentContext?.findRenderObject() as RenderBox?;
return box?.hasSize == true ? box!.size.height : null;
}
Widget _buildFilterValuesView(MediaFilter filter) {
final error = _filterValuesError;
if (error != null) {
return ErrorStateWidget(
message: error,
onRetry: () => _loadFilterValues(filter),
actionFocusNode: _initialFocusNode,
onActionBack: _goBack,
actionAutofocus: InputModeTracker.isKeyboardMode(context),
actionUseBackgroundFocus: true,
// The StateMessageWidget family is filling by design — 33 other sites
// render it in page bodies and SliverFillRemaining. The unbounded scroll
// axis here is what lets its inner Center shrink to content, so the sheet
// does not stretch to the full height cap for one line of text.
return SingleChildScrollView(
primary: false,
child: ErrorStateWidget(
message: error,
onRetry: () => _loadFilterValues(filter),
actionFocusNode: _initialFocusNode,
onActionBack: _goBack,
actionAutofocus: InputModeTracker.isKeyboardMode(context),
actionUseBackgroundFocus: true,
),
);
}
if (_isLoadingValues) {
assert(_transitionMinHeight != null, '_transitionMinHeight must be set before entering the loading state');
// Held at the outgoing page's height (see [_loadFilterValues]) so the
// transient spinner cannot move the header. Settled states below hug.
return Focus(
autofocus: InputModeTracker.isKeyboardMode(context),
child: const Center(child: CircularProgressIndicator()),
// Exactly the outgoing height, so the swap moves nothing.
// [_loadFilterValues] assigns it immediately before setting
// `_isLoadingValues`, so it is never null here.
child: SizedBox(
height: _transitionMinHeight,
child: const Center(child: CircularProgressIndicator()),
),
);
}
@@ -262,6 +295,7 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
return ListView.builder(
controller: _valuesScrollController,
primary: false,
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _filterValues.length + 1,
itemBuilder: (context, index) {
@@ -309,6 +343,7 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
final autofocusFirst = InputModeTracker.isKeyboardMode(context);
return ListView.builder(
primary: false,
shrinkWrap: true,
padding: const EdgeInsets.symmetric(vertical: 8),
itemCount: _sortedFilters.length,
itemBuilder: (context, index) {
@@ -836,7 +836,6 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
return BottomSheetPageScaffold(
title: t.libraries.libraryOptions,
icon: Symbols.tune_rounded,
shrinkWrap: true,
child: ListView(
primary: false,
shrinkWrap: true,
@@ -917,7 +916,6 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
title: t.libraries.groupings.title,
icon: Symbols.category_rounded,
onBack: onBack,
shrinkWrap: true,
child: ListView(
primary: false,
shrinkWrap: true,
@@ -315,6 +315,13 @@ class _RecordOptionsContentState extends State<_RecordOptionsContent> {
? Padding(
padding: const EdgeInsets.symmetric(vertical: 24),
child: Center(
// Hugs like every other empty state. Switching entries
// still moves the chooser chips above by the difference
// in row count, which is inherent to content sizing;
// filling this one branch instead made the mixed case
// (one entry with settings, one without) far worse, since
// the empty entry then inflated to the whole height cap.
heightFactor: 1,
child: Text(
_entry.airingsType ?? '',
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
@@ -104,13 +104,14 @@ class _ReorderFavoritesSheetState extends State<ReorderFavoritesSheet>
mainAxisSize: .min,
children: [
BottomSheetHeader(title: t.liveTv.reorderFavorites, icon: Symbols.swap_vert_rounded),
Expanded(
Flexible(
child: Focus(
focusNode: _listFocusNode,
descendantsAreFocusable: false,
autofocus: isKeyboardMode,
onKeyEvent: handleReorderKeyEvent,
child: ReorderableListView.builder(
shrinkWrap: true,
scrollController: _scrollController,
onReorderItem: _onReorder,
itemCount: _tempFavorites.length,
-2
View File
@@ -1561,12 +1561,10 @@ class _MainScreenState extends State<MainScreen>
final controller = OverlaySheetController.of(context);
final groupByServer = SettingsService.instanceOrNull?.read(SettingsService.groupLibrariesByServer) ?? false;
final maxHeight = MediaQuery.sizeOf(context).height * 0.62;
controller
.show<String>(
showDragHandle: true,
constraints: BoxConstraints(maxHeight: maxHeight),
builder: (sheetContext) {
return Consumer2<LibrariesProvider, HiddenLibrariesProvider>(
builder: (context, librariesProvider, hiddenLibrariesProvider, _) {
@@ -169,6 +169,7 @@ class _SessionMenuSheet extends StatelessWidget {
),
Flexible(
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.all(16),
children: [
Text(
+15 -4
View File
@@ -5,6 +5,19 @@ import '../focus/key_event_utils.dart';
import 'bottom_sheet_header.dart';
/// Shared page layout for bottom sheets with a stable header and content area.
///
/// The content area is a [Flexible], so the page is as tall as [child] wants to
/// be and no taller, while still being clamped by the sheet's own maximum
/// height. [child] should therefore shrink-wrap in the vertical axis: use a
/// `SingleChildScrollView`, or a list with `shrinkWrap: true`. A plain
/// scrollable sizes itself to the incoming maximum and reintroduces the empty
/// space this layout exists to avoid.
///
/// A child may deliberately fill instead when a content-driven height would
/// move a control the user is operating — sheets are bottom-anchored, so a
/// shrinking body drags the top edge and anything above the scroll area with
/// it. `SubtitleSearchSheet` and its language picker opt out for that
/// reason; document any other.
class BottomSheetPageScaffold extends StatelessWidget {
final String title;
final Widget child;
@@ -19,7 +32,6 @@ class BottomSheetPageScaffold extends StatelessWidget {
final bool showHeaderBorder;
final bool showHeaderDivider;
final FocusNode? closeFocusNode;
final bool shrinkWrap;
const BottomSheetPageScaffold({
super.key,
@@ -36,13 +48,12 @@ class BottomSheetPageScaffold extends StatelessWidget {
this.showHeaderBorder = true,
this.showHeaderDivider = false,
this.closeFocusNode,
this.shrinkWrap = false,
});
@override
Widget build(BuildContext context) {
Widget content = Column(
mainAxisSize: shrinkWrap ? MainAxisSize.min : MainAxisSize.max,
mainAxisSize: MainAxisSize.min,
children: [
BottomSheetHeader(
title: title,
@@ -58,7 +69,7 @@ class BottomSheetPageScaffold extends StatelessWidget {
closeFocusNode: closeFocusNode,
),
if (showHeaderDivider) Divider(color: Theme.of(context).dividerColor, height: 1),
if (shrinkWrap) child else Expanded(child: child),
Flexible(child: child),
],
);
+3 -1
View File
@@ -48,6 +48,7 @@ class _FileInfoBottomSheetState extends State<FileInfoBottomSheet> {
Widget build(BuildContext context) {
final versions = widget.fileInfo.versions;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
BottomSheetHeader(
title: t.fileInfo.title,
@@ -57,8 +58,9 @@ class _FileInfoBottomSheetState extends State<FileInfoBottomSheet> {
// keeps no rule under it.
showBorder: false,
),
Expanded(
Flexible(
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
children: [
if (widget.title.isNotEmpty) _ItemHeadline(title: widget.title, versions: versions),
+9 -3
View File
@@ -395,7 +395,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet>
descendantsAreFocusable: false,
autofocus: InputModeTracker.isKeyboardMode(context),
onKeyEvent: handleReorderKeyEvent,
child: _buildFlatLibraryList(_dialogScrollController, hiddenLibraryKeys),
child: _buildFlatLibraryList(_dialogScrollController, hiddenLibraryKeys, shrinkWrap: false),
),
),
),
@@ -403,6 +403,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet>
}
return Column(
mainAxisSize: .min,
children: [
BottomSheetHeader(title: t.libraries.manageLibraries, icon: Symbols.edit_rounded),
Flexible(
@@ -411,7 +412,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet>
descendantsAreFocusable: false,
autofocus: InputModeTracker.isKeyboardMode(context),
onKeyEvent: handleReorderKeyEvent,
child: _buildFlatLibraryList(_sheetScrollController, hiddenLibraryKeys),
child: _buildFlatLibraryList(_sheetScrollController, hiddenLibraryKeys, shrinkWrap: true),
),
),
],
@@ -421,12 +422,17 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet>
/// Build flat library list with a server subtitle when multiple servers are
/// connected. The TV dialog passes [_dialogScrollController] so focused rows
/// can be scrolled into view; the bottom sheet passes its own controller.
Widget _buildFlatLibraryList(ScrollController scrollController, Set<String> hiddenLibraryKeys) {
Widget _buildFlatLibraryList(
ScrollController scrollController,
Set<String> hiddenLibraryKeys, {
required bool shrinkWrap,
}) {
final showServerNames = _hasMultipleServers();
final isKeyboardMode = InputModeTracker.isKeyboardMode(context);
return ReorderableListView.builder(
scrollController: scrollController,
shrinkWrap: shrinkWrap,
onReorderItem: _reorderLibraries,
itemCount: _tempLibraries.length,
padding: const EdgeInsets.symmetric(vertical: 8),
+59 -6
View File
@@ -1,4 +1,5 @@
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
@@ -102,12 +103,29 @@ class OverlaySheetController {
_state._autoFocus(clearSelectSuppression: false);
}
/// Sizing applied when a caller supplies no explicit constraints: capped
/// width on desktop, three quarters of the screen height everywhere.
/// Absolute height ceiling for resizable desktop windows. Without it a 4K
/// window yields a 1620px sheet, which reads as a wall of list rather than a
/// sheet.
static const _windowedMaxHeight = 720.0;
/// Sizing applied when a caller supplies no explicit constraints: three
/// quarters of the viewport height everywhere, the capped width on wide
/// viewports, and the absolute height ceiling on desktop windows only.
///
/// Both caps require `width > 600`. The height ceiling additionally requires
/// a desktop OS and not TV, because it exists for a window the user can
/// resize arbitrarily tall: a portrait tablet and a 10-foot UI keep the full
/// 75%, and so does a desktop window narrower than 601px, which is
/// phone-shaped and where 75% is the norm.
static BoxConstraints _defaultSheetConstraints(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final isDesktop = size.width > 600;
return BoxConstraints(maxWidth: isDesktop ? 700 : double.infinity, maxHeight: size.height * 0.75);
final isWideViewport = size.width > 600;
final isDesktopWindow = isWideViewport && PlatformDetector.isDesktopOS() && !PlatformDetector.isTV();
final maxHeight = size.height * 0.75;
return BoxConstraints(
maxWidth: isWideViewport ? 700 : double.infinity,
maxHeight: isDesktopWindow ? math.min(maxHeight, _windowedMaxHeight) : maxHeight,
);
}
/// Show a sheet using the overlay system if available, otherwise fall back
@@ -288,6 +306,15 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
Offset? _lastPointerPosition;
double? _sheetHorizontalAnchor;
/// Bumped on every [_show]. Keys the resize animation so a freshly opened
/// sheet adopts its own height immediately instead of animating down from
/// the previous sheet's; nested pushes within one sheet still animate.
///
/// Changing the key also remounts the sheet subtree, so a `show` that
/// replaces a live sheet of the same widget type starts with fresh [State]
/// rather than reconciling into the outgoing sheet's.
int _sheetSession = 0;
// Drag-to-dismiss state
double _dragOffset = 0;
bool _isDragging = false;
@@ -356,6 +383,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
setState(() {
_pageStack.add(entry);
_sheetSession++;
_isOpen = true;
_isClosing = false;
_barrierDismissible = barrierDismissible;
@@ -602,9 +630,22 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
return renderBox?.size.height ?? 300;
}
/// Minimum drag distance that dismisses a sheet, regardless of how short the
/// sheet is. Content-sized sheets can be ~150px tall, where a bare 25% of the
/// height is barely more than touch slop, so a slow nudge while scrolling or
/// reaching would close them. Fast flicks are already handled by the velocity
/// check in [_checkDismiss].
static const _minDismissDrag = 96.0;
/// Ceiling on that floor, as a fraction of the sheet. A one-row menu can be
/// shorter than [_minDismissDrag], and an unclamped floor would mean the only
/// way to dismiss it by distance is to drag it clean off the screen.
static const _maxDismissDragFraction = 0.6;
void _checkDismiss(double velocity) {
final sheetHeight = _getSheetHeight();
if (_dragOffset > sheetHeight * 0.25 || velocity > 500) {
final threshold = math.min(math.max(sheetHeight * 0.25, _minDismissDrag), sheetHeight * _maxDismissDragFraction);
if (_dragOffset > threshold || velocity > 500) {
_close();
} else {
setState(() {
@@ -734,7 +775,19 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
bottom: !isTop,
left: false,
right: false,
child: ConstrainedBox(constraints: effectiveConstraints, child: sheetContent),
// Content is sized by the sheet body, so pushing a nested
// page or resolving async content changes the sheet's
// height. Ease the box between those heights instead of
// snapping. The child is laid out at its final size and
// pinned to the anchored edge throughout, so it is revealed
// rather than stretched.
child: AnimatedSize(
key: ValueKey(_sheetSession),
duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic,
alignment: isTop ? Alignment.topCenter : Alignment.bottomCenter,
child: ConstrainedBox(constraints: effectiveConstraints, child: sheetContent),
),
),
),
),
+50 -73
View File
@@ -56,7 +56,6 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
final Map<String, Timer> _autoSaveTimers = {};
final Map<String, _TrackerRatingSource> _trackerSourcesByKey = {};
final Set<String> _pendingAutoSaves = {};
final Set<TrackerService> _hiddenTrackers = {};
final Set<String> _loading = {};
final Map<String, _SectionStatus> _statuses = {};
TrackerIdResolver? _resolver;
@@ -86,17 +85,13 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final maxHeight = size.height * (size.width > 600 ? 0.64 : 0.74);
// Trakt's account provider is watched by [_trackerSources] via `context`.
return Consumer<TrackersProvider>(
builder: (context, trackers, _) {
final allTrackerSources = _trackerSources(context);
final trackerSources = allTrackerSources.where((source) => !_hiddenTrackers.contains(source.service)).toList();
final trackerSources = _trackerSources(context);
_updateTrackerSourceMap(trackerSources);
_resolverNeedsFribb = trackers.isMalConnected || trackers.isAnilistConnected;
_queueTrackerScoreLoad(allTrackerSources);
_queueTrackerScoreLoad(trackerSources);
final serverCaps = widget.serverClient?.capabilities;
final showServerRow = serverCaps != null && (serverCaps.numericUserRating || serverCaps.userFavorites);
@@ -106,47 +101,49 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
];
var focusIndex = 0;
return ConstrainedBox(
constraints: BoxConstraints(maxHeight: maxHeight),
child: Column(
mainAxisSize: .min,
children: [
BottomSheetHeader(title: t.rateSheet.title, icon: Symbols.star_rounded),
Flexible(
child: ListView(
padding: const EdgeInsets.fromLTRB(10, 4, 10, 12),
children: [
if (showServerRow)
_buildServerRow(
widget.serverClient!,
_serverFocusNode,
autofocus: focusIndex == 0,
onNavigateUp: _navTo(focusNodes, focusIndex - 1),
onNavigateDown: _navTo(focusNodes, focusIndex++ + 1),
// Hugs its content: a handful of rows in a 720px sheet was mostly empty
// space. The row set is therefore fixed from the first frame — see
// [_loadTrackerScores], which marks an unratable tracker `notAvailable`
// rather than removing its row.
return Column(
mainAxisSize: .min,
children: [
BottomSheetHeader(title: t.rateSheet.title, icon: Symbols.star_rounded),
Flexible(
child: ListView(
shrinkWrap: true,
padding: const EdgeInsets.fromLTRB(10, 4, 10, 12),
children: [
if (showServerRow)
_buildServerRow(
widget.serverClient!,
_serverFocusNode,
autofocus: focusIndex == 0,
onNavigateUp: _navTo(focusNodes, focusIndex - 1),
onNavigateDown: _navTo(focusNodes, focusIndex++ + 1),
),
for (final source in trackerSources)
_buildTrackerRow(
source,
_trackerFocusNode(source.service),
autofocus: focusIndex == 0,
onNavigateUp: _navTo(focusNodes, focusIndex - 1),
onNavigateDown: _navTo(focusNodes, focusIndex++ + 1),
),
if (trackerSources.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4),
child: Text(
t.rateSheet.noConnectedServices,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
for (final source in trackerSources)
_buildTrackerRow(
source,
_trackerFocusNode(source.service),
autofocus: focusIndex == 0,
onNavigateUp: _navTo(focusNodes, focusIndex - 1),
onNavigateDown: _navTo(focusNodes, focusIndex++ + 1),
),
if (allTrackerSources.isEmpty)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4),
child: Text(
t.rateSheet.noConnectedServices,
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(color: Theme.of(context).colorScheme.onSurfaceVariant),
),
),
],
),
),
],
),
],
),
),
],
);
},
);
@@ -279,6 +276,11 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
});
}
/// Every tracker that cannot rate this item keeps its row and shows
/// `notAvailable`. Removing a row instead would shorten the sheet several
/// hundred ms after it opens, and because sheets are bottom-anchored that
/// slides the rows above it — which here are live rating controls — out from
/// under the user's finger.
Future<void> _loadTrackerScores(List<_TrackerRatingSource> sources) async {
setState(() {
for (final source in sources) {
@@ -294,12 +296,8 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
if (!mounted) return;
setState(() {
for (final source in sources) {
if (_hidesWhenUnavailable(source)) {
_hideTrackerSource(source);
} else {
_loading.remove(source.service.name);
_statuses[source.service.name] = _SectionStatus(t.rateSheet.notAvailable, isError: true);
}
_loading.remove(source.service.name);
_statuses[source.service.name] = _SectionStatus(t.rateSheet.notAvailable, isError: true);
}
});
return;
@@ -318,12 +316,6 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
} on TrackerRatingUnavailableException catch (e) {
appLogger.d('Rating unavailable', error: e);
if (!mounted) return;
if (_hidesWhenUnavailable(source)) {
setState(() {
_hideTrackerSource(source);
});
return;
}
setState(() {
_statuses[key] = _SectionStatus(t.rateSheet.notAvailable, isError: true);
});
@@ -344,21 +336,6 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
);
}
bool _hidesWhenUnavailable(_TrackerRatingSource source) {
return source.service == TrackerService.mal || source.service == TrackerService.anilist;
}
void _hideTrackerSource(_TrackerRatingSource source) {
final key = source.service.name;
_hiddenTrackers.add(source.service);
_loading.remove(key);
_statuses.remove(key);
_trackerScores.remove(source.service);
_autoSaveTimers.remove(key)?.cancel();
_pendingAutoSaves.remove(key);
_trackerSourcesByKey.remove(key);
}
void _setServerStarUnits(int units) {
final clamped = units.clamp(0, 10).toInt();
if ((_serverStars * 2).round() == clamped) return;
@@ -100,15 +100,23 @@ class _ChapterSheetState extends State<ChapterSheet> {
final currentChapterIndex = chapterSnapshot.data;
Widget content;
if (!widget.chaptersLoaded) {
content = const Center(child: CircularProgressIndicator());
content = const Padding(
padding: EdgeInsets.symmetric(vertical: 32),
child: Center(heightFactor: 1, child: CircularProgressIndicator()),
);
} else if (widget.chapters.isEmpty) {
content = Center(
child: Text(t.videoControls.noChaptersAvailable, style: TextStyle(color: tokens(context).textMuted)),
content = Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Center(
heightFactor: 1,
child: Text(t.videoControls.noChaptersAvailable, style: TextStyle(color: tokens(context).textMuted)),
),
);
} else {
_initialScroll.maybeScrollTo(currentChapterIndex);
content = ListView.builder(
shrinkWrap: true,
controller: _initialScroll.controller,
itemCount: widget.chapters.length,
itemBuilder: (context, index) {
@@ -53,14 +53,19 @@ class _QueueSheetState extends State<QueueSheet> {
Widget content;
if (items.isEmpty) {
content = Center(
child: Text(t.videoControls.noQueueItems, style: TextStyle(color: tokens(context).textMuted)),
content = Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Center(
heightFactor: 1,
child: Text(t.videoControls.noQueueItems, style: TextStyle(color: tokens(context).textMuted)),
),
);
} else {
final currentIndex = items.indexWhere((item) => playbackState.playQueueItemIdFor(item) == currentItemID);
_initialScroll.maybeScrollTo(currentIndex);
content = ListView.builder(
shrinkWrap: true,
controller: _initialScroll.controller,
itemCount: items.length,
itemBuilder: (context, index) {
@@ -1,5 +1,10 @@
import 'package:flutter/material.dart';
/// Left-aligned label above a sheet selection column.
///
/// The [Align] has no `heightFactor`, so it only shrink-wraps while it sits on
/// an unbounded main axis — i.e. as a non-flex child of a [Column]. Placing it
/// under a [Flexible] would make it fill the sheet's whole height cap.
class SheetColumnHeader extends StatelessWidget {
final String label;
@@ -76,11 +76,15 @@ class _SheetSelectionColumnState extends State<SheetSelectionColumn> implements
_initialScroll.maybeScrollTo(widget.initialIndex);
return Column(
mainAxisSize: .min,
children: [
if (widget.headerLabel != null) SheetColumnHeader(label: widget.headerLabel!),
if (_selectionPending) const LinearProgressIndicator(minHeight: 2),
Expanded(
// Reserve the bar's 2px unconditionally: growing the column mid-tap
// would nudge a content-sized sheet, and the host eases that as a twitch.
SizedBox(height: 2, child: _selectionPending ? const LinearProgressIndicator(minHeight: 2) : null),
Flexible(
child: ListView.builder(
shrinkWrap: true,
controller: _initialScroll.controller,
itemCount: widget.itemCount,
itemBuilder: (context, index) => widget.itemBuilder(context, index, this),
@@ -0,0 +1,39 @@
import 'package:flutter/material.dart';
/// Side-by-side pair of sheet columns separated by a hairline rule.
///
/// The pair is as tall as its taller column, not as tall as the sheet allows.
/// That rules out a plain [VerticalDivider] between the columns: it has no
/// intrinsic height, so under loose constraints it expands to the incoming
/// maximum and drags the whole row to full height. Instead the row sizes
/// itself from the columns alone and the rule is painted over it, stretched to
/// the resolved height.
class SheetSplitColumns extends StatelessWidget {
final Widget start;
final Widget end;
const SheetSplitColumns({super.key, required this.start, required this.end});
@override
Widget build(BuildContext context) {
return Stack(
children: [
Row(
crossAxisAlignment: .start,
children: [
Expanded(child: start),
const SizedBox(width: 1),
Expanded(child: end),
],
),
// Both halves have equal flex, so the reserved 1px gap above is exactly
// at the horizontal centre. VerticalDivider has no intrinsic height and
// fills whatever it is given — harmless here, because a positioned
// child cannot influence the Stack's size.
Positioned.fill(
child: Center(child: VerticalDivider(width: 1, color: Theme.of(context).dividerColor)),
),
],
);
}
}
@@ -241,6 +241,11 @@ class _SubtitleSearchSheetState extends State<SubtitleSearchSheet> with Controll
title: t.videoControls.searchSubtitles,
icon: Symbols.search_rounded,
onBack: () => OverlaySheetController.of(context).pop(),
// Deliberately fills the sheet's height cap instead of hugging content.
// Overlay sheets are bottom-anchored, so a content-driven height would
// move the search field on every state transition — spinner, results,
// error, empty — while the user is still typing in it. A search surface
// needs a stable frame; the results list normally fills it anyway.
child: Column(
children: [
Padding(
@@ -440,6 +445,9 @@ class _LanguagePickerViewState extends State<_LanguagePickerView> with Controlle
title: t.videoControls.language,
icon: Symbols.language_rounded,
onBack: widget.onBack,
// Fills the height cap for the same reason as the search body: the filter
// field is autofocused and refilters on every keystroke, so a
// content-driven height would slide the field the user is typing in.
child: Column(
children: [
Padding(
@@ -11,6 +11,7 @@ import '../../../widgets/focusable_list_tile.dart';
import '../../../widgets/overlay_sheet.dart';
import 'base_video_control_sheet.dart';
import 'sheet_selection_column.dart';
import 'sheet_split_columns.dart';
import 'subtitle_search_sheet.dart';
import '../models/track_controls_state.dart';
import '../helpers/track_filter_helper.dart';
@@ -108,13 +109,9 @@ class TrackSheet extends StatelessWidget {
}
if (showAudio && showSubtitles) {
return Row(
crossAxisAlignment: .start,
children: [
Expanded(child: FocusTraversalGroup(child: audioColumnFor(selection, true))),
VerticalDivider(width: 1, color: Theme.of(context).dividerColor),
Expanded(child: FocusTraversalGroup(child: subtitleColumnFor(selection, true))),
],
return SheetSplitColumns(
start: FocusTraversalGroup(child: audioColumnFor(selection, true)),
end: FocusTraversalGroup(child: subtitleColumnFor(selection, true)),
);
}
@@ -9,6 +9,7 @@ import '../../../utils/quality_preset_labels.dart';
import '../../../widgets/focusable_list_tile.dart';
import '../../../widgets/overlay_sheet.dart';
import 'sheet_selection_column.dart';
import 'sheet_split_columns.dart';
String versionQualityPickerTitle({required bool showVersions, required bool showQuality}) {
return showQuality
@@ -73,14 +74,7 @@ class VersionQualityPicker extends StatelessWidget {
);
if (showVersions && showQuality) {
return Row(
crossAxisAlignment: .start,
children: [
Expanded(child: versionColumn),
VerticalDivider(width: 1, color: Theme.of(context).dividerColor),
Expanded(child: qualityColumn),
],
);
return SheetSplitColumns(start: versionColumn, end: qualityColumn);
} else if (showVersions) {
return versionColumn;
} else {
@@ -529,6 +529,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
final isDesktop = PlatformDetector.isDesktop(context);
return ListView(
shrinkWrap: true,
children: [
// Playback Speed - hidden for live TV and when user cannot control playback
if (_state.canControl && !_state.isLive)
@@ -756,6 +757,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
final primary = Theme.of(context).colorScheme.primary;
return ListView(
shrinkWrap: true,
children: [
for (final mode in modes)
FocusableListTile(
@@ -796,6 +798,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
];
return ListView.builder(
shrinkWrap: true,
itemCount: speeds.length,
itemBuilder: (context, index) {
final speed = speeds[index];
@@ -826,6 +829,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
final primary = Theme.of(context).colorScheme.primary;
return ListView(
shrinkWrap: true,
children: [
FocusableListTile(
leading: AppIcon(Symbols.restart_alt_rounded, fill: 1, color: tokens(context).textMuted),
@@ -927,6 +931,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
}
return ListView(
shrinkWrap: true,
children: [
for (final d in ungrouped) _buildDeviceTile(d, currentDevice),
for (final entry in groups.entries) ...[
@@ -948,7 +953,25 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
}
Widget _buildFlatDeviceList(List<AudioDevice> devices, AudioDevice currentDevice) {
// The device list arrives asynchronously, so an empty list is the normal
// first frame. Without a placeholder the shrink-wrapped page would render
// as a bare header and then jump once devices land.
//
// A fixed placeholder rather than FiltersBottomSheet's hold-the-outgoing-
// height technique: this page is entered from the menu, whose height is
// unrelated to a device list, so holding it would be arbitrary. One small
// upward move when the devices land beats two.
if (devices.isEmpty) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 32),
child: Center(
heightFactor: 1,
child: Text(t.videoControls.noAudioDevicesAvailable, style: TextStyle(color: tokens(context).textMuted)),
),
);
}
return ListView.builder(
shrinkWrap: true,
itemCount: devices.length,
itemBuilder: (context, index) => _buildDeviceTile(devices[index], currentDevice),
);
@@ -979,6 +1002,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
// +1 for the import button at the end
return ListView.builder(
shrinkWrap: true,
itemCount: presets.length + 1,
itemBuilder: (context, index) {
if (index == presets.length) {
@@ -31,6 +31,7 @@ class SleepTimerActiveStatus extends StatelessWidget {
padding: const EdgeInsets.all(16),
color: Colors.amber.withValues(alpha: 0.1),
child: Column(
mainAxisSize: .min,
children: [
Text(
t.videoControls.timerActive,
@@ -10,13 +10,14 @@ import '../../../widgets/app_icon.dart';
import '../../../widgets/focusable_list_tile.dart';
import '../../../widgets/overlay_sheet.dart';
import '../sheets/sheet_column_header.dart';
import '../sheets/sheet_split_columns.dart';
import 'sleep_timer_active_status.dart';
/// Shared UI for sleep timer selection and active status.
///
/// Layout mirrors the audio/subtitle [TrackSheet]: two side-by-side columns
/// inside a [Row], each in its own [FocusTraversalGroup] so D-pad navigation
/// stays inside the column the user is acting on.
/// inside a [SheetSplitColumns], each in its own [FocusTraversalGroup] so
/// D-pad navigation stays inside the column the user is acting on.
///
/// * Left column ("Stop at") — event-based stop options. Today this is just
/// "End of current video"; the column is intentionally open-ended for
@@ -48,31 +49,24 @@ class SleepTimerContent extends StatelessWidget {
final showActiveStatus = sleepTimer.isActive && (remainingTime != null || sleepTimer.isEndOfVideoMode);
return Column(
mainAxisSize: .min,
children: [
if (showActiveStatus) ...[
SleepTimerActiveStatus(sleepTimer: sleepTimer, remainingTime: remainingTime, onCancel: onCancel),
Divider(color: Theme.of(context).dividerColor, height: 1),
],
Expanded(
child: Row(
crossAxisAlignment: .start,
children: [
Expanded(
child: FocusTraversalGroup(
child: _SleepTimerEventColumn(player: player, sleepTimer: sleepTimer),
),
Flexible(
child: SheetSplitColumns(
start: FocusTraversalGroup(
child: _SleepTimerEventColumn(player: player, sleepTimer: sleepTimer),
),
end: FocusTraversalGroup(
child: _SleepTimerDurationColumn(
player: player,
sleepTimer: sleepTimer,
defaultDuration: defaultDuration,
),
VerticalDivider(width: 1, color: Theme.of(context).dividerColor),
Expanded(
child: FocusTraversalGroup(
child: _SleepTimerDurationColumn(
player: player,
sleepTimer: sleepTimer,
defaultDuration: defaultDuration,
),
),
),
],
),
),
),
],
@@ -93,10 +87,12 @@ class _SleepTimerEventColumn extends StatelessWidget {
final label = t.videoControls.sleepTimerEndOfVideo;
return Column(
mainAxisSize: .min,
children: [
SheetColumnHeader(label: t.videoControls.sleepTimerStopAtHeader),
Expanded(
Flexible(
child: ListView(
shrinkWrap: true,
children: [
FocusableListTile(
leading: AppIcon(
@@ -147,10 +143,12 @@ class _SleepTimerDurationColumn extends StatelessWidget {
: null;
return Column(
mainAxisSize: .min,
children: [
SheetColumnHeader(label: t.videoControls.sleepTimerDurationHeader),
Expanded(
Flexible(
child: ListView.builder(
shrinkWrap: true,
itemCount: durations.length,
itemBuilder: (context, index) {
final minutes = durations[index];