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:
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Hissələr əlçatan deyil",
|
"noChaptersAvailable": "Hissələr əlçatan deyil",
|
||||||
"queue": "Növbə",
|
"queue": "Növbə",
|
||||||
"noQueueItems": "Növbədə element yoxdur",
|
"noQueueItems": "Növbədə element yoxdur",
|
||||||
|
"noAudioDevicesAvailable": "Səs cihazları əlçatan deyil",
|
||||||
"searchSubtitles": "Altyazı axtar",
|
"searchSubtitles": "Altyazı axtar",
|
||||||
"language": "Dil",
|
"language": "Dil",
|
||||||
"noSubtitlesFound": "Altyazı tapılmadı",
|
"noSubtitlesFound": "Altyazı tapılmadı",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Няма налични глави",
|
"noChaptersAvailable": "Няма налични глави",
|
||||||
"queue": "Опашка",
|
"queue": "Опашка",
|
||||||
"noQueueItems": "Няма елементи в опашката",
|
"noQueueItems": "Няма елементи в опашката",
|
||||||
|
"noAudioDevicesAvailable": "Няма налични аудио устройства",
|
||||||
"searchSubtitles": "Търсене на субтитри",
|
"searchSubtitles": "Търсене на субтитри",
|
||||||
"language": "Език",
|
"language": "Език",
|
||||||
"noSubtitlesFound": "Не са намерени субтитри",
|
"noSubtitlesFound": "Не са намерени субтитри",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Ingen kapitler tilgængelige",
|
"noChaptersAvailable": "Ingen kapitler tilgængelige",
|
||||||
"queue": "Kø",
|
"queue": "Kø",
|
||||||
"noQueueItems": "Ingen elementer i køen",
|
"noQueueItems": "Ingen elementer i køen",
|
||||||
|
"noAudioDevicesAvailable": "Ingen lydenheder tilgængelige",
|
||||||
"searchSubtitles": "Søg undertekster",
|
"searchSubtitles": "Søg undertekster",
|
||||||
"language": "Sprog",
|
"language": "Sprog",
|
||||||
"noSubtitlesFound": "Ingen undertekster fundet",
|
"noSubtitlesFound": "Ingen undertekster fundet",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Keine Kapitel verfügbar",
|
"noChaptersAvailable": "Keine Kapitel verfügbar",
|
||||||
"queue": "Warteschlange",
|
"queue": "Warteschlange",
|
||||||
"noQueueItems": "Keine Elemente in der Warteschlange",
|
"noQueueItems": "Keine Elemente in der Warteschlange",
|
||||||
|
"noAudioDevicesAvailable": "Keine Audiogeräte verfügbar",
|
||||||
"searchSubtitles": "Untertitel suchen",
|
"searchSubtitles": "Untertitel suchen",
|
||||||
"language": "Sprache",
|
"language": "Sprache",
|
||||||
"noSubtitlesFound": "Keine Untertitel gefunden",
|
"noSubtitlesFound": "Keine Untertitel gefunden",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "No chapters available",
|
"noChaptersAvailable": "No chapters available",
|
||||||
"queue": "Queue",
|
"queue": "Queue",
|
||||||
"noQueueItems": "No items in queue",
|
"noQueueItems": "No items in queue",
|
||||||
|
"noAudioDevicesAvailable": "No audio devices available",
|
||||||
"searchSubtitles": "Search Subtitles",
|
"searchSubtitles": "Search Subtitles",
|
||||||
"language": "Language",
|
"language": "Language",
|
||||||
"noSubtitlesFound": "No subtitles found",
|
"noSubtitlesFound": "No subtitles found",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "No hay capítulos disponibles",
|
"noChaptersAvailable": "No hay capítulos disponibles",
|
||||||
"queue": "Cola",
|
"queue": "Cola",
|
||||||
"noQueueItems": "No hay elementos en la cola",
|
"noQueueItems": "No hay elementos en la cola",
|
||||||
|
"noAudioDevicesAvailable": "No hay dispositivos de audio disponibles",
|
||||||
"searchSubtitles": "Buscar subtítulos",
|
"searchSubtitles": "Buscar subtítulos",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"noSubtitlesFound": "No se encontraron subtítulos",
|
"noSubtitlesFound": "No se encontraron subtítulos",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Aucun chapitre disponible",
|
"noChaptersAvailable": "Aucun chapitre disponible",
|
||||||
"queue": "File d'attente",
|
"queue": "File d'attente",
|
||||||
"noQueueItems": "Aucun élément dans la file d'attente",
|
"noQueueItems": "Aucun élément dans la file d'attente",
|
||||||
|
"noAudioDevicesAvailable": "Aucun appareil audio disponible",
|
||||||
"searchSubtitles": "Rechercher des sous-titres",
|
"searchSubtitles": "Rechercher des sous-titres",
|
||||||
"language": "Langue",
|
"language": "Langue",
|
||||||
"noSubtitlesFound": "Aucun sous-titre trouvé",
|
"noSubtitlesFound": "Aucun sous-titre trouvé",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Nincsenek elérhető fejezetek",
|
"noChaptersAvailable": "Nincsenek elérhető fejezetek",
|
||||||
"queue": "Lejátszási sor",
|
"queue": "Lejátszási sor",
|
||||||
"noQueueItems": "Nincsenek elemek a sorban",
|
"noQueueItems": "Nincsenek elemek a sorban",
|
||||||
|
"noAudioDevicesAvailable": "Nincsenek elérhető audioeszközök",
|
||||||
"searchSubtitles": "Feliratok keresése",
|
"searchSubtitles": "Feliratok keresése",
|
||||||
"language": "Nyelv",
|
"language": "Nyelv",
|
||||||
"noSubtitlesFound": "Nem találhatók feliratok",
|
"noSubtitlesFound": "Nem találhatók feliratok",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Nessun capitolo disponibile",
|
"noChaptersAvailable": "Nessun capitolo disponibile",
|
||||||
"queue": "Coda",
|
"queue": "Coda",
|
||||||
"noQueueItems": "Nessun elemento in coda",
|
"noQueueItems": "Nessun elemento in coda",
|
||||||
|
"noAudioDevicesAvailable": "Nessun dispositivo audio disponibile",
|
||||||
"searchSubtitles": "Cerca sottotitoli",
|
"searchSubtitles": "Cerca sottotitoli",
|
||||||
"language": "Lingua",
|
"language": "Lingua",
|
||||||
"noSubtitlesFound": "Nessun sottotitolo trovato",
|
"noSubtitlesFound": "Nessun sottotitolo trovato",
|
||||||
|
|||||||
@@ -678,6 +678,7 @@
|
|||||||
"noChaptersAvailable": "チャプターがありません",
|
"noChaptersAvailable": "チャプターがありません",
|
||||||
"queue": "キュー",
|
"queue": "キュー",
|
||||||
"noQueueItems": "キューにアイテムがありません",
|
"noQueueItems": "キューにアイテムがありません",
|
||||||
|
"noAudioDevicesAvailable": "オーディオデバイスがありません",
|
||||||
"searchSubtitles": "字幕を検索",
|
"searchSubtitles": "字幕を検索",
|
||||||
"language": "言語",
|
"language": "言語",
|
||||||
"noSubtitlesFound": "字幕が見つかりません",
|
"noSubtitlesFound": "字幕が見つかりません",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Бөлімдер қолжетімсіз",
|
"noChaptersAvailable": "Бөлімдер қолжетімсіз",
|
||||||
"queue": "Кезек",
|
"queue": "Кезек",
|
||||||
"noQueueItems": "Кезекте элементтер жоқ",
|
"noQueueItems": "Кезекте элементтер жоқ",
|
||||||
|
"noAudioDevicesAvailable": "Қолжетімді аудио құрылғылар жоқ",
|
||||||
"searchSubtitles": "Субтитр іздеу",
|
"searchSubtitles": "Субтитр іздеу",
|
||||||
"language": "Тіл",
|
"language": "Тіл",
|
||||||
"noSubtitlesFound": "Субтитр табылмады",
|
"noSubtitlesFound": "Субтитр табылмады",
|
||||||
|
|||||||
@@ -678,6 +678,7 @@
|
|||||||
"noChaptersAvailable": "사용 가능한 챕터가 없습니다",
|
"noChaptersAvailable": "사용 가능한 챕터가 없습니다",
|
||||||
"queue": "재생 대기열",
|
"queue": "재생 대기열",
|
||||||
"noQueueItems": "대기열에 항목이 없습니다",
|
"noQueueItems": "대기열에 항목이 없습니다",
|
||||||
|
"noAudioDevicesAvailable": "사용 가능한 오디오 기기가 없습니다",
|
||||||
"searchSubtitles": "자막 검색",
|
"searchSubtitles": "자막 검색",
|
||||||
"language": "언어",
|
"language": "언어",
|
||||||
"noSubtitlesFound": "자막을 찾을 수 없습니다",
|
"noSubtitlesFound": "자막을 찾을 수 없습니다",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Ingen kapitler tilgjengelig",
|
"noChaptersAvailable": "Ingen kapitler tilgjengelig",
|
||||||
"queue": "Kø",
|
"queue": "Kø",
|
||||||
"noQueueItems": "Ingen elementer i kø",
|
"noQueueItems": "Ingen elementer i kø",
|
||||||
|
"noAudioDevicesAvailable": "Ingen lydenheter tilgjengelig",
|
||||||
"searchSubtitles": "Søk etter undertekster",
|
"searchSubtitles": "Søk etter undertekster",
|
||||||
"language": "Språk",
|
"language": "Språk",
|
||||||
"noSubtitlesFound": "Ingen undertekster funnet",
|
"noSubtitlesFound": "Ingen undertekster funnet",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Geen hoofdstukken beschikbaar",
|
"noChaptersAvailable": "Geen hoofdstukken beschikbaar",
|
||||||
"queue": "Wachtrij",
|
"queue": "Wachtrij",
|
||||||
"noQueueItems": "Geen items in de wachtrij",
|
"noQueueItems": "Geen items in de wachtrij",
|
||||||
|
"noAudioDevicesAvailable": "Geen audioapparaten beschikbaar",
|
||||||
"searchSubtitles": "Ondertitels zoeken",
|
"searchSubtitles": "Ondertitels zoeken",
|
||||||
"language": "Taal",
|
"language": "Taal",
|
||||||
"noSubtitlesFound": "Geen ondertitels gevonden",
|
"noSubtitlesFound": "Geen ondertitels gevonden",
|
||||||
|
|||||||
@@ -687,6 +687,7 @@
|
|||||||
"noChaptersAvailable": "Brak dostępnych rozdziałów",
|
"noChaptersAvailable": "Brak dostępnych rozdziałów",
|
||||||
"queue": "Kolejka",
|
"queue": "Kolejka",
|
||||||
"noQueueItems": "Brak elementów w kolejce",
|
"noQueueItems": "Brak elementów w kolejce",
|
||||||
|
"noAudioDevicesAvailable": "Brak dostępnych urządzeń audio",
|
||||||
"searchSubtitles": "Szukaj napisów",
|
"searchSubtitles": "Szukaj napisów",
|
||||||
"language": "Język",
|
"language": "Język",
|
||||||
"noSubtitlesFound": "Nie znaleziono napisów",
|
"noSubtitlesFound": "Nie znaleziono napisów",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Nenhum capítulo disponível",
|
"noChaptersAvailable": "Nenhum capítulo disponível",
|
||||||
"queue": "Fila",
|
"queue": "Fila",
|
||||||
"noQueueItems": "Nenhum item na fila",
|
"noQueueItems": "Nenhum item na fila",
|
||||||
|
"noAudioDevicesAvailable": "Nenhum dispositivo de áudio disponível",
|
||||||
"searchSubtitles": "Pesquisar legendas",
|
"searchSubtitles": "Pesquisar legendas",
|
||||||
"language": "Idioma",
|
"language": "Idioma",
|
||||||
"noSubtitlesFound": "Nenhuma legenda encontrada",
|
"noSubtitlesFound": "Nenhuma legenda encontrada",
|
||||||
|
|||||||
@@ -687,6 +687,7 @@
|
|||||||
"noChaptersAvailable": "Главы недоступны",
|
"noChaptersAvailable": "Главы недоступны",
|
||||||
"queue": "Очередь",
|
"queue": "Очередь",
|
||||||
"noQueueItems": "В очереди нет элементов",
|
"noQueueItems": "В очереди нет элементов",
|
||||||
|
"noAudioDevicesAvailable": "Нет доступных аудиоустройств",
|
||||||
"searchSubtitles": "Поиск субтитров",
|
"searchSubtitles": "Поиск субтитров",
|
||||||
"language": "Язык",
|
"language": "Язык",
|
||||||
"noSubtitlesFound": "Субтитры не найдены",
|
"noSubtitlesFound": "Субтитры не найдены",
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
/// To regenerate, run: `dart run slang`
|
/// To regenerate, run: `dart run slang`
|
||||||
///
|
///
|
||||||
/// Locales: 22
|
/// Locales: 22
|
||||||
/// Strings: 38852 (1766 per locale)
|
/// Strings: 38874 (1767 per locale)
|
||||||
|
|
||||||
// coverage:ignore-file
|
// coverage:ignore-file
|
||||||
// ignore_for_file: type=lint, unused_import
|
// ignore_for_file: type=lint, unused_import
|
||||||
|
|||||||
@@ -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 noChaptersAvailable => 'Hissələr əlçatan deyil';
|
||||||
@override String get queue => 'Növbə';
|
@override String get queue => 'Növbə';
|
||||||
@override String get noQueueItems => 'Növbədə element yoxdur';
|
@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 searchSubtitles => 'Altyazı axtar';
|
||||||
@override String get language => 'Dil';
|
@override String get language => 'Dil';
|
||||||
@override String get noSubtitlesFound => 'Altyazı tapılmadı';
|
@override String get noSubtitlesFound => 'Altyazı tapılmadı';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsAz {
|
|||||||
'videoControls.noChaptersAvailable' => 'Hissələr əlçatan deyil',
|
'videoControls.noChaptersAvailable' => 'Hissələr əlçatan deyil',
|
||||||
'videoControls.queue' => 'Növbə',
|
'videoControls.queue' => 'Növbə',
|
||||||
'videoControls.noQueueItems' => 'Növbədə element yoxdur',
|
'videoControls.noQueueItems' => 'Növbədə element yoxdur',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Səs cihazları əlçatan deyil',
|
||||||
'videoControls.searchSubtitles' => 'Altyazı axtar',
|
'videoControls.searchSubtitles' => 'Altyazı axtar',
|
||||||
'videoControls.language' => 'Dil',
|
'videoControls.language' => 'Dil',
|
||||||
'videoControls.noSubtitlesFound' => 'Altyazı tapılmadı',
|
'videoControls.noSubtitlesFound' => 'Altyazı tapılmadı',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsAz {
|
|||||||
'explore.badge.requested' => 'Sorğu göndərildi',
|
'explore.badge.requested' => 'Sorğu göndərildi',
|
||||||
'explore.badge.pendingApproval' => 'Təsdiq gözlənilir',
|
'explore.badge.pendingApproval' => 'Təsdiq gözlənilir',
|
||||||
'explore.badge.processing' => 'Emal edilir',
|
'explore.badge.processing' => 'Emal edilir',
|
||||||
'explore.badge.declined' => 'Rədd edildi',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Rədd edildi',
|
||||||
'explore.badge.requestFailed' => 'Sorğu uğursuz oldu',
|
'explore.badge.requestFailed' => 'Sorğu uğursuz oldu',
|
||||||
'explore.badge.requested4k' => '4K sorğu göndərildi',
|
'explore.badge.requested4k' => '4K sorğu göndərildi',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} mövsüm',
|
'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.dropped' => 'İtirilmiş kadrlar',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU-ları',
|
'performanceOverlay.dvRpus' => 'DV RPU-ları',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU Ort.',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU Ort.',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV Nümunə Ort.',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV Nümunə Ort.',
|
||||||
'performanceOverlay.maxLuma' => 'Maks Luma',
|
'performanceOverlay.maxLuma' => 'Maks Luma',
|
||||||
'performanceOverlay.minLuma' => 'Min Luma',
|
'performanceOverlay.minLuma' => 'Min Luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$bg extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Няма налични глави';
|
@override String get noChaptersAvailable => 'Няма налични глави';
|
||||||
@override String get queue => 'Опашка';
|
@override String get queue => 'Опашка';
|
||||||
@override String get noQueueItems => 'Няма елементи в опашката';
|
@override String get noQueueItems => 'Няма елементи в опашката';
|
||||||
|
@override String get noAudioDevicesAvailable => 'Няма налични аудио устройства';
|
||||||
@override String get searchSubtitles => 'Търсене на субтитри';
|
@override String get searchSubtitles => 'Търсене на субтитри';
|
||||||
@override String get language => 'Език';
|
@override String get language => 'Език';
|
||||||
@override String get noSubtitlesFound => 'Не са намерени субтитри';
|
@override String get noSubtitlesFound => 'Не са намерени субтитри';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsBg {
|
|||||||
'videoControls.noChaptersAvailable' => 'Няма налични глави',
|
'videoControls.noChaptersAvailable' => 'Няма налични глави',
|
||||||
'videoControls.queue' => 'Опашка',
|
'videoControls.queue' => 'Опашка',
|
||||||
'videoControls.noQueueItems' => 'Няма елементи в опашката',
|
'videoControls.noQueueItems' => 'Няма елементи в опашката',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Няма налични аудио устройства',
|
||||||
'videoControls.searchSubtitles' => 'Търсене на субтитри',
|
'videoControls.searchSubtitles' => 'Търсене на субтитри',
|
||||||
'videoControls.language' => 'Език',
|
'videoControls.language' => 'Език',
|
||||||
'videoControls.noSubtitlesFound' => 'Не са намерени субтитри',
|
'videoControls.noSubtitlesFound' => 'Не са намерени субтитри',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsBg {
|
|||||||
'explore.badge.requested' => 'Заявен',
|
'explore.badge.requested' => 'Заявен',
|
||||||
'explore.badge.pendingApproval' => 'В очакване на одобрение',
|
'explore.badge.pendingApproval' => 'В очакване на одобрение',
|
||||||
'explore.badge.processing' => 'Обработва се',
|
'explore.badge.processing' => 'Обработва се',
|
||||||
'explore.badge.declined' => 'Отхвърлен',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Отхвърлен',
|
||||||
'explore.badge.requestFailed' => 'Заявката се провали',
|
'explore.badge.requestFailed' => 'Заявката се провали',
|
||||||
'explore.badge.requested4k' => 'Заявен в 4K',
|
'explore.badge.requested4k' => 'Заявен в 4K',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} сезона',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} сезона',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsBg {
|
|||||||
'performanceOverlay.dropped' => 'Пропуснати кадри',
|
'performanceOverlay.dropped' => 'Пропуснати кадри',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU',
|
'performanceOverlay.dvRpus' => 'DV RPU',
|
||||||
'performanceOverlay.dvRpuAverage' => 'Средно DV RPU',
|
'performanceOverlay.dvRpuAverage' => 'Средно DV RPU',
|
||||||
'performanceOverlay.dvSampleAverage' => 'Средно DV семпл',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'Средно DV семпл',
|
||||||
'performanceOverlay.maxLuma' => 'Макс. яркост',
|
'performanceOverlay.maxLuma' => 'Макс. яркост',
|
||||||
'performanceOverlay.minLuma' => 'Мин. яркост',
|
'performanceOverlay.minLuma' => 'Мин. яркост',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$da extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Ingen kapitler tilgængelige';
|
@override String get noChaptersAvailable => 'Ingen kapitler tilgængelige';
|
||||||
@override String get queue => 'Kø';
|
@override String get queue => 'Kø';
|
||||||
@override String get noQueueItems => 'Ingen elementer i køen';
|
@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 searchSubtitles => 'Søg undertekster';
|
||||||
@override String get language => 'Sprog';
|
@override String get language => 'Sprog';
|
||||||
@override String get noSubtitlesFound => 'Ingen undertekster fundet';
|
@override String get noSubtitlesFound => 'Ingen undertekster fundet';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsDa {
|
|||||||
'videoControls.noChaptersAvailable' => 'Ingen kapitler tilgængelige',
|
'videoControls.noChaptersAvailable' => 'Ingen kapitler tilgængelige',
|
||||||
'videoControls.queue' => 'Kø',
|
'videoControls.queue' => 'Kø',
|
||||||
'videoControls.noQueueItems' => 'Ingen elementer i køen',
|
'videoControls.noQueueItems' => 'Ingen elementer i køen',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Ingen lydenheder tilgængelige',
|
||||||
'videoControls.searchSubtitles' => 'Søg undertekster',
|
'videoControls.searchSubtitles' => 'Søg undertekster',
|
||||||
'videoControls.language' => 'Sprog',
|
'videoControls.language' => 'Sprog',
|
||||||
'videoControls.noSubtitlesFound' => 'Ingen undertekster fundet',
|
'videoControls.noSubtitlesFound' => 'Ingen undertekster fundet',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsDa {
|
|||||||
'explore.badge.requested' => 'Anmodet',
|
'explore.badge.requested' => 'Anmodet',
|
||||||
'explore.badge.pendingApproval' => 'Afventer godkendelse',
|
'explore.badge.pendingApproval' => 'Afventer godkendelse',
|
||||||
'explore.badge.processing' => 'Behandles',
|
'explore.badge.processing' => 'Behandles',
|
||||||
'explore.badge.declined' => 'Afvist',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Afvist',
|
||||||
'explore.badge.requestFailed' => 'Anmodningen mislykkedes',
|
'explore.badge.requestFailed' => 'Anmodningen mislykkedes',
|
||||||
'explore.badge.requested4k' => '4K anmodet',
|
'explore.badge.requested4k' => '4K anmodet',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} sæsoner',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} sæsoner',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsDa {
|
|||||||
'performanceOverlay.dropped' => 'Tabte',
|
'performanceOverlay.dropped' => 'Tabte',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU’er',
|
'performanceOverlay.dvRpus' => 'DV RPU’er',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU gns.',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU gns.',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV-sample gns.',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV-sample gns.',
|
||||||
'performanceOverlay.maxLuma' => 'Maks. luma',
|
'performanceOverlay.maxLuma' => 'Maks. luma',
|
||||||
'performanceOverlay.minLuma' => 'Min. luma',
|
'performanceOverlay.minLuma' => 'Min. luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$de extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Keine Kapitel verfügbar';
|
@override String get noChaptersAvailable => 'Keine Kapitel verfügbar';
|
||||||
@override String get queue => 'Warteschlange';
|
@override String get queue => 'Warteschlange';
|
||||||
@override String get noQueueItems => 'Keine Elemente in der 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 searchSubtitles => 'Untertitel suchen';
|
||||||
@override String get language => 'Sprache';
|
@override String get language => 'Sprache';
|
||||||
@override String get noSubtitlesFound => 'Keine Untertitel gefunden';
|
@override String get noSubtitlesFound => 'Keine Untertitel gefunden';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsDe {
|
|||||||
'videoControls.noChaptersAvailable' => 'Keine Kapitel verfügbar',
|
'videoControls.noChaptersAvailable' => 'Keine Kapitel verfügbar',
|
||||||
'videoControls.queue' => 'Warteschlange',
|
'videoControls.queue' => 'Warteschlange',
|
||||||
'videoControls.noQueueItems' => 'Keine Elemente in der Warteschlange',
|
'videoControls.noQueueItems' => 'Keine Elemente in der Warteschlange',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Keine Audiogeräte verfügbar',
|
||||||
'videoControls.searchSubtitles' => 'Untertitel suchen',
|
'videoControls.searchSubtitles' => 'Untertitel suchen',
|
||||||
'videoControls.language' => 'Sprache',
|
'videoControls.language' => 'Sprache',
|
||||||
'videoControls.noSubtitlesFound' => 'Keine Untertitel gefunden',
|
'videoControls.noSubtitlesFound' => 'Keine Untertitel gefunden',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsDe {
|
|||||||
'explore.badge.requested' => 'Angefragt',
|
'explore.badge.requested' => 'Angefragt',
|
||||||
'explore.badge.pendingApproval' => 'Genehmigung ausstehend',
|
'explore.badge.pendingApproval' => 'Genehmigung ausstehend',
|
||||||
'explore.badge.processing' => 'Wird verarbeitet',
|
'explore.badge.processing' => 'Wird verarbeitet',
|
||||||
'explore.badge.declined' => 'Abgelehnt',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Abgelehnt',
|
||||||
'explore.badge.requestFailed' => 'Anfrage fehlgeschlagen',
|
'explore.badge.requestFailed' => 'Anfrage fehlgeschlagen',
|
||||||
'explore.badge.requested4k' => '4K angefragt',
|
'explore.badge.requested4k' => '4K angefragt',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} Staffeln',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} Staffeln',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsDe {
|
|||||||
'performanceOverlay.dropped' => 'Verworfen',
|
'performanceOverlay.dropped' => 'Verworfen',
|
||||||
'performanceOverlay.dvRpus' => 'DV-RPUs',
|
'performanceOverlay.dvRpus' => 'DV-RPUs',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV-RPU Ø',
|
'performanceOverlay.dvRpuAverage' => 'DV-RPU Ø',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV-Sample Ø',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV-Sample Ø',
|
||||||
'performanceOverlay.maxLuma' => 'Max. Luma',
|
'performanceOverlay.maxLuma' => 'Max. Luma',
|
||||||
'performanceOverlay.minLuma' => 'Min. Luma',
|
'performanceOverlay.minLuma' => 'Min. Luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -2026,6 +2026,9 @@ class Translations$videoControls$en {
|
|||||||
/// en: 'No items in queue'
|
/// en: 'No items in queue'
|
||||||
String get noQueueItems => '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'
|
/// en: 'Search Subtitles'
|
||||||
String get searchSubtitles => 'Search Subtitles';
|
String get searchSubtitles => 'Search Subtitles';
|
||||||
|
|
||||||
@@ -6758,6 +6761,7 @@ extension on Translations {
|
|||||||
'videoControls.noChaptersAvailable' => 'No chapters available',
|
'videoControls.noChaptersAvailable' => 'No chapters available',
|
||||||
'videoControls.queue' => 'Queue',
|
'videoControls.queue' => 'Queue',
|
||||||
'videoControls.noQueueItems' => 'No items in queue',
|
'videoControls.noQueueItems' => 'No items in queue',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'No audio devices available',
|
||||||
'videoControls.searchSubtitles' => 'Search Subtitles',
|
'videoControls.searchSubtitles' => 'Search Subtitles',
|
||||||
'videoControls.language' => 'Language',
|
'videoControls.language' => 'Language',
|
||||||
'videoControls.noSubtitlesFound' => 'No subtitles found',
|
'videoControls.noSubtitlesFound' => 'No subtitles found',
|
||||||
@@ -7143,9 +7147,9 @@ extension on Translations {
|
|||||||
'explore.badge.requested' => 'Requested',
|
'explore.badge.requested' => 'Requested',
|
||||||
'explore.badge.pendingApproval' => 'Pending approval',
|
'explore.badge.pendingApproval' => 'Pending approval',
|
||||||
'explore.badge.processing' => 'Processing',
|
'explore.badge.processing' => 'Processing',
|
||||||
'explore.badge.declined' => 'Declined',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Declined',
|
||||||
'explore.badge.requestFailed' => 'Request failed',
|
'explore.badge.requestFailed' => 'Request failed',
|
||||||
'explore.badge.requested4k' => '4K requested',
|
'explore.badge.requested4k' => '4K requested',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} seasons',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} seasons',
|
||||||
@@ -7657,9 +7661,9 @@ extension on Translations {
|
|||||||
'performanceOverlay.dropped' => 'Dropped',
|
'performanceOverlay.dropped' => 'Dropped',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPUs',
|
'performanceOverlay.dvRpus' => 'DV RPUs',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU Avg',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU Avg',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV Sample Avg',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV Sample Avg',
|
||||||
'performanceOverlay.maxLuma' => 'Max Luma',
|
'performanceOverlay.maxLuma' => 'Max Luma',
|
||||||
'performanceOverlay.minLuma' => 'Min Luma',
|
'performanceOverlay.minLuma' => 'Min Luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$es extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'No hay capítulos disponibles';
|
@override String get noChaptersAvailable => 'No hay capítulos disponibles';
|
||||||
@override String get queue => 'Cola';
|
@override String get queue => 'Cola';
|
||||||
@override String get noQueueItems => 'No hay elementos en la 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 searchSubtitles => 'Buscar subtítulos';
|
||||||
@override String get language => 'Idioma';
|
@override String get language => 'Idioma';
|
||||||
@override String get noSubtitlesFound => 'No se encontraron subtítulos';
|
@override String get noSubtitlesFound => 'No se encontraron subtítulos';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsEs {
|
|||||||
'videoControls.noChaptersAvailable' => 'No hay capítulos disponibles',
|
'videoControls.noChaptersAvailable' => 'No hay capítulos disponibles',
|
||||||
'videoControls.queue' => 'Cola',
|
'videoControls.queue' => 'Cola',
|
||||||
'videoControls.noQueueItems' => 'No hay elementos en la cola',
|
'videoControls.noQueueItems' => 'No hay elementos en la cola',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'No hay dispositivos de audio disponibles',
|
||||||
'videoControls.searchSubtitles' => 'Buscar subtítulos',
|
'videoControls.searchSubtitles' => 'Buscar subtítulos',
|
||||||
'videoControls.language' => 'Idioma',
|
'videoControls.language' => 'Idioma',
|
||||||
'videoControls.noSubtitlesFound' => 'No se encontraron subtítulos',
|
'videoControls.noSubtitlesFound' => 'No se encontraron subtítulos',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsEs {
|
|||||||
'explore.badge.requested' => 'Solicitado',
|
'explore.badge.requested' => 'Solicitado',
|
||||||
'explore.badge.pendingApproval' => 'Pendiente de aprobación',
|
'explore.badge.pendingApproval' => 'Pendiente de aprobación',
|
||||||
'explore.badge.processing' => 'Procesando',
|
'explore.badge.processing' => 'Procesando',
|
||||||
'explore.badge.declined' => 'Rechazado',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Rechazado',
|
||||||
'explore.badge.requestFailed' => 'La solicitud falló',
|
'explore.badge.requestFailed' => 'La solicitud falló',
|
||||||
'explore.badge.requested4k' => 'Solicitado en 4K',
|
'explore.badge.requested4k' => 'Solicitado en 4K',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} temporadas',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} temporadas',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsEs {
|
|||||||
'performanceOverlay.dropped' => 'Descartados',
|
'performanceOverlay.dropped' => 'Descartados',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPUs',
|
'performanceOverlay.dvRpus' => 'DV RPUs',
|
||||||
'performanceOverlay.dvRpuAverage' => 'Prom. DV RPU',
|
'performanceOverlay.dvRpuAverage' => 'Prom. DV RPU',
|
||||||
'performanceOverlay.dvSampleAverage' => 'Prom. muestra DV',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'Prom. muestra DV',
|
||||||
'performanceOverlay.maxLuma' => 'Luma máx.',
|
'performanceOverlay.maxLuma' => 'Luma máx.',
|
||||||
'performanceOverlay.minLuma' => 'Luma mín.',
|
'performanceOverlay.minLuma' => 'Luma mín.',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$fr extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Aucun chapitre disponible';
|
@override String get noChaptersAvailable => 'Aucun chapitre disponible';
|
||||||
@override String get queue => 'File d\'attente';
|
@override String get queue => 'File d\'attente';
|
||||||
@override String get noQueueItems => 'Aucun élément dans la 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 searchSubtitles => 'Rechercher des sous-titres';
|
||||||
@override String get language => 'Langue';
|
@override String get language => 'Langue';
|
||||||
@override String get noSubtitlesFound => 'Aucun sous-titre trouvé';
|
@override String get noSubtitlesFound => 'Aucun sous-titre trouvé';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsFr {
|
|||||||
'videoControls.noChaptersAvailable' => 'Aucun chapitre disponible',
|
'videoControls.noChaptersAvailable' => 'Aucun chapitre disponible',
|
||||||
'videoControls.queue' => 'File d\'attente',
|
'videoControls.queue' => 'File d\'attente',
|
||||||
'videoControls.noQueueItems' => 'Aucun élément dans la 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.searchSubtitles' => 'Rechercher des sous-titres',
|
||||||
'videoControls.language' => 'Langue',
|
'videoControls.language' => 'Langue',
|
||||||
'videoControls.noSubtitlesFound' => 'Aucun sous-titre trouvé',
|
'videoControls.noSubtitlesFound' => 'Aucun sous-titre trouvé',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsFr {
|
|||||||
'explore.badge.requested' => 'Demandé',
|
'explore.badge.requested' => 'Demandé',
|
||||||
'explore.badge.pendingApproval' => 'En attente d\'approbation',
|
'explore.badge.pendingApproval' => 'En attente d\'approbation',
|
||||||
'explore.badge.processing' => 'En cours de traitement',
|
'explore.badge.processing' => 'En cours de traitement',
|
||||||
'explore.badge.declined' => 'Refusé',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Refusé',
|
||||||
'explore.badge.requestFailed' => 'Échec de la demande',
|
'explore.badge.requestFailed' => 'Échec de la demande',
|
||||||
'explore.badge.requested4k' => '4K demandé',
|
'explore.badge.requested4k' => '4K demandé',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} saisons',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} saisons',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsFr {
|
|||||||
'performanceOverlay.dropped' => 'Perdues',
|
'performanceOverlay.dropped' => 'Perdues',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU',
|
'performanceOverlay.dvRpus' => 'DV RPU',
|
||||||
'performanceOverlay.dvRpuAverage' => 'Moy. DV RPU',
|
'performanceOverlay.dvRpuAverage' => 'Moy. DV RPU',
|
||||||
'performanceOverlay.dvSampleAverage' => 'Moy. échant. DV',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'Moy. échant. DV',
|
||||||
'performanceOverlay.maxLuma' => 'Luma max.',
|
'performanceOverlay.maxLuma' => 'Luma max.',
|
||||||
'performanceOverlay.minLuma' => 'Luma min.',
|
'performanceOverlay.minLuma' => 'Luma min.',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$hu extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Nincsenek elérhető fejezetek';
|
@override String get noChaptersAvailable => 'Nincsenek elérhető fejezetek';
|
||||||
@override String get queue => 'Lejátszási sor';
|
@override String get queue => 'Lejátszási sor';
|
||||||
@override String get noQueueItems => 'Nincsenek elemek a sorban';
|
@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 searchSubtitles => 'Feliratok keresése';
|
||||||
@override String get language => 'Nyelv';
|
@override String get language => 'Nyelv';
|
||||||
@override String get noSubtitlesFound => 'Nem találhatók feliratok';
|
@override String get noSubtitlesFound => 'Nem találhatók feliratok';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsHu {
|
|||||||
'videoControls.noChaptersAvailable' => 'Nincsenek elérhető fejezetek',
|
'videoControls.noChaptersAvailable' => 'Nincsenek elérhető fejezetek',
|
||||||
'videoControls.queue' => 'Lejátszási sor',
|
'videoControls.queue' => 'Lejátszási sor',
|
||||||
'videoControls.noQueueItems' => 'Nincsenek elemek a sorban',
|
'videoControls.noQueueItems' => 'Nincsenek elemek a sorban',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Nincsenek elérhető audioeszközök',
|
||||||
'videoControls.searchSubtitles' => 'Feliratok keresése',
|
'videoControls.searchSubtitles' => 'Feliratok keresése',
|
||||||
'videoControls.language' => 'Nyelv',
|
'videoControls.language' => 'Nyelv',
|
||||||
'videoControls.noSubtitlesFound' => 'Nem találhatók feliratok',
|
'videoControls.noSubtitlesFound' => 'Nem találhatók feliratok',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsHu {
|
|||||||
'explore.badge.requested' => 'Kérve',
|
'explore.badge.requested' => 'Kérve',
|
||||||
'explore.badge.pendingApproval' => 'Jóváhagyásra vár',
|
'explore.badge.pendingApproval' => 'Jóváhagyásra vár',
|
||||||
'explore.badge.processing' => 'Feldolgozás alatt',
|
'explore.badge.processing' => 'Feldolgozás alatt',
|
||||||
'explore.badge.declined' => 'Elutasítva',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Elutasítva',
|
||||||
'explore.badge.requestFailed' => 'A kérés nem sikerült',
|
'explore.badge.requestFailed' => 'A kérés nem sikerült',
|
||||||
'explore.badge.requested4k' => '4K kérve',
|
'explore.badge.requested4k' => '4K kérve',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} évad',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} évad',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsHu {
|
|||||||
'performanceOverlay.dropped' => 'Eldobva',
|
'performanceOverlay.dropped' => 'Eldobva',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU-k',
|
'performanceOverlay.dvRpus' => 'DV RPU-k',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU-átlag',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU-átlag',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV-mintaátlag',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV-mintaátlag',
|
||||||
'performanceOverlay.maxLuma' => 'Maximális luma',
|
'performanceOverlay.maxLuma' => 'Maximális luma',
|
||||||
'performanceOverlay.minLuma' => 'Minimális luma',
|
'performanceOverlay.minLuma' => 'Minimális luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$it extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Nessun capitolo disponibile';
|
@override String get noChaptersAvailable => 'Nessun capitolo disponibile';
|
||||||
@override String get queue => 'Coda';
|
@override String get queue => 'Coda';
|
||||||
@override String get noQueueItems => 'Nessun elemento in 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 searchSubtitles => 'Cerca sottotitoli';
|
||||||
@override String get language => 'Lingua';
|
@override String get language => 'Lingua';
|
||||||
@override String get noSubtitlesFound => 'Nessun sottotitolo trovato';
|
@override String get noSubtitlesFound => 'Nessun sottotitolo trovato';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsIt {
|
|||||||
'videoControls.noChaptersAvailable' => 'Nessun capitolo disponibile',
|
'videoControls.noChaptersAvailable' => 'Nessun capitolo disponibile',
|
||||||
'videoControls.queue' => 'Coda',
|
'videoControls.queue' => 'Coda',
|
||||||
'videoControls.noQueueItems' => 'Nessun elemento in coda',
|
'videoControls.noQueueItems' => 'Nessun elemento in coda',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Nessun dispositivo audio disponibile',
|
||||||
'videoControls.searchSubtitles' => 'Cerca sottotitoli',
|
'videoControls.searchSubtitles' => 'Cerca sottotitoli',
|
||||||
'videoControls.language' => 'Lingua',
|
'videoControls.language' => 'Lingua',
|
||||||
'videoControls.noSubtitlesFound' => 'Nessun sottotitolo trovato',
|
'videoControls.noSubtitlesFound' => 'Nessun sottotitolo trovato',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsIt {
|
|||||||
'explore.badge.requested' => 'Richiesto',
|
'explore.badge.requested' => 'Richiesto',
|
||||||
'explore.badge.pendingApproval' => 'In attesa di approvazione',
|
'explore.badge.pendingApproval' => 'In attesa di approvazione',
|
||||||
'explore.badge.processing' => 'In elaborazione',
|
'explore.badge.processing' => 'In elaborazione',
|
||||||
'explore.badge.declined' => 'Rifiutato',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Rifiutato',
|
||||||
'explore.badge.requestFailed' => 'Richiesta non riuscita',
|
'explore.badge.requestFailed' => 'Richiesta non riuscita',
|
||||||
'explore.badge.requested4k' => 'Richiesto in 4K',
|
'explore.badge.requested4k' => 'Richiesto in 4K',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} stagioni',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} stagioni',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsIt {
|
|||||||
'performanceOverlay.dropped' => 'Scartati',
|
'performanceOverlay.dropped' => 'Scartati',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU',
|
'performanceOverlay.dvRpus' => 'DV RPU',
|
||||||
'performanceOverlay.dvRpuAverage' => 'Media DV RPU',
|
'performanceOverlay.dvRpuAverage' => 'Media DV RPU',
|
||||||
'performanceOverlay.dvSampleAverage' => 'Media camp. DV',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'Media camp. DV',
|
||||||
'performanceOverlay.maxLuma' => 'Luma max',
|
'performanceOverlay.maxLuma' => 'Luma max',
|
||||||
'performanceOverlay.minLuma' => 'Luma min',
|
'performanceOverlay.minLuma' => 'Luma min',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -828,6 +828,7 @@ class _Translations$videoControls$ja extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'チャプターがありません';
|
@override String get noChaptersAvailable => 'チャプターがありません';
|
||||||
@override String get queue => 'キュー';
|
@override String get queue => 'キュー';
|
||||||
@override String get noQueueItems => 'キューにアイテムがありません';
|
@override String get noQueueItems => 'キューにアイテムがありません';
|
||||||
|
@override String get noAudioDevicesAvailable => 'オーディオデバイスがありません';
|
||||||
@override String get searchSubtitles => '字幕を検索';
|
@override String get searchSubtitles => '字幕を検索';
|
||||||
@override String get language => '言語';
|
@override String get language => '言語';
|
||||||
@override String get noSubtitlesFound => '字幕が見つかりません';
|
@override String get noSubtitlesFound => '字幕が見つかりません';
|
||||||
@@ -3224,6 +3225,7 @@ extension on TranslationsJa {
|
|||||||
'videoControls.noChaptersAvailable' => 'チャプターがありません',
|
'videoControls.noChaptersAvailable' => 'チャプターがありません',
|
||||||
'videoControls.queue' => 'キュー',
|
'videoControls.queue' => 'キュー',
|
||||||
'videoControls.noQueueItems' => 'キューにアイテムがありません',
|
'videoControls.noQueueItems' => 'キューにアイテムがありません',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'オーディオデバイスがありません',
|
||||||
'videoControls.searchSubtitles' => '字幕を検索',
|
'videoControls.searchSubtitles' => '字幕を検索',
|
||||||
'videoControls.language' => '言語',
|
'videoControls.language' => '言語',
|
||||||
'videoControls.noSubtitlesFound' => '字幕が見つかりません',
|
'videoControls.noSubtitlesFound' => '字幕が見つかりません',
|
||||||
@@ -3609,9 +3611,9 @@ extension on TranslationsJa {
|
|||||||
'explore.badge.requested' => 'リクエスト済み',
|
'explore.badge.requested' => 'リクエスト済み',
|
||||||
'explore.badge.pendingApproval' => '承認待ち',
|
'explore.badge.pendingApproval' => '承認待ち',
|
||||||
'explore.badge.processing' => '処理中',
|
'explore.badge.processing' => '処理中',
|
||||||
'explore.badge.declined' => '却下',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => '却下',
|
||||||
'explore.badge.requestFailed' => 'リクエスト失敗',
|
'explore.badge.requestFailed' => 'リクエスト失敗',
|
||||||
'explore.badge.requested4k' => '4Kリクエスト済み',
|
'explore.badge.requested4k' => '4Kリクエスト済み',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => 'シーズン ${available}/${total}',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => 'シーズン ${available}/${total}',
|
||||||
@@ -4123,9 +4125,9 @@ extension on TranslationsJa {
|
|||||||
'performanceOverlay.dropped' => 'ドロップ',
|
'performanceOverlay.dropped' => 'ドロップ',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU',
|
'performanceOverlay.dvRpus' => 'DV RPU',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU 平均',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU 平均',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV サンプル平均',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV サンプル平均',
|
||||||
'performanceOverlay.maxLuma' => '最大輝度',
|
'performanceOverlay.maxLuma' => '最大輝度',
|
||||||
'performanceOverlay.minLuma' => '最小輝度',
|
'performanceOverlay.minLuma' => '最小輝度',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$kk extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Бөлімдер қолжетімсіз';
|
@override String get noChaptersAvailable => 'Бөлімдер қолжетімсіз';
|
||||||
@override String get queue => 'Кезек';
|
@override String get queue => 'Кезек';
|
||||||
@override String get noQueueItems => 'Кезекте элементтер жоқ';
|
@override String get noQueueItems => 'Кезекте элементтер жоқ';
|
||||||
|
@override String get noAudioDevicesAvailable => 'Қолжетімді аудио құрылғылар жоқ';
|
||||||
@override String get searchSubtitles => 'Субтитр іздеу';
|
@override String get searchSubtitles => 'Субтитр іздеу';
|
||||||
@override String get language => 'Тіл';
|
@override String get language => 'Тіл';
|
||||||
@override String get noSubtitlesFound => 'Субтитр табылмады';
|
@override String get noSubtitlesFound => 'Субтитр табылмады';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsKk {
|
|||||||
'videoControls.noChaptersAvailable' => 'Бөлімдер қолжетімсіз',
|
'videoControls.noChaptersAvailable' => 'Бөлімдер қолжетімсіз',
|
||||||
'videoControls.queue' => 'Кезек',
|
'videoControls.queue' => 'Кезек',
|
||||||
'videoControls.noQueueItems' => 'Кезекте элементтер жоқ',
|
'videoControls.noQueueItems' => 'Кезекте элементтер жоқ',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Қолжетімді аудио құрылғылар жоқ',
|
||||||
'videoControls.searchSubtitles' => 'Субтитр іздеу',
|
'videoControls.searchSubtitles' => 'Субтитр іздеу',
|
||||||
'videoControls.language' => 'Тіл',
|
'videoControls.language' => 'Тіл',
|
||||||
'videoControls.noSubtitlesFound' => 'Субтитр табылмады',
|
'videoControls.noSubtitlesFound' => 'Субтитр табылмады',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsKk {
|
|||||||
'explore.badge.requested' => 'Сұралған',
|
'explore.badge.requested' => 'Сұралған',
|
||||||
'explore.badge.pendingApproval' => 'Растау күтілуде',
|
'explore.badge.pendingApproval' => 'Растау күтілуде',
|
||||||
'explore.badge.processing' => 'Өңделуде',
|
'explore.badge.processing' => 'Өңделуде',
|
||||||
'explore.badge.declined' => 'Қабылданбады',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Қабылданбады',
|
||||||
'explore.badge.requestFailed' => 'Сұрау сәтсіз аяқталды',
|
'explore.badge.requestFailed' => 'Сұрау сәтсіз аяқталды',
|
||||||
'explore.badge.requested4k' => '4K сұралған',
|
'explore.badge.requested4k' => '4K сұралған',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} маусым',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} маусым',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsKk {
|
|||||||
'performanceOverlay.dropped' => 'Өткізілген кадрлар',
|
'performanceOverlay.dropped' => 'Өткізілген кадрлар',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU-лар',
|
'performanceOverlay.dvRpus' => 'DV RPU-лар',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU Орт.',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU Орт.',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV Үлгі Орт.',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV Үлгі Орт.',
|
||||||
'performanceOverlay.maxLuma' => 'Макс Luma',
|
'performanceOverlay.maxLuma' => 'Макс Luma',
|
||||||
'performanceOverlay.minLuma' => 'Мин Luma',
|
'performanceOverlay.minLuma' => 'Мин Luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -828,6 +828,7 @@ class _Translations$videoControls$ko extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => '사용 가능한 챕터가 없습니다';
|
@override String get noChaptersAvailable => '사용 가능한 챕터가 없습니다';
|
||||||
@override String get queue => '재생 대기열';
|
@override String get queue => '재생 대기열';
|
||||||
@override String get noQueueItems => '대기열에 항목이 없습니다';
|
@override String get noQueueItems => '대기열에 항목이 없습니다';
|
||||||
|
@override String get noAudioDevicesAvailable => '사용 가능한 오디오 기기가 없습니다';
|
||||||
@override String get searchSubtitles => '자막 검색';
|
@override String get searchSubtitles => '자막 검색';
|
||||||
@override String get language => '언어';
|
@override String get language => '언어';
|
||||||
@override String get noSubtitlesFound => '자막을 찾을 수 없습니다';
|
@override String get noSubtitlesFound => '자막을 찾을 수 없습니다';
|
||||||
@@ -3224,6 +3225,7 @@ extension on TranslationsKo {
|
|||||||
'videoControls.noChaptersAvailable' => '사용 가능한 챕터가 없습니다',
|
'videoControls.noChaptersAvailable' => '사용 가능한 챕터가 없습니다',
|
||||||
'videoControls.queue' => '재생 대기열',
|
'videoControls.queue' => '재생 대기열',
|
||||||
'videoControls.noQueueItems' => '대기열에 항목이 없습니다',
|
'videoControls.noQueueItems' => '대기열에 항목이 없습니다',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => '사용 가능한 오디오 기기가 없습니다',
|
||||||
'videoControls.searchSubtitles' => '자막 검색',
|
'videoControls.searchSubtitles' => '자막 검색',
|
||||||
'videoControls.language' => '언어',
|
'videoControls.language' => '언어',
|
||||||
'videoControls.noSubtitlesFound' => '자막을 찾을 수 없습니다',
|
'videoControls.noSubtitlesFound' => '자막을 찾을 수 없습니다',
|
||||||
@@ -3609,9 +3611,9 @@ extension on TranslationsKo {
|
|||||||
'explore.badge.requested' => '요청됨',
|
'explore.badge.requested' => '요청됨',
|
||||||
'explore.badge.pendingApproval' => '승인 대기 중',
|
'explore.badge.pendingApproval' => '승인 대기 중',
|
||||||
'explore.badge.processing' => '처리 중',
|
'explore.badge.processing' => '처리 중',
|
||||||
'explore.badge.declined' => '거절됨',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => '거절됨',
|
||||||
'explore.badge.requestFailed' => '요청 실패',
|
'explore.badge.requestFailed' => '요청 실패',
|
||||||
'explore.badge.requested4k' => '4K 요청됨',
|
'explore.badge.requested4k' => '4K 요청됨',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '시즌 ${available}/${total}',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '시즌 ${available}/${total}',
|
||||||
@@ -4123,9 +4125,9 @@ extension on TranslationsKo {
|
|||||||
'performanceOverlay.dropped' => '드롭됨',
|
'performanceOverlay.dropped' => '드롭됨',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU',
|
'performanceOverlay.dvRpus' => 'DV RPU',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU 평균',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU 평균',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV 샘플 평균',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV 샘플 평균',
|
||||||
'performanceOverlay.maxLuma' => '최대 휘도',
|
'performanceOverlay.maxLuma' => '최대 휘도',
|
||||||
'performanceOverlay.minLuma' => '최소 휘도',
|
'performanceOverlay.minLuma' => '최소 휘도',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$nb extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Ingen kapitler tilgjengelig';
|
@override String get noChaptersAvailable => 'Ingen kapitler tilgjengelig';
|
||||||
@override String get queue => 'Kø';
|
@override String get queue => 'Kø';
|
||||||
@override String get noQueueItems => 'Ingen elementer i kø';
|
@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 searchSubtitles => 'Søk etter undertekster';
|
||||||
@override String get language => 'Språk';
|
@override String get language => 'Språk';
|
||||||
@override String get noSubtitlesFound => 'Ingen undertekster funnet';
|
@override String get noSubtitlesFound => 'Ingen undertekster funnet';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsNb {
|
|||||||
'videoControls.noChaptersAvailable' => 'Ingen kapitler tilgjengelig',
|
'videoControls.noChaptersAvailable' => 'Ingen kapitler tilgjengelig',
|
||||||
'videoControls.queue' => 'Kø',
|
'videoControls.queue' => 'Kø',
|
||||||
'videoControls.noQueueItems' => 'Ingen elementer i kø',
|
'videoControls.noQueueItems' => 'Ingen elementer i kø',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Ingen lydenheter tilgjengelig',
|
||||||
'videoControls.searchSubtitles' => 'Søk etter undertekster',
|
'videoControls.searchSubtitles' => 'Søk etter undertekster',
|
||||||
'videoControls.language' => 'Språk',
|
'videoControls.language' => 'Språk',
|
||||||
'videoControls.noSubtitlesFound' => 'Ingen undertekster funnet',
|
'videoControls.noSubtitlesFound' => 'Ingen undertekster funnet',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsNb {
|
|||||||
'explore.badge.requested' => 'Forespurt',
|
'explore.badge.requested' => 'Forespurt',
|
||||||
'explore.badge.pendingApproval' => 'Venter på godkjenning',
|
'explore.badge.pendingApproval' => 'Venter på godkjenning',
|
||||||
'explore.badge.processing' => 'Behandler',
|
'explore.badge.processing' => 'Behandler',
|
||||||
'explore.badge.declined' => 'Avslått',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Avslått',
|
||||||
'explore.badge.requestFailed' => 'Forespørsel mislyktes',
|
'explore.badge.requestFailed' => 'Forespørsel mislyktes',
|
||||||
'explore.badge.requested4k' => '4K forespurt',
|
'explore.badge.requested4k' => '4K forespurt',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} sesonger',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} sesonger',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsNb {
|
|||||||
'performanceOverlay.dropped' => 'Tapte',
|
'performanceOverlay.dropped' => 'Tapte',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU-er',
|
'performanceOverlay.dvRpus' => 'DV RPU-er',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU snitt',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU snitt',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV-sample snitt',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV-sample snitt',
|
||||||
'performanceOverlay.maxLuma' => 'Maks luma',
|
'performanceOverlay.maxLuma' => 'Maks luma',
|
||||||
'performanceOverlay.minLuma' => 'Min luma',
|
'performanceOverlay.minLuma' => 'Min luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$nl extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Geen hoofdstukken beschikbaar';
|
@override String get noChaptersAvailable => 'Geen hoofdstukken beschikbaar';
|
||||||
@override String get queue => 'Wachtrij';
|
@override String get queue => 'Wachtrij';
|
||||||
@override String get noQueueItems => 'Geen items in de 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 searchSubtitles => 'Ondertitels zoeken';
|
||||||
@override String get language => 'Taal';
|
@override String get language => 'Taal';
|
||||||
@override String get noSubtitlesFound => 'Geen ondertitels gevonden';
|
@override String get noSubtitlesFound => 'Geen ondertitels gevonden';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsNl {
|
|||||||
'videoControls.noChaptersAvailable' => 'Geen hoofdstukken beschikbaar',
|
'videoControls.noChaptersAvailable' => 'Geen hoofdstukken beschikbaar',
|
||||||
'videoControls.queue' => 'Wachtrij',
|
'videoControls.queue' => 'Wachtrij',
|
||||||
'videoControls.noQueueItems' => 'Geen items in de wachtrij',
|
'videoControls.noQueueItems' => 'Geen items in de wachtrij',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Geen audioapparaten beschikbaar',
|
||||||
'videoControls.searchSubtitles' => 'Ondertitels zoeken',
|
'videoControls.searchSubtitles' => 'Ondertitels zoeken',
|
||||||
'videoControls.language' => 'Taal',
|
'videoControls.language' => 'Taal',
|
||||||
'videoControls.noSubtitlesFound' => 'Geen ondertitels gevonden',
|
'videoControls.noSubtitlesFound' => 'Geen ondertitels gevonden',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsNl {
|
|||||||
'explore.badge.requested' => 'Aangevraagd',
|
'explore.badge.requested' => 'Aangevraagd',
|
||||||
'explore.badge.pendingApproval' => 'In afwachting van goedkeuring',
|
'explore.badge.pendingApproval' => 'In afwachting van goedkeuring',
|
||||||
'explore.badge.processing' => 'Wordt verwerkt',
|
'explore.badge.processing' => 'Wordt verwerkt',
|
||||||
'explore.badge.declined' => 'Afgewezen',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Afgewezen',
|
||||||
'explore.badge.requestFailed' => 'Aanvraag mislukt',
|
'explore.badge.requestFailed' => 'Aanvraag mislukt',
|
||||||
'explore.badge.requested4k' => '4K aangevraagd',
|
'explore.badge.requested4k' => '4K aangevraagd',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} seizoenen',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} seizoenen',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsNl {
|
|||||||
'performanceOverlay.dropped' => 'Gedropt',
|
'performanceOverlay.dropped' => 'Gedropt',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU’s',
|
'performanceOverlay.dvRpus' => 'DV RPU’s',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU gem.',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU gem.',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV-sample gem.',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV-sample gem.',
|
||||||
'performanceOverlay.maxLuma' => 'Max luma',
|
'performanceOverlay.maxLuma' => 'Max luma',
|
||||||
'performanceOverlay.minLuma' => 'Min luma',
|
'performanceOverlay.minLuma' => 'Min luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -837,6 +837,7 @@ class _Translations$videoControls$pl extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Brak dostępnych rozdziałów';
|
@override String get noChaptersAvailable => 'Brak dostępnych rozdziałów';
|
||||||
@override String get queue => 'Kolejka';
|
@override String get queue => 'Kolejka';
|
||||||
@override String get noQueueItems => 'Brak elementów w kolejce';
|
@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 searchSubtitles => 'Szukaj napisów';
|
||||||
@override String get language => 'Język';
|
@override String get language => 'Język';
|
||||||
@override String get noSubtitlesFound => 'Nie znaleziono napisów';
|
@override String get noSubtitlesFound => 'Nie znaleziono napisów';
|
||||||
@@ -3251,6 +3252,7 @@ extension on TranslationsPl {
|
|||||||
'videoControls.noChaptersAvailable' => 'Brak dostępnych rozdziałów',
|
'videoControls.noChaptersAvailable' => 'Brak dostępnych rozdziałów',
|
||||||
'videoControls.queue' => 'Kolejka',
|
'videoControls.queue' => 'Kolejka',
|
||||||
'videoControls.noQueueItems' => 'Brak elementów w kolejce',
|
'videoControls.noQueueItems' => 'Brak elementów w kolejce',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Brak dostępnych urządzeń audio',
|
||||||
'videoControls.searchSubtitles' => 'Szukaj napisów',
|
'videoControls.searchSubtitles' => 'Szukaj napisów',
|
||||||
'videoControls.language' => 'Język',
|
'videoControls.language' => 'Język',
|
||||||
'videoControls.noSubtitlesFound' => 'Nie znaleziono napisów',
|
'videoControls.noSubtitlesFound' => 'Nie znaleziono napisów',
|
||||||
@@ -3636,9 +3638,9 @@ extension on TranslationsPl {
|
|||||||
'explore.badge.requested' => 'Zamówiono',
|
'explore.badge.requested' => 'Zamówiono',
|
||||||
'explore.badge.pendingApproval' => 'Oczekuje na zatwierdzenie',
|
'explore.badge.pendingApproval' => 'Oczekuje na zatwierdzenie',
|
||||||
'explore.badge.processing' => 'Przetwarzanie',
|
'explore.badge.processing' => 'Przetwarzanie',
|
||||||
'explore.badge.declined' => 'Odrzucono',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Odrzucono',
|
||||||
'explore.badge.requestFailed' => 'Żądanie nie powiodło się',
|
'explore.badge.requestFailed' => 'Żądanie nie powiodło się',
|
||||||
'explore.badge.requested4k' => 'Zamówiono w 4K',
|
'explore.badge.requested4k' => 'Zamówiono w 4K',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} sezonów',
|
'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.dropped' => 'Pominięte',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU',
|
'performanceOverlay.dvRpus' => 'DV RPU',
|
||||||
'performanceOverlay.dvRpuAverage' => 'Śr. DV RPU',
|
'performanceOverlay.dvRpuAverage' => 'Śr. DV RPU',
|
||||||
'performanceOverlay.dvSampleAverage' => 'Śr. próbki DV',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'Śr. próbki DV',
|
||||||
'performanceOverlay.maxLuma' => 'Maks. luma',
|
'performanceOverlay.maxLuma' => 'Maks. luma',
|
||||||
'performanceOverlay.minLuma' => 'Min. luma',
|
'performanceOverlay.minLuma' => 'Min. luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$pt extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Nenhum capítulo disponível';
|
@override String get noChaptersAvailable => 'Nenhum capítulo disponível';
|
||||||
@override String get queue => 'Fila';
|
@override String get queue => 'Fila';
|
||||||
@override String get noQueueItems => 'Nenhum item na 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 searchSubtitles => 'Pesquisar legendas';
|
||||||
@override String get language => 'Idioma';
|
@override String get language => 'Idioma';
|
||||||
@override String get noSubtitlesFound => 'Nenhuma legenda encontrada';
|
@override String get noSubtitlesFound => 'Nenhuma legenda encontrada';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsPt {
|
|||||||
'videoControls.noChaptersAvailable' => 'Nenhum capítulo disponível',
|
'videoControls.noChaptersAvailable' => 'Nenhum capítulo disponível',
|
||||||
'videoControls.queue' => 'Fila',
|
'videoControls.queue' => 'Fila',
|
||||||
'videoControls.noQueueItems' => 'Nenhum item na fila',
|
'videoControls.noQueueItems' => 'Nenhum item na fila',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Nenhum dispositivo de áudio disponível',
|
||||||
'videoControls.searchSubtitles' => 'Pesquisar legendas',
|
'videoControls.searchSubtitles' => 'Pesquisar legendas',
|
||||||
'videoControls.language' => 'Idioma',
|
'videoControls.language' => 'Idioma',
|
||||||
'videoControls.noSubtitlesFound' => 'Nenhuma legenda encontrada',
|
'videoControls.noSubtitlesFound' => 'Nenhuma legenda encontrada',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsPt {
|
|||||||
'explore.badge.requested' => 'Solicitado',
|
'explore.badge.requested' => 'Solicitado',
|
||||||
'explore.badge.pendingApproval' => 'Aguardando aprovação',
|
'explore.badge.pendingApproval' => 'Aguardando aprovação',
|
||||||
'explore.badge.processing' => 'Processando',
|
'explore.badge.processing' => 'Processando',
|
||||||
'explore.badge.declined' => 'Recusado',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Recusado',
|
||||||
'explore.badge.requestFailed' => 'Falha na solicitação',
|
'explore.badge.requestFailed' => 'Falha na solicitação',
|
||||||
'explore.badge.requested4k' => '4K solicitado',
|
'explore.badge.requested4k' => '4K solicitado',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} temporadas',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} temporadas',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsPt {
|
|||||||
'performanceOverlay.dropped' => 'Descartados',
|
'performanceOverlay.dropped' => 'Descartados',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPUs',
|
'performanceOverlay.dvRpus' => 'DV RPUs',
|
||||||
'performanceOverlay.dvRpuAverage' => 'Média DV RPU',
|
'performanceOverlay.dvRpuAverage' => 'Média DV RPU',
|
||||||
'performanceOverlay.dvSampleAverage' => 'Média amostra DV',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'Média amostra DV',
|
||||||
'performanceOverlay.maxLuma' => 'Luma máx.',
|
'performanceOverlay.maxLuma' => 'Luma máx.',
|
||||||
'performanceOverlay.minLuma' => 'Luma mín.',
|
'performanceOverlay.minLuma' => 'Luma mín.',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -837,6 +837,7 @@ class _Translations$videoControls$ru extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Главы недоступны';
|
@override String get noChaptersAvailable => 'Главы недоступны';
|
||||||
@override String get queue => 'Очередь';
|
@override String get queue => 'Очередь';
|
||||||
@override String get noQueueItems => 'В очереди нет элементов';
|
@override String get noQueueItems => 'В очереди нет элементов';
|
||||||
|
@override String get noAudioDevicesAvailable => 'Нет доступных аудиоустройств';
|
||||||
@override String get searchSubtitles => 'Поиск субтитров';
|
@override String get searchSubtitles => 'Поиск субтитров';
|
||||||
@override String get language => 'Язык';
|
@override String get language => 'Язык';
|
||||||
@override String get noSubtitlesFound => 'Субтитры не найдены';
|
@override String get noSubtitlesFound => 'Субтитры не найдены';
|
||||||
@@ -3251,6 +3252,7 @@ extension on TranslationsRu {
|
|||||||
'videoControls.noChaptersAvailable' => 'Главы недоступны',
|
'videoControls.noChaptersAvailable' => 'Главы недоступны',
|
||||||
'videoControls.queue' => 'Очередь',
|
'videoControls.queue' => 'Очередь',
|
||||||
'videoControls.noQueueItems' => 'В очереди нет элементов',
|
'videoControls.noQueueItems' => 'В очереди нет элементов',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Нет доступных аудиоустройств',
|
||||||
'videoControls.searchSubtitles' => 'Поиск субтитров',
|
'videoControls.searchSubtitles' => 'Поиск субтитров',
|
||||||
'videoControls.language' => 'Язык',
|
'videoControls.language' => 'Язык',
|
||||||
'videoControls.noSubtitlesFound' => 'Субтитры не найдены',
|
'videoControls.noSubtitlesFound' => 'Субтитры не найдены',
|
||||||
@@ -3636,9 +3638,9 @@ extension on TranslationsRu {
|
|||||||
'explore.badge.requested' => 'Запрошено',
|
'explore.badge.requested' => 'Запрошено',
|
||||||
'explore.badge.pendingApproval' => 'Ожидает одобрения',
|
'explore.badge.pendingApproval' => 'Ожидает одобрения',
|
||||||
'explore.badge.processing' => 'В обработке',
|
'explore.badge.processing' => 'В обработке',
|
||||||
'explore.badge.declined' => 'Отклонено',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Отклонено',
|
||||||
'explore.badge.requestFailed' => 'Запрос не удался',
|
'explore.badge.requestFailed' => 'Запрос не удался',
|
||||||
'explore.badge.requested4k' => 'Запрошено в 4K',
|
'explore.badge.requested4k' => 'Запрошено в 4K',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} сезонов',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} сезонов',
|
||||||
@@ -4150,9 +4152,9 @@ extension on TranslationsRu {
|
|||||||
'performanceOverlay.dropped' => 'Пропущено',
|
'performanceOverlay.dropped' => 'Пропущено',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU',
|
'performanceOverlay.dvRpus' => 'DV RPU',
|
||||||
'performanceOverlay.dvRpuAverage' => 'Сред. DV RPU',
|
'performanceOverlay.dvRpuAverage' => 'Сред. DV RPU',
|
||||||
'performanceOverlay.dvSampleAverage' => 'Сред. сэмпл DV',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'Сред. сэмпл DV',
|
||||||
'performanceOverlay.maxLuma' => 'Макс. яркость',
|
'performanceOverlay.maxLuma' => 'Макс. яркость',
|
||||||
'performanceOverlay.minLuma' => 'Мин. яркость',
|
'performanceOverlay.minLuma' => 'Мин. яркость',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$sv extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Inga kapitel tillgängliga';
|
@override String get noChaptersAvailable => 'Inga kapitel tillgängliga';
|
||||||
@override String get queue => 'Kö';
|
@override String get queue => 'Kö';
|
||||||
@override String get noQueueItems => 'Inga objekt i kön';
|
@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 searchSubtitles => 'Sök undertexter';
|
||||||
@override String get language => 'Språk';
|
@override String get language => 'Språk';
|
||||||
@override String get noSubtitlesFound => 'Inga undertexter hittades';
|
@override String get noSubtitlesFound => 'Inga undertexter hittades';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsSv {
|
|||||||
'videoControls.noChaptersAvailable' => 'Inga kapitel tillgängliga',
|
'videoControls.noChaptersAvailable' => 'Inga kapitel tillgängliga',
|
||||||
'videoControls.queue' => 'Kö',
|
'videoControls.queue' => 'Kö',
|
||||||
'videoControls.noQueueItems' => 'Inga objekt i kön',
|
'videoControls.noQueueItems' => 'Inga objekt i kön',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Inga ljudenheter tillgängliga',
|
||||||
'videoControls.searchSubtitles' => 'Sök undertexter',
|
'videoControls.searchSubtitles' => 'Sök undertexter',
|
||||||
'videoControls.language' => 'Språk',
|
'videoControls.language' => 'Språk',
|
||||||
'videoControls.noSubtitlesFound' => 'Inga undertexter hittades',
|
'videoControls.noSubtitlesFound' => 'Inga undertexter hittades',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsSv {
|
|||||||
'explore.badge.requested' => 'Begärd',
|
'explore.badge.requested' => 'Begärd',
|
||||||
'explore.badge.pendingApproval' => 'Väntar på godkännande',
|
'explore.badge.pendingApproval' => 'Väntar på godkännande',
|
||||||
'explore.badge.processing' => 'Bearbetas',
|
'explore.badge.processing' => 'Bearbetas',
|
||||||
'explore.badge.declined' => 'Avvisad',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Avvisad',
|
||||||
'explore.badge.requestFailed' => 'Begäran misslyckades',
|
'explore.badge.requestFailed' => 'Begäran misslyckades',
|
||||||
'explore.badge.requested4k' => '4K begärd',
|
'explore.badge.requested4k' => '4K begärd',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} säsonger',
|
'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.dropped' => 'Tappade bildrutor',
|
||||||
'performanceOverlay.dvRpus' => 'DV-RPU:er',
|
'performanceOverlay.dvRpus' => 'DV-RPU:er',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV-RPU, genomsnitt',
|
'performanceOverlay.dvRpuAverage' => 'DV-RPU, genomsnitt',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV-sampling, genomsnitt',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV-sampling, genomsnitt',
|
||||||
'performanceOverlay.maxLuma' => 'Max luma',
|
'performanceOverlay.maxLuma' => 'Max luma',
|
||||||
'performanceOverlay.minLuma' => 'Min luma',
|
'performanceOverlay.minLuma' => 'Min luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$tr extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Kısım bulunmuyor';
|
@override String get noChaptersAvailable => 'Kısım bulunmuyor';
|
||||||
@override String get queue => 'Kuyruk';
|
@override String get queue => 'Kuyruk';
|
||||||
@override String get noQueueItems => 'Kuyrukta öge yok';
|
@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 searchSubtitles => 'Altyazı Ara';
|
||||||
@override String get language => 'Dil';
|
@override String get language => 'Dil';
|
||||||
@override String get noSubtitlesFound => 'Altyazı bulunamadı';
|
@override String get noSubtitlesFound => 'Altyazı bulunamadı';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsTr {
|
|||||||
'videoControls.noChaptersAvailable' => 'Kısım bulunmuyor',
|
'videoControls.noChaptersAvailable' => 'Kısım bulunmuyor',
|
||||||
'videoControls.queue' => 'Kuyruk',
|
'videoControls.queue' => 'Kuyruk',
|
||||||
'videoControls.noQueueItems' => 'Kuyrukta öge yok',
|
'videoControls.noQueueItems' => 'Kuyrukta öge yok',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Kullanılabilir ses cihazı yok',
|
||||||
'videoControls.searchSubtitles' => 'Altyazı Ara',
|
'videoControls.searchSubtitles' => 'Altyazı Ara',
|
||||||
'videoControls.language' => 'Dil',
|
'videoControls.language' => 'Dil',
|
||||||
'videoControls.noSubtitlesFound' => 'Altyazı bulunamadı',
|
'videoControls.noSubtitlesFound' => 'Altyazı bulunamadı',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsTr {
|
|||||||
'explore.badge.requested' => 'İstendi',
|
'explore.badge.requested' => 'İstendi',
|
||||||
'explore.badge.pendingApproval' => 'Onay bekliyor',
|
'explore.badge.pendingApproval' => 'Onay bekliyor',
|
||||||
'explore.badge.processing' => 'İşleniyor',
|
'explore.badge.processing' => 'İşleniyor',
|
||||||
'explore.badge.declined' => 'Reddedildi',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Reddedildi',
|
||||||
'explore.badge.requestFailed' => 'İstek başarısız oldu',
|
'explore.badge.requestFailed' => 'İstek başarısız oldu',
|
||||||
'explore.badge.requested4k' => '4K istendi',
|
'explore.badge.requested4k' => '4K istendi',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} sezon',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} sezon',
|
||||||
@@ -4132,9 +4134,9 @@ extension on TranslationsTr {
|
|||||||
'performanceOverlay.dropped' => 'Kare Kaybı',
|
'performanceOverlay.dropped' => 'Kare Kaybı',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU\'ları',
|
'performanceOverlay.dvRpus' => 'DV RPU\'ları',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU Ort.',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU Ort.',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV Örnek Ort.',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV Örnek Ort.',
|
||||||
'performanceOverlay.maxLuma' => 'Maks Luma',
|
'performanceOverlay.maxLuma' => 'Maks Luma',
|
||||||
'performanceOverlay.minLuma' => 'Min Luma',
|
'performanceOverlay.minLuma' => 'Min Luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -831,6 +831,7 @@ class _Translations$videoControls$uz extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => 'Boʻlimlar mavjud emas';
|
@override String get noChaptersAvailable => 'Boʻlimlar mavjud emas';
|
||||||
@override String get queue => 'Navbat';
|
@override String get queue => 'Navbat';
|
||||||
@override String get noQueueItems => 'Navbatda elementlar yoʻq';
|
@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 searchSubtitles => 'Subtitr qidirish';
|
||||||
@override String get language => 'Til';
|
@override String get language => 'Til';
|
||||||
@override String get noSubtitlesFound => 'Subtitr topilmadi';
|
@override String get noSubtitlesFound => 'Subtitr topilmadi';
|
||||||
@@ -3233,6 +3234,7 @@ extension on TranslationsUz {
|
|||||||
'videoControls.noChaptersAvailable' => 'Boʻlimlar mavjud emas',
|
'videoControls.noChaptersAvailable' => 'Boʻlimlar mavjud emas',
|
||||||
'videoControls.queue' => 'Navbat',
|
'videoControls.queue' => 'Navbat',
|
||||||
'videoControls.noQueueItems' => 'Navbatda elementlar yoʻq',
|
'videoControls.noQueueItems' => 'Navbatda elementlar yoʻq',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => 'Mavjud audio qurilmalar yoʻq',
|
||||||
'videoControls.searchSubtitles' => 'Subtitr qidirish',
|
'videoControls.searchSubtitles' => 'Subtitr qidirish',
|
||||||
'videoControls.language' => 'Til',
|
'videoControls.language' => 'Til',
|
||||||
'videoControls.noSubtitlesFound' => 'Subtitr topilmadi',
|
'videoControls.noSubtitlesFound' => 'Subtitr topilmadi',
|
||||||
@@ -3618,9 +3620,9 @@ extension on TranslationsUz {
|
|||||||
'explore.badge.requested' => 'Soʻralgan',
|
'explore.badge.requested' => 'Soʻralgan',
|
||||||
'explore.badge.pendingApproval' => 'Tasdiq kutilmoqda',
|
'explore.badge.pendingApproval' => 'Tasdiq kutilmoqda',
|
||||||
'explore.badge.processing' => 'Ishlanmoqda',
|
'explore.badge.processing' => 'Ishlanmoqda',
|
||||||
'explore.badge.declined' => 'Rad etilgan',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => 'Rad etilgan',
|
||||||
'explore.badge.requestFailed' => 'Soʻrov amalga oshmadi',
|
'explore.badge.requestFailed' => 'Soʻrov amalga oshmadi',
|
||||||
'explore.badge.requested4k' => '4K soʻralgan',
|
'explore.badge.requested4k' => '4K soʻralgan',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} mavsum',
|
'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.dropped' => 'Tushirib qoldirilgan kadrlar',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU-lar',
|
'performanceOverlay.dvRpus' => 'DV RPU-lar',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU Oʻrt.',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU Oʻrt.',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV Namuna Oʻrt.',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV Namuna Oʻrt.',
|
||||||
'performanceOverlay.maxLuma' => 'Maks Luma',
|
'performanceOverlay.maxLuma' => 'Maks Luma',
|
||||||
'performanceOverlay.minLuma' => 'Min Luma',
|
'performanceOverlay.minLuma' => 'Min Luma',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -828,6 +828,7 @@ class Translations$videoControls$zh extends Translations$videoControls$en {
|
|||||||
@override String get noChaptersAvailable => '没有可用的章节';
|
@override String get noChaptersAvailable => '没有可用的章节';
|
||||||
@override String get queue => '播放队列';
|
@override String get queue => '播放队列';
|
||||||
@override String get noQueueItems => '队列中没有项目';
|
@override String get noQueueItems => '队列中没有项目';
|
||||||
|
@override String get noAudioDevicesAvailable => '没有可用的音频设备';
|
||||||
@override String get searchSubtitles => '搜索字幕';
|
@override String get searchSubtitles => '搜索字幕';
|
||||||
@override String get language => '语言';
|
@override String get language => '语言';
|
||||||
@override String get noSubtitlesFound => '未找到字幕';
|
@override String get noSubtitlesFound => '未找到字幕';
|
||||||
@@ -3224,6 +3225,7 @@ extension on TranslationsZh {
|
|||||||
'videoControls.noChaptersAvailable' => '没有可用的章节',
|
'videoControls.noChaptersAvailable' => '没有可用的章节',
|
||||||
'videoControls.queue' => '播放队列',
|
'videoControls.queue' => '播放队列',
|
||||||
'videoControls.noQueueItems' => '队列中没有项目',
|
'videoControls.noQueueItems' => '队列中没有项目',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => '没有可用的音频设备',
|
||||||
'videoControls.searchSubtitles' => '搜索字幕',
|
'videoControls.searchSubtitles' => '搜索字幕',
|
||||||
'videoControls.language' => '语言',
|
'videoControls.language' => '语言',
|
||||||
'videoControls.noSubtitlesFound' => '未找到字幕',
|
'videoControls.noSubtitlesFound' => '未找到字幕',
|
||||||
@@ -3609,9 +3611,9 @@ extension on TranslationsZh {
|
|||||||
'explore.badge.requested' => '已请求',
|
'explore.badge.requested' => '已请求',
|
||||||
'explore.badge.pendingApproval' => '待批准',
|
'explore.badge.pendingApproval' => '待批准',
|
||||||
'explore.badge.processing' => '处理中',
|
'explore.badge.processing' => '处理中',
|
||||||
'explore.badge.declined' => '已拒绝',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => '已拒绝',
|
||||||
'explore.badge.requestFailed' => '请求失败',
|
'explore.badge.requestFailed' => '请求失败',
|
||||||
'explore.badge.requested4k' => '已请求 4K',
|
'explore.badge.requested4k' => '已请求 4K',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '已有 ${available}/${total} 季',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '已有 ${available}/${total} 季',
|
||||||
@@ -4123,9 +4125,9 @@ extension on TranslationsZh {
|
|||||||
'performanceOverlay.dropped' => '丢帧',
|
'performanceOverlay.dropped' => '丢帧',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU',
|
'performanceOverlay.dvRpus' => 'DV RPU',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU 平均',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU 平均',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV 采样平均',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV 采样平均',
|
||||||
'performanceOverlay.maxLuma' => '最大亮度',
|
'performanceOverlay.maxLuma' => '最大亮度',
|
||||||
'performanceOverlay.minLuma' => '最小亮度',
|
'performanceOverlay.minLuma' => '最小亮度',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -829,6 +829,7 @@ class _Translations$videoControls$zh_Hant extends Translations$videoControls$zh
|
|||||||
@override String get noChaptersAvailable => '沒有可用的章節';
|
@override String get noChaptersAvailable => '沒有可用的章節';
|
||||||
@override String get queue => '播放佇列';
|
@override String get queue => '播放佇列';
|
||||||
@override String get noQueueItems => '佇列中沒有項目';
|
@override String get noQueueItems => '佇列中沒有項目';
|
||||||
|
@override String get noAudioDevicesAvailable => '沒有可用的音訊裝置';
|
||||||
@override String get searchSubtitles => '搜尋字幕';
|
@override String get searchSubtitles => '搜尋字幕';
|
||||||
@override String get language => '語言';
|
@override String get language => '語言';
|
||||||
@override String get noSubtitlesFound => '找不到字幕';
|
@override String get noSubtitlesFound => '找不到字幕';
|
||||||
@@ -3225,6 +3226,7 @@ extension on TranslationsZhHant {
|
|||||||
'videoControls.noChaptersAvailable' => '沒有可用的章節',
|
'videoControls.noChaptersAvailable' => '沒有可用的章節',
|
||||||
'videoControls.queue' => '播放佇列',
|
'videoControls.queue' => '播放佇列',
|
||||||
'videoControls.noQueueItems' => '佇列中沒有項目',
|
'videoControls.noQueueItems' => '佇列中沒有項目',
|
||||||
|
'videoControls.noAudioDevicesAvailable' => '沒有可用的音訊裝置',
|
||||||
'videoControls.searchSubtitles' => '搜尋字幕',
|
'videoControls.searchSubtitles' => '搜尋字幕',
|
||||||
'videoControls.language' => '語言',
|
'videoControls.language' => '語言',
|
||||||
'videoControls.noSubtitlesFound' => '找不到字幕',
|
'videoControls.noSubtitlesFound' => '找不到字幕',
|
||||||
@@ -3610,9 +3612,9 @@ extension on TranslationsZhHant {
|
|||||||
'explore.badge.requested' => '已提出請求',
|
'explore.badge.requested' => '已提出請求',
|
||||||
'explore.badge.pendingApproval' => '等待核准',
|
'explore.badge.pendingApproval' => '等待核准',
|
||||||
'explore.badge.processing' => '處理中',
|
'explore.badge.processing' => '處理中',
|
||||||
'explore.badge.declined' => '已拒絕',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'explore.badge.declined' => '已拒絕',
|
||||||
'explore.badge.requestFailed' => '請求失敗',
|
'explore.badge.requestFailed' => '請求失敗',
|
||||||
'explore.badge.requested4k' => '已請求 4K',
|
'explore.badge.requested4k' => '已請求 4K',
|
||||||
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} 季',
|
'explore.badge.seasonsAvailable' => ({required Object available, required Object total}) => '${available}/${total} 季',
|
||||||
@@ -4124,9 +4126,9 @@ extension on TranslationsZhHant {
|
|||||||
'performanceOverlay.dropped' => '丟格數(Dropped)',
|
'performanceOverlay.dropped' => '丟格數(Dropped)',
|
||||||
'performanceOverlay.dvRpus' => 'DV RPU 數',
|
'performanceOverlay.dvRpus' => 'DV RPU 數',
|
||||||
'performanceOverlay.dvRpuAverage' => 'DV RPU 平均',
|
'performanceOverlay.dvRpuAverage' => 'DV RPU 平均',
|
||||||
'performanceOverlay.dvSampleAverage' => 'DV 取樣平均',
|
|
||||||
_ => null,
|
_ => null,
|
||||||
} ?? switch (path) {
|
} ?? switch (path) {
|
||||||
|
'performanceOverlay.dvSampleAverage' => 'DV 取樣平均',
|
||||||
'performanceOverlay.maxLuma' => '最大亮度',
|
'performanceOverlay.maxLuma' => '最大亮度',
|
||||||
'performanceOverlay.minLuma' => '最小亮度',
|
'performanceOverlay.minLuma' => '最小亮度',
|
||||||
'performanceOverlay.maxCll' => 'MaxCLL',
|
'performanceOverlay.maxCll' => 'MaxCLL',
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Inga kapitel tillgängliga",
|
"noChaptersAvailable": "Inga kapitel tillgängliga",
|
||||||
"queue": "Kö",
|
"queue": "Kö",
|
||||||
"noQueueItems": "Inga objekt i kön",
|
"noQueueItems": "Inga objekt i kön",
|
||||||
|
"noAudioDevicesAvailable": "Inga ljudenheter tillgängliga",
|
||||||
"searchSubtitles": "Sök undertexter",
|
"searchSubtitles": "Sök undertexter",
|
||||||
"language": "Språk",
|
"language": "Språk",
|
||||||
"noSubtitlesFound": "Inga undertexter hittades",
|
"noSubtitlesFound": "Inga undertexter hittades",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Kısım bulunmuyor",
|
"noChaptersAvailable": "Kısım bulunmuyor",
|
||||||
"queue": "Kuyruk",
|
"queue": "Kuyruk",
|
||||||
"noQueueItems": "Kuyrukta öge yok",
|
"noQueueItems": "Kuyrukta öge yok",
|
||||||
|
"noAudioDevicesAvailable": "Kullanılabilir ses cihazı yok",
|
||||||
"searchSubtitles": "Altyazı Ara",
|
"searchSubtitles": "Altyazı Ara",
|
||||||
"language": "Dil",
|
"language": "Dil",
|
||||||
"noSubtitlesFound": "Altyazı bulunamadı",
|
"noSubtitlesFound": "Altyazı bulunamadı",
|
||||||
|
|||||||
@@ -681,6 +681,7 @@
|
|||||||
"noChaptersAvailable": "Boʻlimlar mavjud emas",
|
"noChaptersAvailable": "Boʻlimlar mavjud emas",
|
||||||
"queue": "Navbat",
|
"queue": "Navbat",
|
||||||
"noQueueItems": "Navbatda elementlar yoʻq",
|
"noQueueItems": "Navbatda elementlar yoʻq",
|
||||||
|
"noAudioDevicesAvailable": "Mavjud audio qurilmalar yoʻq",
|
||||||
"searchSubtitles": "Subtitr qidirish",
|
"searchSubtitles": "Subtitr qidirish",
|
||||||
"language": "Til",
|
"language": "Til",
|
||||||
"noSubtitlesFound": "Subtitr topilmadi",
|
"noSubtitlesFound": "Subtitr topilmadi",
|
||||||
|
|||||||
@@ -678,6 +678,7 @@
|
|||||||
"noChaptersAvailable": "沒有可用的章節",
|
"noChaptersAvailable": "沒有可用的章節",
|
||||||
"queue": "播放佇列",
|
"queue": "播放佇列",
|
||||||
"noQueueItems": "佇列中沒有項目",
|
"noQueueItems": "佇列中沒有項目",
|
||||||
|
"noAudioDevicesAvailable": "沒有可用的音訊裝置",
|
||||||
"searchSubtitles": "搜尋字幕",
|
"searchSubtitles": "搜尋字幕",
|
||||||
"language": "語言",
|
"language": "語言",
|
||||||
"noSubtitlesFound": "找不到字幕",
|
"noSubtitlesFound": "找不到字幕",
|
||||||
|
|||||||
@@ -678,6 +678,7 @@
|
|||||||
"noChaptersAvailable": "没有可用的章节",
|
"noChaptersAvailable": "没有可用的章节",
|
||||||
"queue": "播放队列",
|
"queue": "播放队列",
|
||||||
"noQueueItems": "队列中没有项目",
|
"noQueueItems": "队列中没有项目",
|
||||||
|
"noAudioDevicesAvailable": "没有可用的音频设备",
|
||||||
"searchSubtitles": "搜索字幕",
|
"searchSubtitles": "搜索字幕",
|
||||||
"language": "语言",
|
"language": "语言",
|
||||||
"noSubtitlesFound": "未找到字幕",
|
"noSubtitlesFound": "未找到字幕",
|
||||||
|
|||||||
@@ -51,6 +51,8 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
|
|||||||
bool _isLoadingValues = false;
|
bool _isLoadingValues = false;
|
||||||
String? _filterValuesError;
|
String? _filterValuesError;
|
||||||
int _filterValuesLoadGeneration = 0;
|
int _filterValuesLoadGeneration = 0;
|
||||||
|
final _contentKey = GlobalKey();
|
||||||
|
double? _transitionMinHeight;
|
||||||
final Map<String, String> _tempSelectedFilters = {};
|
final Map<String, String> _tempSelectedFilters = {};
|
||||||
static final Map<String, String> _filterDisplayNames = {}; // Cache for display names
|
static final Map<String, String> _filterDisplayNames = {}; // Cache for display names
|
||||||
static const int _maxCachedDisplayNames = 1000;
|
static const int _maxCachedDisplayNames = 1000;
|
||||||
@@ -116,6 +118,11 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
|
|||||||
final libraryKey = widget.libraryKey;
|
final libraryKey = widget.libraryKey;
|
||||||
final cachedValues = widget.cachedValues;
|
final cachedValues = widget.cachedValues;
|
||||||
final loader = widget.loadFilterValues;
|
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(() {
|
setState(() {
|
||||||
_currentFilter = filter;
|
_currentFilter = filter;
|
||||||
_filterValues = [];
|
_filterValues = [];
|
||||||
@@ -235,26 +242,52 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
|
child: KeyedSubtree(
|
||||||
|
key: _contentKey,
|
||||||
child: currentFilter != null ? _buildFilterValuesView(currentFilter) : _buildFiltersView(),
|
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) {
|
Widget _buildFilterValuesView(MediaFilter filter) {
|
||||||
final error = _filterValuesError;
|
final error = _filterValuesError;
|
||||||
if (error != null) {
|
if (error != null) {
|
||||||
return ErrorStateWidget(
|
// 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,
|
message: error,
|
||||||
onRetry: () => _loadFilterValues(filter),
|
onRetry: () => _loadFilterValues(filter),
|
||||||
actionFocusNode: _initialFocusNode,
|
actionFocusNode: _initialFocusNode,
|
||||||
onActionBack: _goBack,
|
onActionBack: _goBack,
|
||||||
actionAutofocus: InputModeTracker.isKeyboardMode(context),
|
actionAutofocus: InputModeTracker.isKeyboardMode(context),
|
||||||
actionUseBackgroundFocus: true,
|
actionUseBackgroundFocus: true,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (_isLoadingValues) {
|
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(
|
return Focus(
|
||||||
autofocus: InputModeTracker.isKeyboardMode(context),
|
autofocus: InputModeTracker.isKeyboardMode(context),
|
||||||
|
// 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()),
|
child: const Center(child: CircularProgressIndicator()),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,6 +295,7 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
|
|||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
controller: _valuesScrollController,
|
controller: _valuesScrollController,
|
||||||
primary: false,
|
primary: false,
|
||||||
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
itemCount: _filterValues.length + 1,
|
itemCount: _filterValues.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
@@ -309,6 +343,7 @@ class _FiltersBottomSheetState extends State<FiltersBottomSheet> {
|
|||||||
final autofocusFirst = InputModeTracker.isKeyboardMode(context);
|
final autofocusFirst = InputModeTracker.isKeyboardMode(context);
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
primary: false,
|
primary: false,
|
||||||
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
itemCount: _sortedFilters.length,
|
itemCount: _sortedFilters.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
|||||||
@@ -836,7 +836,6 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
|||||||
return BottomSheetPageScaffold(
|
return BottomSheetPageScaffold(
|
||||||
title: t.libraries.libraryOptions,
|
title: t.libraries.libraryOptions,
|
||||||
icon: Symbols.tune_rounded,
|
icon: Symbols.tune_rounded,
|
||||||
shrinkWrap: true,
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
primary: false,
|
primary: false,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
@@ -917,7 +916,6 @@ class _LibraryBrowseTabState extends BaseLibraryTabState<MediaItem, LibraryBrows
|
|||||||
title: t.libraries.groupings.title,
|
title: t.libraries.groupings.title,
|
||||||
icon: Symbols.category_rounded,
|
icon: Symbols.category_rounded,
|
||||||
onBack: onBack,
|
onBack: onBack,
|
||||||
shrinkWrap: true,
|
|
||||||
child: ListView(
|
child: ListView(
|
||||||
primary: false,
|
primary: false,
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
|
|||||||
@@ -315,6 +315,13 @@ class _RecordOptionsContentState extends State<_RecordOptionsContent> {
|
|||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 24),
|
padding: const EdgeInsets.symmetric(vertical: 24),
|
||||||
child: Center(
|
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(
|
child: Text(
|
||||||
_entry.airingsType ?? '',
|
_entry.airingsType ?? '',
|
||||||
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
style: theme.textTheme.bodySmall?.copyWith(color: theme.colorScheme.onSurfaceVariant),
|
||||||
|
|||||||
@@ -104,13 +104,14 @@ class _ReorderFavoritesSheetState extends State<ReorderFavoritesSheet>
|
|||||||
mainAxisSize: .min,
|
mainAxisSize: .min,
|
||||||
children: [
|
children: [
|
||||||
BottomSheetHeader(title: t.liveTv.reorderFavorites, icon: Symbols.swap_vert_rounded),
|
BottomSheetHeader(title: t.liveTv.reorderFavorites, icon: Symbols.swap_vert_rounded),
|
||||||
Expanded(
|
Flexible(
|
||||||
child: Focus(
|
child: Focus(
|
||||||
focusNode: _listFocusNode,
|
focusNode: _listFocusNode,
|
||||||
descendantsAreFocusable: false,
|
descendantsAreFocusable: false,
|
||||||
autofocus: isKeyboardMode,
|
autofocus: isKeyboardMode,
|
||||||
onKeyEvent: handleReorderKeyEvent,
|
onKeyEvent: handleReorderKeyEvent,
|
||||||
child: ReorderableListView.builder(
|
child: ReorderableListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
scrollController: _scrollController,
|
scrollController: _scrollController,
|
||||||
onReorderItem: _onReorder,
|
onReorderItem: _onReorder,
|
||||||
itemCount: _tempFavorites.length,
|
itemCount: _tempFavorites.length,
|
||||||
|
|||||||
@@ -1561,12 +1561,10 @@ class _MainScreenState extends State<MainScreen>
|
|||||||
|
|
||||||
final controller = OverlaySheetController.of(context);
|
final controller = OverlaySheetController.of(context);
|
||||||
final groupByServer = SettingsService.instanceOrNull?.read(SettingsService.groupLibrariesByServer) ?? false;
|
final groupByServer = SettingsService.instanceOrNull?.read(SettingsService.groupLibrariesByServer) ?? false;
|
||||||
final maxHeight = MediaQuery.sizeOf(context).height * 0.62;
|
|
||||||
|
|
||||||
controller
|
controller
|
||||||
.show<String>(
|
.show<String>(
|
||||||
showDragHandle: true,
|
showDragHandle: true,
|
||||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
|
||||||
builder: (sheetContext) {
|
builder: (sheetContext) {
|
||||||
return Consumer2<LibrariesProvider, HiddenLibrariesProvider>(
|
return Consumer2<LibrariesProvider, HiddenLibrariesProvider>(
|
||||||
builder: (context, librariesProvider, hiddenLibrariesProvider, _) {
|
builder: (context, librariesProvider, hiddenLibrariesProvider, _) {
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ class _SessionMenuSheet extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
|
|||||||
@@ -5,6 +5,19 @@ import '../focus/key_event_utils.dart';
|
|||||||
import 'bottom_sheet_header.dart';
|
import 'bottom_sheet_header.dart';
|
||||||
|
|
||||||
/// Shared page layout for bottom sheets with a stable header and content area.
|
/// 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 {
|
class BottomSheetPageScaffold extends StatelessWidget {
|
||||||
final String title;
|
final String title;
|
||||||
final Widget child;
|
final Widget child;
|
||||||
@@ -19,7 +32,6 @@ class BottomSheetPageScaffold extends StatelessWidget {
|
|||||||
final bool showHeaderBorder;
|
final bool showHeaderBorder;
|
||||||
final bool showHeaderDivider;
|
final bool showHeaderDivider;
|
||||||
final FocusNode? closeFocusNode;
|
final FocusNode? closeFocusNode;
|
||||||
final bool shrinkWrap;
|
|
||||||
|
|
||||||
const BottomSheetPageScaffold({
|
const BottomSheetPageScaffold({
|
||||||
super.key,
|
super.key,
|
||||||
@@ -36,13 +48,12 @@ class BottomSheetPageScaffold extends StatelessWidget {
|
|||||||
this.showHeaderBorder = true,
|
this.showHeaderBorder = true,
|
||||||
this.showHeaderDivider = false,
|
this.showHeaderDivider = false,
|
||||||
this.closeFocusNode,
|
this.closeFocusNode,
|
||||||
this.shrinkWrap = false,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
Widget content = Column(
|
Widget content = Column(
|
||||||
mainAxisSize: shrinkWrap ? MainAxisSize.min : MainAxisSize.max,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
BottomSheetHeader(
|
BottomSheetHeader(
|
||||||
title: title,
|
title: title,
|
||||||
@@ -58,7 +69,7 @@ class BottomSheetPageScaffold extends StatelessWidget {
|
|||||||
closeFocusNode: closeFocusNode,
|
closeFocusNode: closeFocusNode,
|
||||||
),
|
),
|
||||||
if (showHeaderDivider) Divider(color: Theme.of(context).dividerColor, height: 1),
|
if (showHeaderDivider) Divider(color: Theme.of(context).dividerColor, height: 1),
|
||||||
if (shrinkWrap) child else Expanded(child: child),
|
Flexible(child: child),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ class _FileInfoBottomSheetState extends State<FileInfoBottomSheet> {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final versions = widget.fileInfo.versions;
|
final versions = widget.fileInfo.versions;
|
||||||
return Column(
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
BottomSheetHeader(
|
BottomSheetHeader(
|
||||||
title: t.fileInfo.title,
|
title: t.fileInfo.title,
|
||||||
@@ -57,8 +58,9 @@ class _FileInfoBottomSheetState extends State<FileInfoBottomSheet> {
|
|||||||
// keeps no rule under it.
|
// keeps no rule under it.
|
||||||
showBorder: false,
|
showBorder: false,
|
||||||
),
|
),
|
||||||
Expanded(
|
Flexible(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 24),
|
||||||
children: [
|
children: [
|
||||||
if (widget.title.isNotEmpty) _ItemHeadline(title: widget.title, versions: versions),
|
if (widget.title.isNotEmpty) _ItemHeadline(title: widget.title, versions: versions),
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet>
|
|||||||
descendantsAreFocusable: false,
|
descendantsAreFocusable: false,
|
||||||
autofocus: InputModeTracker.isKeyboardMode(context),
|
autofocus: InputModeTracker.isKeyboardMode(context),
|
||||||
onKeyEvent: handleReorderKeyEvent,
|
onKeyEvent: handleReorderKeyEvent,
|
||||||
child: _buildFlatLibraryList(_dialogScrollController, hiddenLibraryKeys),
|
child: _buildFlatLibraryList(_dialogScrollController, hiddenLibraryKeys, shrinkWrap: false),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -403,6 +403,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet>
|
|||||||
}
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
children: [
|
children: [
|
||||||
BottomSheetHeader(title: t.libraries.manageLibraries, icon: Symbols.edit_rounded),
|
BottomSheetHeader(title: t.libraries.manageLibraries, icon: Symbols.edit_rounded),
|
||||||
Flexible(
|
Flexible(
|
||||||
@@ -411,7 +412,7 @@ class _LibraryManagementSheetState extends State<_LibraryManagementSheet>
|
|||||||
descendantsAreFocusable: false,
|
descendantsAreFocusable: false,
|
||||||
autofocus: InputModeTracker.isKeyboardMode(context),
|
autofocus: InputModeTracker.isKeyboardMode(context),
|
||||||
onKeyEvent: handleReorderKeyEvent,
|
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
|
/// Build flat library list with a server subtitle when multiple servers are
|
||||||
/// connected. The TV dialog passes [_dialogScrollController] so focused rows
|
/// connected. The TV dialog passes [_dialogScrollController] so focused rows
|
||||||
/// can be scrolled into view; the bottom sheet passes its own controller.
|
/// 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 showServerNames = _hasMultipleServers();
|
||||||
final isKeyboardMode = InputModeTracker.isKeyboardMode(context);
|
final isKeyboardMode = InputModeTracker.isKeyboardMode(context);
|
||||||
|
|
||||||
return ReorderableListView.builder(
|
return ReorderableListView.builder(
|
||||||
scrollController: scrollController,
|
scrollController: scrollController,
|
||||||
|
shrinkWrap: shrinkWrap,
|
||||||
onReorderItem: _reorderLibraries,
|
onReorderItem: _reorderLibraries,
|
||||||
itemCount: _tempLibraries.length,
|
itemCount: _tempLibraries.length,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
import 'dart:math' as math;
|
||||||
|
|
||||||
import 'package:flutter/gestures.dart';
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -102,12 +103,29 @@ class OverlaySheetController {
|
|||||||
_state._autoFocus(clearSelectSuppression: false);
|
_state._autoFocus(clearSelectSuppression: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sizing applied when a caller supplies no explicit constraints: capped
|
/// Absolute height ceiling for resizable desktop windows. Without it a 4K
|
||||||
/// width on desktop, three quarters of the screen height everywhere.
|
/// 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) {
|
static BoxConstraints _defaultSheetConstraints(BuildContext context) {
|
||||||
final size = MediaQuery.sizeOf(context);
|
final size = MediaQuery.sizeOf(context);
|
||||||
final isDesktop = size.width > 600;
|
final isWideViewport = size.width > 600;
|
||||||
return BoxConstraints(maxWidth: isDesktop ? 700 : double.infinity, maxHeight: size.height * 0.75);
|
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
|
/// 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;
|
Offset? _lastPointerPosition;
|
||||||
double? _sheetHorizontalAnchor;
|
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
|
// Drag-to-dismiss state
|
||||||
double _dragOffset = 0;
|
double _dragOffset = 0;
|
||||||
bool _isDragging = false;
|
bool _isDragging = false;
|
||||||
@@ -356,6 +383,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
|
|||||||
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_pageStack.add(entry);
|
_pageStack.add(entry);
|
||||||
|
_sheetSession++;
|
||||||
_isOpen = true;
|
_isOpen = true;
|
||||||
_isClosing = false;
|
_isClosing = false;
|
||||||
_barrierDismissible = barrierDismissible;
|
_barrierDismissible = barrierDismissible;
|
||||||
@@ -602,9 +630,22 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
|
|||||||
return renderBox?.size.height ?? 300;
|
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) {
|
void _checkDismiss(double velocity) {
|
||||||
final sheetHeight = _getSheetHeight();
|
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();
|
_close();
|
||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -734,6 +775,17 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
|
|||||||
bottom: !isTop,
|
bottom: !isTop,
|
||||||
left: false,
|
left: false,
|
||||||
right: false,
|
right: false,
|
||||||
|
// 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),
|
child: ConstrainedBox(constraints: effectiveConstraints, child: sheetContent),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -742,6 +794,7 @@ class _OverlaySheetHostState extends State<OverlaySheetHost> with SingleTickerPr
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Swipe-down-to-dismiss on non-scrollable areas (skip on TV and top-aligned)
|
// Swipe-down-to-dismiss on non-scrollable areas (skip on TV and top-aligned)
|
||||||
|
|||||||
@@ -56,7 +56,6 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
|
|||||||
final Map<String, Timer> _autoSaveTimers = {};
|
final Map<String, Timer> _autoSaveTimers = {};
|
||||||
final Map<String, _TrackerRatingSource> _trackerSourcesByKey = {};
|
final Map<String, _TrackerRatingSource> _trackerSourcesByKey = {};
|
||||||
final Set<String> _pendingAutoSaves = {};
|
final Set<String> _pendingAutoSaves = {};
|
||||||
final Set<TrackerService> _hiddenTrackers = {};
|
|
||||||
final Set<String> _loading = {};
|
final Set<String> _loading = {};
|
||||||
final Map<String, _SectionStatus> _statuses = {};
|
final Map<String, _SectionStatus> _statuses = {};
|
||||||
TrackerIdResolver? _resolver;
|
TrackerIdResolver? _resolver;
|
||||||
@@ -86,17 +85,13 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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`.
|
// Trakt's account provider is watched by [_trackerSources] via `context`.
|
||||||
return Consumer<TrackersProvider>(
|
return Consumer<TrackersProvider>(
|
||||||
builder: (context, trackers, _) {
|
builder: (context, trackers, _) {
|
||||||
final allTrackerSources = _trackerSources(context);
|
final trackerSources = _trackerSources(context);
|
||||||
final trackerSources = allTrackerSources.where((source) => !_hiddenTrackers.contains(source.service)).toList();
|
|
||||||
_updateTrackerSourceMap(trackerSources);
|
_updateTrackerSourceMap(trackerSources);
|
||||||
_resolverNeedsFribb = trackers.isMalConnected || trackers.isAnilistConnected;
|
_resolverNeedsFribb = trackers.isMalConnected || trackers.isAnilistConnected;
|
||||||
_queueTrackerScoreLoad(allTrackerSources);
|
_queueTrackerScoreLoad(trackerSources);
|
||||||
|
|
||||||
final serverCaps = widget.serverClient?.capabilities;
|
final serverCaps = widget.serverClient?.capabilities;
|
||||||
final showServerRow = serverCaps != null && (serverCaps.numericUserRating || serverCaps.userFavorites);
|
final showServerRow = serverCaps != null && (serverCaps.numericUserRating || serverCaps.userFavorites);
|
||||||
@@ -106,14 +101,17 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
|
|||||||
];
|
];
|
||||||
var focusIndex = 0;
|
var focusIndex = 0;
|
||||||
|
|
||||||
return ConstrainedBox(
|
// Hugs its content: a handful of rows in a 720px sheet was mostly empty
|
||||||
constraints: BoxConstraints(maxHeight: maxHeight),
|
// space. The row set is therefore fixed from the first frame — see
|
||||||
child: Column(
|
// [_loadTrackerScores], which marks an unratable tracker `notAvailable`
|
||||||
|
// rather than removing its row.
|
||||||
|
return Column(
|
||||||
mainAxisSize: .min,
|
mainAxisSize: .min,
|
||||||
children: [
|
children: [
|
||||||
BottomSheetHeader(title: t.rateSheet.title, icon: Symbols.star_rounded),
|
BottomSheetHeader(title: t.rateSheet.title, icon: Symbols.star_rounded),
|
||||||
Flexible(
|
Flexible(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
padding: const EdgeInsets.fromLTRB(10, 4, 10, 12),
|
padding: const EdgeInsets.fromLTRB(10, 4, 10, 12),
|
||||||
children: [
|
children: [
|
||||||
if (showServerRow)
|
if (showServerRow)
|
||||||
@@ -132,7 +130,7 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
|
|||||||
onNavigateUp: _navTo(focusNodes, focusIndex - 1),
|
onNavigateUp: _navTo(focusNodes, focusIndex - 1),
|
||||||
onNavigateDown: _navTo(focusNodes, focusIndex++ + 1),
|
onNavigateDown: _navTo(focusNodes, focusIndex++ + 1),
|
||||||
),
|
),
|
||||||
if (allTrackerSources.isEmpty)
|
if (trackerSources.isEmpty)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4),
|
padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4),
|
||||||
child: Text(
|
child: Text(
|
||||||
@@ -146,7 +144,6 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -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 {
|
Future<void> _loadTrackerScores(List<_TrackerRatingSource> sources) async {
|
||||||
setState(() {
|
setState(() {
|
||||||
for (final source in sources) {
|
for (final source in sources) {
|
||||||
@@ -294,13 +296,9 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
|
|||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
setState(() {
|
setState(() {
|
||||||
for (final source in sources) {
|
for (final source in sources) {
|
||||||
if (_hidesWhenUnavailable(source)) {
|
|
||||||
_hideTrackerSource(source);
|
|
||||||
} else {
|
|
||||||
_loading.remove(source.service.name);
|
_loading.remove(source.service.name);
|
||||||
_statuses[source.service.name] = _SectionStatus(t.rateSheet.notAvailable, isError: true);
|
_statuses[source.service.name] = _SectionStatus(t.rateSheet.notAvailable, isError: true);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -318,12 +316,6 @@ class _RatingBottomSheetState extends State<RatingBottomSheet> {
|
|||||||
} on TrackerRatingUnavailableException catch (e) {
|
} on TrackerRatingUnavailableException catch (e) {
|
||||||
appLogger.d('Rating unavailable', error: e);
|
appLogger.d('Rating unavailable', error: e);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (_hidesWhenUnavailable(source)) {
|
|
||||||
setState(() {
|
|
||||||
_hideTrackerSource(source);
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setState(() {
|
setState(() {
|
||||||
_statuses[key] = _SectionStatus(t.rateSheet.notAvailable, isError: true);
|
_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) {
|
void _setServerStarUnits(int units) {
|
||||||
final clamped = units.clamp(0, 10).toInt();
|
final clamped = units.clamp(0, 10).toInt();
|
||||||
if ((_serverStars * 2).round() == clamped) return;
|
if ((_serverStars * 2).round() == clamped) return;
|
||||||
|
|||||||
@@ -100,15 +100,23 @@ class _ChapterSheetState extends State<ChapterSheet> {
|
|||||||
final currentChapterIndex = chapterSnapshot.data;
|
final currentChapterIndex = chapterSnapshot.data;
|
||||||
Widget content;
|
Widget content;
|
||||||
if (!widget.chaptersLoaded) {
|
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) {
|
} else if (widget.chapters.isEmpty) {
|
||||||
content = Center(
|
content = Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||||
|
child: Center(
|
||||||
|
heightFactor: 1,
|
||||||
child: Text(t.videoControls.noChaptersAvailable, style: TextStyle(color: tokens(context).textMuted)),
|
child: Text(t.videoControls.noChaptersAvailable, style: TextStyle(color: tokens(context).textMuted)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
_initialScroll.maybeScrollTo(currentChapterIndex);
|
_initialScroll.maybeScrollTo(currentChapterIndex);
|
||||||
|
|
||||||
content = ListView.builder(
|
content = ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
controller: _initialScroll.controller,
|
controller: _initialScroll.controller,
|
||||||
itemCount: widget.chapters.length,
|
itemCount: widget.chapters.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
|||||||
@@ -53,14 +53,19 @@ class _QueueSheetState extends State<QueueSheet> {
|
|||||||
|
|
||||||
Widget content;
|
Widget content;
|
||||||
if (items.isEmpty) {
|
if (items.isEmpty) {
|
||||||
content = Center(
|
content = Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 32),
|
||||||
|
child: Center(
|
||||||
|
heightFactor: 1,
|
||||||
child: Text(t.videoControls.noQueueItems, style: TextStyle(color: tokens(context).textMuted)),
|
child: Text(t.videoControls.noQueueItems, style: TextStyle(color: tokens(context).textMuted)),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final currentIndex = items.indexWhere((item) => playbackState.playQueueItemIdFor(item) == currentItemID);
|
final currentIndex = items.indexWhere((item) => playbackState.playQueueItemIdFor(item) == currentItemID);
|
||||||
_initialScroll.maybeScrollTo(currentIndex);
|
_initialScroll.maybeScrollTo(currentIndex);
|
||||||
|
|
||||||
content = ListView.builder(
|
content = ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
controller: _initialScroll.controller,
|
controller: _initialScroll.controller,
|
||||||
itemCount: items.length,
|
itemCount: items.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
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 {
|
class SheetColumnHeader extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
|
|
||||||
|
|||||||
@@ -76,11 +76,15 @@ class _SheetSelectionColumnState extends State<SheetSelectionColumn> implements
|
|||||||
_initialScroll.maybeScrollTo(widget.initialIndex);
|
_initialScroll.maybeScrollTo(widget.initialIndex);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
children: [
|
children: [
|
||||||
if (widget.headerLabel != null) SheetColumnHeader(label: widget.headerLabel!),
|
if (widget.headerLabel != null) SheetColumnHeader(label: widget.headerLabel!),
|
||||||
if (_selectionPending) const LinearProgressIndicator(minHeight: 2),
|
// Reserve the bar's 2px unconditionally: growing the column mid-tap
|
||||||
Expanded(
|
// 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(
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
controller: _initialScroll.controller,
|
controller: _initialScroll.controller,
|
||||||
itemCount: widget.itemCount,
|
itemCount: widget.itemCount,
|
||||||
itemBuilder: (context, index) => widget.itemBuilder(context, index, this),
|
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,
|
title: t.videoControls.searchSubtitles,
|
||||||
icon: Symbols.search_rounded,
|
icon: Symbols.search_rounded,
|
||||||
onBack: () => OverlaySheetController.of(context).pop(),
|
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(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
@@ -440,6 +445,9 @@ class _LanguagePickerViewState extends State<_LanguagePickerView> with Controlle
|
|||||||
title: t.videoControls.language,
|
title: t.videoControls.language,
|
||||||
icon: Symbols.language_rounded,
|
icon: Symbols.language_rounded,
|
||||||
onBack: widget.onBack,
|
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(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../../../widgets/focusable_list_tile.dart';
|
|||||||
import '../../../widgets/overlay_sheet.dart';
|
import '../../../widgets/overlay_sheet.dart';
|
||||||
import 'base_video_control_sheet.dart';
|
import 'base_video_control_sheet.dart';
|
||||||
import 'sheet_selection_column.dart';
|
import 'sheet_selection_column.dart';
|
||||||
|
import 'sheet_split_columns.dart';
|
||||||
import 'subtitle_search_sheet.dart';
|
import 'subtitle_search_sheet.dart';
|
||||||
import '../models/track_controls_state.dart';
|
import '../models/track_controls_state.dart';
|
||||||
import '../helpers/track_filter_helper.dart';
|
import '../helpers/track_filter_helper.dart';
|
||||||
@@ -108,13 +109,9 @@ class TrackSheet extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showAudio && showSubtitles) {
|
if (showAudio && showSubtitles) {
|
||||||
return Row(
|
return SheetSplitColumns(
|
||||||
crossAxisAlignment: .start,
|
start: FocusTraversalGroup(child: audioColumnFor(selection, true)),
|
||||||
children: [
|
end: FocusTraversalGroup(child: subtitleColumnFor(selection, true)),
|
||||||
Expanded(child: FocusTraversalGroup(child: audioColumnFor(selection, true))),
|
|
||||||
VerticalDivider(width: 1, color: Theme.of(context).dividerColor),
|
|
||||||
Expanded(child: FocusTraversalGroup(child: subtitleColumnFor(selection, true))),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import '../../../utils/quality_preset_labels.dart';
|
|||||||
import '../../../widgets/focusable_list_tile.dart';
|
import '../../../widgets/focusable_list_tile.dart';
|
||||||
import '../../../widgets/overlay_sheet.dart';
|
import '../../../widgets/overlay_sheet.dart';
|
||||||
import 'sheet_selection_column.dart';
|
import 'sheet_selection_column.dart';
|
||||||
|
import 'sheet_split_columns.dart';
|
||||||
|
|
||||||
String versionQualityPickerTitle({required bool showVersions, required bool showQuality}) {
|
String versionQualityPickerTitle({required bool showVersions, required bool showQuality}) {
|
||||||
return showQuality
|
return showQuality
|
||||||
@@ -73,14 +74,7 @@ class VersionQualityPicker extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (showVersions && showQuality) {
|
if (showVersions && showQuality) {
|
||||||
return Row(
|
return SheetSplitColumns(start: versionColumn, end: qualityColumn);
|
||||||
crossAxisAlignment: .start,
|
|
||||||
children: [
|
|
||||||
Expanded(child: versionColumn),
|
|
||||||
VerticalDivider(width: 1, color: Theme.of(context).dividerColor),
|
|
||||||
Expanded(child: qualityColumn),
|
|
||||||
],
|
|
||||||
);
|
|
||||||
} else if (showVersions) {
|
} else if (showVersions) {
|
||||||
return versionColumn;
|
return versionColumn;
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -529,6 +529,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
|||||||
final isDesktop = PlatformDetector.isDesktop(context);
|
final isDesktop = PlatformDetector.isDesktop(context);
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
// Playback Speed - hidden for live TV and when user cannot control playback
|
// Playback Speed - hidden for live TV and when user cannot control playback
|
||||||
if (_state.canControl && !_state.isLive)
|
if (_state.canControl && !_state.isLive)
|
||||||
@@ -756,6 +757,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
|||||||
final primary = Theme.of(context).colorScheme.primary;
|
final primary = Theme.of(context).colorScheme.primary;
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
for (final mode in modes)
|
for (final mode in modes)
|
||||||
FocusableListTile(
|
FocusableListTile(
|
||||||
@@ -796,6 +798,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
|||||||
];
|
];
|
||||||
|
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
itemCount: speeds.length,
|
itemCount: speeds.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final speed = speeds[index];
|
final speed = speeds[index];
|
||||||
@@ -826,6 +829,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
|||||||
final primary = Theme.of(context).colorScheme.primary;
|
final primary = Theme.of(context).colorScheme.primary;
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
FocusableListTile(
|
FocusableListTile(
|
||||||
leading: AppIcon(Symbols.restart_alt_rounded, fill: 1, color: tokens(context).textMuted),
|
leading: AppIcon(Symbols.restart_alt_rounded, fill: 1, color: tokens(context).textMuted),
|
||||||
@@ -927,6 +931,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return ListView(
|
return ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
for (final d in ungrouped) _buildDeviceTile(d, currentDevice),
|
for (final d in ungrouped) _buildDeviceTile(d, currentDevice),
|
||||||
for (final entry in groups.entries) ...[
|
for (final entry in groups.entries) ...[
|
||||||
@@ -948,7 +953,25 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildFlatDeviceList(List<AudioDevice> devices, AudioDevice currentDevice) {
|
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(
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
itemCount: devices.length,
|
itemCount: devices.length,
|
||||||
itemBuilder: (context, index) => _buildDeviceTile(devices[index], currentDevice),
|
itemBuilder: (context, index) => _buildDeviceTile(devices[index], currentDevice),
|
||||||
);
|
);
|
||||||
@@ -979,6 +1002,7 @@ class _VideoSettingsSheetState extends State<VideoSettingsSheet> {
|
|||||||
|
|
||||||
// +1 for the import button at the end
|
// +1 for the import button at the end
|
||||||
return ListView.builder(
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
itemCount: presets.length + 1,
|
itemCount: presets.length + 1,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
if (index == presets.length) {
|
if (index == presets.length) {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class SleepTimerActiveStatus extends StatelessWidget {
|
|||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
color: Colors.amber.withValues(alpha: 0.1),
|
color: Colors.amber.withValues(alpha: 0.1),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
t.videoControls.timerActive,
|
t.videoControls.timerActive,
|
||||||
|
|||||||
@@ -10,13 +10,14 @@ import '../../../widgets/app_icon.dart';
|
|||||||
import '../../../widgets/focusable_list_tile.dart';
|
import '../../../widgets/focusable_list_tile.dart';
|
||||||
import '../../../widgets/overlay_sheet.dart';
|
import '../../../widgets/overlay_sheet.dart';
|
||||||
import '../sheets/sheet_column_header.dart';
|
import '../sheets/sheet_column_header.dart';
|
||||||
|
import '../sheets/sheet_split_columns.dart';
|
||||||
import 'sleep_timer_active_status.dart';
|
import 'sleep_timer_active_status.dart';
|
||||||
|
|
||||||
/// Shared UI for sleep timer selection and active status.
|
/// Shared UI for sleep timer selection and active status.
|
||||||
///
|
///
|
||||||
/// Layout mirrors the audio/subtitle [TrackSheet]: two side-by-side columns
|
/// Layout mirrors the audio/subtitle [TrackSheet]: two side-by-side columns
|
||||||
/// inside a [Row], each in its own [FocusTraversalGroup] so D-pad navigation
|
/// inside a [SheetSplitColumns], each in its own [FocusTraversalGroup] so
|
||||||
/// stays inside the column the user is acting on.
|
/// D-pad navigation stays inside the column the user is acting on.
|
||||||
///
|
///
|
||||||
/// * Left column ("Stop at") — event-based stop options. Today this is just
|
/// * Left column ("Stop at") — event-based stop options. Today this is just
|
||||||
/// "End of current video"; the column is intentionally open-ended for
|
/// "End of current video"; the column is intentionally open-ended for
|
||||||
@@ -48,23 +49,18 @@ class SleepTimerContent extends StatelessWidget {
|
|||||||
final showActiveStatus = sleepTimer.isActive && (remainingTime != null || sleepTimer.isEndOfVideoMode);
|
final showActiveStatus = sleepTimer.isActive && (remainingTime != null || sleepTimer.isEndOfVideoMode);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
children: [
|
children: [
|
||||||
if (showActiveStatus) ...[
|
if (showActiveStatus) ...[
|
||||||
SleepTimerActiveStatus(sleepTimer: sleepTimer, remainingTime: remainingTime, onCancel: onCancel),
|
SleepTimerActiveStatus(sleepTimer: sleepTimer, remainingTime: remainingTime, onCancel: onCancel),
|
||||||
Divider(color: Theme.of(context).dividerColor, height: 1),
|
Divider(color: Theme.of(context).dividerColor, height: 1),
|
||||||
],
|
],
|
||||||
Expanded(
|
Flexible(
|
||||||
child: Row(
|
child: SheetSplitColumns(
|
||||||
crossAxisAlignment: .start,
|
start: FocusTraversalGroup(
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: FocusTraversalGroup(
|
|
||||||
child: _SleepTimerEventColumn(player: player, sleepTimer: sleepTimer),
|
child: _SleepTimerEventColumn(player: player, sleepTimer: sleepTimer),
|
||||||
),
|
),
|
||||||
),
|
end: FocusTraversalGroup(
|
||||||
VerticalDivider(width: 1, color: Theme.of(context).dividerColor),
|
|
||||||
Expanded(
|
|
||||||
child: FocusTraversalGroup(
|
|
||||||
child: _SleepTimerDurationColumn(
|
child: _SleepTimerDurationColumn(
|
||||||
player: player,
|
player: player,
|
||||||
sleepTimer: sleepTimer,
|
sleepTimer: sleepTimer,
|
||||||
@@ -72,8 +68,6 @@ class SleepTimerContent extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -93,10 +87,12 @@ class _SleepTimerEventColumn extends StatelessWidget {
|
|||||||
final label = t.videoControls.sleepTimerEndOfVideo;
|
final label = t.videoControls.sleepTimerEndOfVideo;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
children: [
|
children: [
|
||||||
SheetColumnHeader(label: t.videoControls.sleepTimerStopAtHeader),
|
SheetColumnHeader(label: t.videoControls.sleepTimerStopAtHeader),
|
||||||
Expanded(
|
Flexible(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
|
shrinkWrap: true,
|
||||||
children: [
|
children: [
|
||||||
FocusableListTile(
|
FocusableListTile(
|
||||||
leading: AppIcon(
|
leading: AppIcon(
|
||||||
@@ -147,10 +143,12 @@ class _SleepTimerDurationColumn extends StatelessWidget {
|
|||||||
: null;
|
: null;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
|
mainAxisSize: .min,
|
||||||
children: [
|
children: [
|
||||||
SheetColumnHeader(label: t.videoControls.sleepTimerDurationHeader),
|
SheetColumnHeader(label: t.videoControls.sleepTimerDurationHeader),
|
||||||
Expanded(
|
Flexible(
|
||||||
child: ListView.builder(
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
itemCount: durations.length,
|
itemCount: durations.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final minutes = durations[index];
|
final minutes = durations[index];
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:plezy/media/media_filter.dart';
|
|||||||
import 'package:plezy/screens/libraries/filters_bottom_sheet.dart';
|
import 'package:plezy/screens/libraries/filters_bottom_sheet.dart';
|
||||||
import 'package:plezy/screens/libraries/state_messages.dart';
|
import 'package:plezy/screens/libraries/state_messages.dart';
|
||||||
import 'package:plezy/widgets/bottom_sheet_header.dart';
|
import 'package:plezy/widgets/bottom_sheet_header.dart';
|
||||||
|
import 'package:plezy/widgets/bottom_sheet_page_scaffold.dart';
|
||||||
import 'package:plezy/widgets/overlay_sheet.dart';
|
import 'package:plezy/widgets/overlay_sheet.dart';
|
||||||
|
|
||||||
final _filters = [
|
final _filters = [
|
||||||
@@ -18,7 +19,7 @@ MediaFilterValue _value(String key, String title) => MediaFilterValue(key: key,
|
|||||||
void main() {
|
void main() {
|
||||||
testWidgets('filter switch rejects an obsolete success and its presentation effects', (tester) async {
|
testWidgets('filter switch rejects an obsolete success and its presentation effects', (tester) async {
|
||||||
final requests = _FilterRequests();
|
final requests = _FilterRequests();
|
||||||
final harness = await _pumpSheet(tester, loader: requests.load);
|
await _pumpSheet(tester, loader: requests.load);
|
||||||
|
|
||||||
await _openFilter(tester, 'Genre');
|
await _openFilter(tester, 'Genre');
|
||||||
await _goBack(tester);
|
await _goBack(tester);
|
||||||
@@ -34,12 +35,11 @@ void main() {
|
|||||||
expect(find.text('Current Studio'), findsOneWidget);
|
expect(find.text('Current Studio'), findsOneWidget);
|
||||||
expect(find.text('Obsolete Genre'), findsNothing);
|
expect(find.text('Obsolete Genre'), findsNothing);
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
harness.dispose();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('same-filter reopen rejects the first request completion', (tester) async {
|
testWidgets('same-filter reopen rejects the first request completion', (tester) async {
|
||||||
final requests = _FilterRequests();
|
final requests = _FilterRequests();
|
||||||
final harness = await _pumpSheet(tester, loader: requests.load);
|
await _pumpSheet(tester, loader: requests.load);
|
||||||
|
|
||||||
await _openFilter(tester, 'Genre');
|
await _openFilter(tester, 'Genre');
|
||||||
await _goBack(tester);
|
await _goBack(tester);
|
||||||
@@ -52,12 +52,11 @@ void main() {
|
|||||||
|
|
||||||
expect(find.text('New Genre'), findsOneWidget);
|
expect(find.text('New Genre'), findsOneWidget);
|
||||||
expect(find.text('Old Genre'), findsNothing);
|
expect(find.text('Old Genre'), findsNothing);
|
||||||
harness.dispose();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('stale failure cannot replace a newer successful value list', (tester) async {
|
testWidgets('stale failure cannot replace a newer successful value list', (tester) async {
|
||||||
final requests = _FilterRequests();
|
final requests = _FilterRequests();
|
||||||
final harness = await _pumpSheet(tester, loader: requests.load);
|
await _pumpSheet(tester, loader: requests.load);
|
||||||
|
|
||||||
await _openFilter(tester, 'Genre');
|
await _openFilter(tester, 'Genre');
|
||||||
await _goBack(tester);
|
await _goBack(tester);
|
||||||
@@ -70,7 +69,6 @@ void main() {
|
|||||||
|
|
||||||
expect(find.byType(ErrorStateWidget), findsNothing);
|
expect(find.byType(ErrorStateWidget), findsNothing);
|
||||||
expect(find.text('Current Studio'), findsOneWidget);
|
expect(find.text('Current Studio'), findsOneWidget);
|
||||||
harness.dispose();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('library replacement retires the old owner request', (tester) async {
|
testWidgets('library replacement retires the old owner request', (tester) async {
|
||||||
@@ -87,13 +85,12 @@ void main() {
|
|||||||
expect(find.text('Old Library Genre'), findsNothing);
|
expect(find.text('Old Library Genre'), findsNothing);
|
||||||
expect(find.byType(CircularProgressIndicator), findsNothing);
|
expect(find.byType(CircularProgressIndicator), findsNothing);
|
||||||
expect(find.text('Filters'), findsOneWidget);
|
expect(find.text('Filters'), findsOneWidget);
|
||||||
harness.dispose();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('back then clear retires a loading request before closing', (tester) async {
|
testWidgets('back then clear retires a loading request before closing', (tester) async {
|
||||||
final requests = _FilterRequests();
|
final requests = _FilterRequests();
|
||||||
final applied = <Map<String, String>>[];
|
final applied = <Map<String, String>>[];
|
||||||
final harness = await _pumpSheet(
|
await _pumpSheet(
|
||||||
tester,
|
tester,
|
||||||
loader: requests.load,
|
loader: requests.load,
|
||||||
selectedFilters: const {'studio': 'selected'},
|
selectedFilters: const {'studio': 'selected'},
|
||||||
@@ -112,13 +109,12 @@ void main() {
|
|||||||
requests.request('genre').complete([_value('late', 'Late Genre')]);
|
requests.request('genre').complete([_value('late', 'Late Genre')]);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(tester.takeException(), isNull);
|
expect(tester.takeException(), isNull);
|
||||||
harness.dispose();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('missing selected value is preserved until explicit user action', (tester) async {
|
testWidgets('missing selected value is preserved until explicit user action', (tester) async {
|
||||||
final requests = _FilterRequests();
|
final requests = _FilterRequests();
|
||||||
final applied = <Map<String, String>>[];
|
final applied = <Map<String, String>>[];
|
||||||
final harness = await _pumpSheet(
|
await _pumpSheet(
|
||||||
tester,
|
tester,
|
||||||
loader: requests.load,
|
loader: requests.load,
|
||||||
selectedFilters: const {'genre': 'missing'},
|
selectedFilters: const {'genre': 'missing'},
|
||||||
@@ -132,12 +128,11 @@ void main() {
|
|||||||
|
|
||||||
expect(find.text('Clear All'), findsOneWidget);
|
expect(find.text('Clear All'), findsOneWidget);
|
||||||
expect(applied, isEmpty);
|
expect(applied, isEmpty);
|
||||||
harness.dispose();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('load failure has retry state while empty success remains selectable', (tester) async {
|
testWidgets('load failure has retry state while empty success remains selectable', (tester) async {
|
||||||
final requests = _FilterRequests();
|
final requests = _FilterRequests();
|
||||||
final harness = await _pumpSheet(tester, loader: requests.load);
|
await _pumpSheet(tester, loader: requests.load);
|
||||||
|
|
||||||
await _openFilter(tester, 'Genre');
|
await _openFilter(tester, 'Genre');
|
||||||
requests.request('genre').completeError(StateError('temporary failure'));
|
requests.request('genre').completeError(StateError('temporary failure'));
|
||||||
@@ -154,12 +149,49 @@ void main() {
|
|||||||
|
|
||||||
expect(find.byType(ErrorStateWidget), findsNothing);
|
expect(find.byType(ErrorStateWidget), findsNothing);
|
||||||
expect(find.text('All'), findsOneWidget);
|
expect(find.text('All'), findsOneWidget);
|
||||||
harness.dispose();
|
});
|
||||||
|
|
||||||
|
testWidgets('settled filter-values states hug while the transient one holds the height', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1280, 800);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
final requests = _FilterRequests();
|
||||||
|
await _pumpSheet(tester, loader: requests.load);
|
||||||
|
|
||||||
|
const cap = 800 * 0.75;
|
||||||
|
double sheetHeight() => tester.getSize(find.byType(BottomSheetPageScaffold)).height;
|
||||||
|
|
||||||
|
// The root filters list must hug too — nothing else in the suite pins it.
|
||||||
|
// (`sheet == header + list` is a layout identity for a min-Column with no
|
||||||
|
// divider, so it holds even under a full fill; only the cap bound below
|
||||||
|
// actually discriminates.)
|
||||||
|
final filtersListHeight = sheetHeight();
|
||||||
|
expect(filtersListHeight, lessThan(cap), reason: 'two filters must not fill the cap');
|
||||||
|
|
||||||
|
// Drilling in is a setState page swap inside one sheet, so the transient
|
||||||
|
// spinner must hold the outgoing height: a change here moves the header and
|
||||||
|
// its Back button, and moves them straight back when the values land.
|
||||||
|
await _openFilter(tester, 'Genre');
|
||||||
|
expect(sheetHeight(), filtersListHeight, reason: 'the transient spinner must not move the sheet');
|
||||||
|
|
||||||
|
// Settled states hug — that is the empty space this change exists to remove.
|
||||||
|
requests.request('genre').completeError(StateError('temporary failure'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byType(ErrorStateWidget), findsOneWidget);
|
||||||
|
expect(sheetHeight(), lessThan(cap), reason: 'error state must not fill the cap');
|
||||||
|
|
||||||
|
await tester.tap(find.text('Retry'));
|
||||||
|
await tester.pump();
|
||||||
|
requests.request('genre', 1).complete([_value('action', 'Action')]);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Action'), findsOneWidget);
|
||||||
|
expect(sheetHeight(), lessThan(cap), reason: 'short value list must not fill the cap');
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('cached values bypass the lazy loader', (tester) async {
|
testWidgets('cached values bypass the lazy loader', (tester) async {
|
||||||
var loadCount = 0;
|
var loadCount = 0;
|
||||||
final harness = await _pumpSheet(
|
await _pumpSheet(
|
||||||
tester,
|
tester,
|
||||||
loader: (_) async {
|
loader: (_) async {
|
||||||
loadCount++;
|
loadCount++;
|
||||||
@@ -174,7 +206,6 @@ void main() {
|
|||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
expect(find.text('Cached Genre'), findsOneWidget);
|
expect(find.text('Cached Genre'), findsOneWidget);
|
||||||
expect(loadCount, 0);
|
expect(loadCount, 0);
|
||||||
harness.dispose();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,19 +257,31 @@ Future<_SheetHarness> _pumpSheet(
|
|||||||
|
|
||||||
await tester.tap(find.text('Open'));
|
await tester.tap(find.text('Open'));
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
return _SheetHarness(config);
|
final harness = _SheetHarness(config);
|
||||||
|
addTearDown(harness.dispose);
|
||||||
|
return harness;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _openFilter(WidgetTester tester, String title) async {
|
Future<void> _openFilter(WidgetTester tester, String title) async {
|
||||||
await tester.tap(find.text(title));
|
await tester.tap(find.text(title));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
expect(find.byType(CircularProgressIndicator), findsOneWidget);
|
||||||
|
await _settleSheetResize(tester);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _goBack(WidgetTester tester) async {
|
Future<void> _goBack(WidgetTester tester) async {
|
||||||
final headerRect = tester.getRect(find.byType(BottomSheetHeader));
|
final headerRect = tester.getRect(find.byType(BottomSheetHeader));
|
||||||
await tester.tapAt(headerRect.centerLeft + const Offset(20, 0));
|
await tester.tapAt(headerRect.centerLeft + const Offset(20, 0));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
await _settleSheetResize(tester);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Advances past the host's 180ms resize tween. A page swap changes the sheet's
|
||||||
|
/// height, and mid-tween the content is laid out at its final size but clipped
|
||||||
|
/// by the still-animating box — so header geometry is not tappable until this
|
||||||
|
/// completes. `pumpAndSettle` cannot be used: the spinner never settles.
|
||||||
|
Future<void> _settleSheetResize(WidgetTester tester) async {
|
||||||
|
await tester.pump(const Duration(milliseconds: 200));
|
||||||
}
|
}
|
||||||
|
|
||||||
class _FilterRequests {
|
class _FilterRequests {
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
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/key_event_utils.dart';
|
import 'package:plezy/focus/key_event_utils.dart';
|
||||||
import 'package:plezy/utils/platform_detector.dart';
|
import 'package:plezy/utils/platform_detector.dart';
|
||||||
|
import 'package:plezy/widgets/bottom_sheet_header.dart';
|
||||||
|
import 'package:plezy/widgets/bottom_sheet_page_scaffold.dart';
|
||||||
import 'package:plezy/widgets/overlay_sheet.dart';
|
import 'package:plezy/widgets/overlay_sheet.dart';
|
||||||
|
import 'package:plezy/widgets/video_controls/sheets/sheet_split_columns.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('scrollable sheet does not attach to parent primary controller', (tester) async {
|
testWidgets('scrollable sheet does not attach to parent primary controller', (tester) async {
|
||||||
@@ -95,6 +100,379 @@ void main() {
|
|||||||
expect(sheetSize.width, 700);
|
expect(sheetSize.width, 700);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
testWidgets('a tall desktop window clamps the sheet to the absolute ceiling, a tall phone does not', (tester) async {
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
// Pin both gates the ceiling reads, not just the OS one.
|
||||||
|
PlatformDetector.debugSetIsDesktopOSOverride(true);
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(false);
|
||||||
|
addTearDown(() {
|
||||||
|
PlatformDetector.debugSetIsDesktopOSOverride(null);
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 1440p desktop: 75% would be 1080px, which reads as a wall of list.
|
||||||
|
tester.view.physicalSize = const Size(2560, 1440);
|
||||||
|
final controller = await _pumpIdleHost(tester);
|
||||||
|
unawaited(
|
||||||
|
controller.show<void>(
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Many', child: _FixedRowList(rowCount: 200)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getSize(find.byType(BottomSheetPageScaffold)).height, 720);
|
||||||
|
|
||||||
|
controller.close();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Narrow viewports keep the plain 75% rule: the ceiling is windows-only.
|
||||||
|
tester.view.physicalSize = const Size(400, 1200);
|
||||||
|
unawaited(
|
||||||
|
controller.show<void>(
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Many', child: _FixedRowList(rowCount: 200)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getSize(find.byType(BottomSheetPageScaffold)).height, 1200 * 0.75);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('a portrait tablet keeps the full 75% height — the ceiling is for resizable windows', (tester) async {
|
||||||
|
// Wide enough for the 700px width cap, but not a window the user can drag
|
||||||
|
// taller, so the absolute ceiling must not apply.
|
||||||
|
PlatformDetector.debugSetIsDesktopOSOverride(false);
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(false);
|
||||||
|
addTearDown(() {
|
||||||
|
PlatformDetector.debugSetIsDesktopOSOverride(null);
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(null);
|
||||||
|
});
|
||||||
|
tester.view.physicalSize = const Size(1024, 1366);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
final controller = await _pumpIdleHost(tester);
|
||||||
|
unawaited(
|
||||||
|
controller.show<void>(
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Many', child: _FixedRowList(rowCount: 200)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final size = tester.getSize(find.byType(BottomSheetPageScaffold));
|
||||||
|
expect(size.height, 1366 * 0.75);
|
||||||
|
expect(size.width, 700, reason: 'the width cap still applies on a wide viewport');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('TV keeps the full 75% height — the ceiling is for resizable windows', (tester) async {
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(true);
|
||||||
|
PlatformDetector.debugSetIsDesktopOSOverride(true);
|
||||||
|
addTearDown(() {
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(null);
|
||||||
|
PlatformDetector.debugSetIsDesktopOSOverride(null);
|
||||||
|
});
|
||||||
|
tester.view.physicalSize = const Size(1920, 1080);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
final controller = await _pumpIdleHost(tester);
|
||||||
|
unawaited(
|
||||||
|
controller.show<void>(
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Many', child: _FixedRowList(rowCount: 200)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// A 10-foot UI wants every row it can get: 810, not the 720 window ceiling.
|
||||||
|
expect(tester.getSize(find.byType(BottomSheetPageScaffold)).height, 1080 * 0.75);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('the hostless modal fallback inherits the same sizing rules', (tester) async {
|
||||||
|
PlatformDetector.debugSetIsDesktopOSOverride(true);
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(false);
|
||||||
|
addTearDown(() {
|
||||||
|
PlatformDetector.debugSetIsDesktopOSOverride(null);
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(null);
|
||||||
|
});
|
||||||
|
tester.view.physicalSize = const Size(2560, 1440);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: ThemeData(platform: TargetPlatform.android),
|
||||||
|
home: Scaffold(
|
||||||
|
body: Builder(
|
||||||
|
// No OverlaySheetHost anywhere above: showAdaptive must fall back
|
||||||
|
// to showModalBottomSheet and still honour the default constraints.
|
||||||
|
builder: (context) => ElevatedButton(
|
||||||
|
onPressed: () => unawaited(
|
||||||
|
OverlaySheetController.showAdaptive<void>(
|
||||||
|
context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Many', child: _FixedRowList(rowCount: 200)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: const Text('Open'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final size = tester.getSize(find.byType(BottomSheetPageScaffold));
|
||||||
|
expect(size.height, 720);
|
||||||
|
expect(size.width, 700);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (final anchor in [Alignment.bottomCenter, Alignment.topCenter]) {
|
||||||
|
final isTop = anchor.y < 0;
|
||||||
|
final label = isTop ? 'top' : 'bottom';
|
||||||
|
|
||||||
|
testWidgets('a $label-anchored sheet keeps its anchored edge still while it grows', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1280, 800);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
final controller = await _pumpIdleHost(tester);
|
||||||
|
unawaited(
|
||||||
|
controller.show<void>(
|
||||||
|
alignment: anchor,
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Small', child: _FixedRowList(rowCount: 2)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
final animated = find.descendant(of: find.byType(OverlaySheetHost), matching: find.byType(AnimatedSize));
|
||||||
|
final smallHeight = tester.getSize(animated).height;
|
||||||
|
// The header is the content row adjacent to the anchored edge for a
|
||||||
|
// top-anchored sheet, and the farthest from it for a bottom-anchored one.
|
||||||
|
final header = find.byType(BottomSheetHeader);
|
||||||
|
final headerTopBefore = tester.getTopLeft(header).dy;
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
controller.push<void>(
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Big', child: _FixedRowList(rowCount: 6)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mid-tween: this is the only point where AnimatedSize's `alignment`
|
||||||
|
// is observable. The child is already laid out at its final size, so a
|
||||||
|
// wrong alignment shows up as the content sliding against its anchor.
|
||||||
|
// The first pump starts the tween; the second advances it.
|
||||||
|
await tester.pump();
|
||||||
|
await tester.pump(const Duration(milliseconds: 90));
|
||||||
|
final growing = tester.getSize(animated).height;
|
||||||
|
expect(growing, greaterThan(smallHeight));
|
||||||
|
expect(growing, lessThan(smallHeight + 160));
|
||||||
|
if (isTop) {
|
||||||
|
expect(tester.getTopLeft(header).dy, headerTopBefore, reason: 'header is pinned to the top anchor');
|
||||||
|
} else {
|
||||||
|
// Bottom-anchored: the child is laid out at its final height and
|
||||||
|
// bottom-pinned, so the header sits at its FINAL y from the first tween
|
||||||
|
// frame and holds there. Under a wrong (top) alignment it would instead
|
||||||
|
// track the animating height. Only the exact value distinguishes them.
|
||||||
|
expect(tester.getTopLeft(header).dy, 800 - (smallHeight + 160));
|
||||||
|
expect(tester.getBottomLeft(animated).dy, 800, reason: 'bottom edge stays pinned to the viewport');
|
||||||
|
}
|
||||||
|
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getSize(animated).height, smallHeight + 160);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('popping back to a shorter page eases the sheet down', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1280, 800);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
final controller = await _pumpIdleHost(tester);
|
||||||
|
unawaited(
|
||||||
|
controller.show<void>(
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Small', child: _FixedRowList(rowCount: 2)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
final animated = find.descendant(of: find.byType(OverlaySheetHost), matching: find.byType(AnimatedSize));
|
||||||
|
final smallHeight = tester.getSize(animated).height;
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
controller.push<void>(
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Big', child: _FixedRowList(rowCount: 6)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getSize(animated).height, smallHeight + 160);
|
||||||
|
|
||||||
|
controller.pop();
|
||||||
|
await tester.pump();
|
||||||
|
expect(tester.getSize(animated).height, smallHeight + 160, reason: 'shrink must ease, not jump');
|
||||||
|
await tester.pump(const Duration(milliseconds: 90));
|
||||||
|
final shrinking = tester.getSize(animated).height;
|
||||||
|
expect(shrinking, lessThan(smallHeight + 160));
|
||||||
|
expect(shrinking, greaterThan(smallHeight));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getSize(animated).height, smallHeight);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('a short content-sized sheet still needs a deliberate drag to dismiss', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1280, 800);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
final controller = await _pumpIdleHost(tester);
|
||||||
|
unawaited(
|
||||||
|
controller.show<void>(
|
||||||
|
showDragHandle: true,
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Tiny', child: _FixedRowList(rowCount: 2)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// ~145px of content plus a 20px drag handle, so a bare 25%-of-height
|
||||||
|
// threshold would be ~41px — barely more than touch slop. The absolute
|
||||||
|
// floor is what keeps a stray flick from closing it.
|
||||||
|
expect(tester.getSize(find.byType(BottomSheetPageScaffold)).height, lessThan(200));
|
||||||
|
|
||||||
|
// 90px of gesture is ~70px of drag once the recogniser's slop is consumed:
|
||||||
|
// past 25% of the sheet, but well short of the floor.
|
||||||
|
await _slowDragDown(tester, 90);
|
||||||
|
expect(find.byType(BottomSheetPageScaffold), findsOneWidget, reason: '70px of drag must not dismiss');
|
||||||
|
|
||||||
|
await _slowDragDown(tester, 140);
|
||||||
|
expect(find.byType(BottomSheetPageScaffold), findsNothing, reason: '140px clears the floor');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('a sheet shorter than the dismiss floor stays dismissible by drag', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1280, 800);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
final controller = await _pumpIdleHost(tester);
|
||||||
|
unawaited(
|
||||||
|
controller.show<void>(
|
||||||
|
showDragHandle: true,
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'One', child: _FixedRowList(rowCount: 1)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// A one-row menu is shorter than the 96px floor. Left unclamped, the floor
|
||||||
|
// would demand a drag longer than the sheet itself, so the only way to
|
||||||
|
// dismiss by distance would be to pull it clean off the screen.
|
||||||
|
final sheetHeight = tester
|
||||||
|
.getSize(find.descendant(of: find.byType(OverlaySheetHost), matching: find.byType(AnimatedSize)))
|
||||||
|
.height;
|
||||||
|
expect(sheetHeight, lessThan(96 / 0.6), reason: 'the clamp must actually be the binding rule here');
|
||||||
|
|
||||||
|
// Just past 60% of the sheet, still short of the raw 96px floor.
|
||||||
|
await _slowDragDown(tester, sheetHeight * 0.6 + 8 + _dragSlop);
|
||||||
|
expect(find.byType(BottomSheetPageScaffold), findsNothing, reason: 'clamped floor keeps a short sheet dismissible');
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('sheet page shrinks to short content instead of filling the height cap', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1280, 800);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
await _pumpHostedSheet(tester, const BottomSheetPageScaffold(title: 'Tiny', child: _FixedRowList(rowCount: 2)));
|
||||||
|
|
||||||
|
final sheetHeight = tester.getSize(find.byType(BottomSheetPageScaffold)).height;
|
||||||
|
final headerHeight = tester.getSize(find.byType(BottomSheetHeader)).height;
|
||||||
|
|
||||||
|
// Exactly header + two 40px rows: no filler between the last row and the
|
||||||
|
// bottom of the sheet.
|
||||||
|
expect(sheetHeight, headerHeight + 80);
|
||||||
|
expect(sheetHeight, lessThan(800 * 0.75));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('sheet page clamps to the height cap once content overflows', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1280, 800);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
await _pumpHostedSheet(tester, const BottomSheetPageScaffold(title: 'Many', child: _FixedRowList(rowCount: 200)));
|
||||||
|
|
||||||
|
expect(tester.getSize(find.byType(BottomSheetPageScaffold)).height, 800 * 0.75);
|
||||||
|
// Still scrollable rather than overflowing.
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
await tester.drag(find.byType(ListView), const Offset(0, -200));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('row 0'), findsNothing);
|
||||||
|
expect(
|
||||||
|
tester.getSize(find.byType(BottomSheetPageScaffold)).height,
|
||||||
|
800 * 0.75,
|
||||||
|
reason: 'scrolling must not resize the sheet',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('uneven row heights do not make the clamped sheet wobble while scrolling', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1280, 800);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
// A shrink-wrapping sliver reports an *estimated* max extent extrapolated
|
||||||
|
// from the rows laid out so far. That estimate can only move the sheet while
|
||||||
|
// it is near the clamp, so this list is deliberately sized to just overflow
|
||||||
|
// the 600px cap (11 rows of 40/64/88 = 616px) rather than to swamp it.
|
||||||
|
await _pumpHostedSheet(
|
||||||
|
tester,
|
||||||
|
const BottomSheetPageScaffold(title: 'Uneven', child: _FixedRowList(rowCount: 11, varyHeights: true)),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (var step = 0; step < 4; step++) {
|
||||||
|
expect(tester.getSize(find.byType(BottomSheetPageScaffold)).height, 800 * 0.75, reason: 'step $step');
|
||||||
|
await tester.drag(find.byType(ListView), const Offset(0, -40));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
expect(tester.getSize(find.byType(BottomSheetPageScaffold)).height, 800 * 0.75);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('split columns size to the taller column and stretch the rule to match', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1280, 800);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
await _pumpHostedSheet(
|
||||||
|
tester,
|
||||||
|
const SheetSplitColumns(start: _FixedRowList(rowCount: 2), end: _FixedRowList(rowCount: 3)),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Taller column wins; the hairline rule must not drag the row to the cap.
|
||||||
|
expect(tester.getSize(find.byType(SheetSplitColumns)).height, 120);
|
||||||
|
final rule = tester.getSize(
|
||||||
|
find.descendant(of: find.byType(SheetSplitColumns), matching: find.byType(VerticalDivider)),
|
||||||
|
);
|
||||||
|
expect(rule, const Size(1, 120));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('replacing an open sheet adopts the new height instead of easing down from the old one', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1280, 800);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
final controller = await _pumpIdleHost(tester);
|
||||||
|
|
||||||
|
unawaited(
|
||||||
|
controller.show<void>(
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Many', child: _FixedRowList(rowCount: 200)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(tester.getSize(find.byType(AnimatedSize)).height, 800 * 0.75);
|
||||||
|
|
||||||
|
// Replacing without closing keeps the sheet mounted, so only the per-sheet
|
||||||
|
// animation key stops the new page from easing down from 600px.
|
||||||
|
unawaited(
|
||||||
|
controller.show<void>(
|
||||||
|
builder: (_) => const BottomSheetPageScaffold(title: 'Tiny', child: _FixedRowList(rowCount: 2)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
final headerHeight = tester.getSize(find.byType(BottomSheetHeader)).height;
|
||||||
|
expect(tester.getSize(find.byType(AnimatedSize)).height, headerHeight + 80);
|
||||||
|
});
|
||||||
|
|
||||||
testWidgets('pointer-opened sheet claims focus and handles Back before the screen', (tester) async {
|
testWidgets('pointer-opened sheet claims focus and handles Back before the screen', (tester) async {
|
||||||
final screenFocusNode = FocusNode(debugLabel: 'Screen');
|
final screenFocusNode = FocusNode(debugLabel: 'Screen');
|
||||||
addTearDown(screenFocusNode.dispose);
|
addTearDown(screenFocusNode.dispose);
|
||||||
@@ -428,3 +806,82 @@ void main() {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Shrink-wrapping list for the sizing tests: 40px rows by default, or a
|
||||||
|
/// 40/64/88 cycle when [varyHeights] is set so the sliver's estimated extent
|
||||||
|
/// keeps changing as rows are laid out.
|
||||||
|
class _FixedRowList extends StatelessWidget {
|
||||||
|
final int rowCount;
|
||||||
|
final bool varyHeights;
|
||||||
|
|
||||||
|
const _FixedRowList({required this.rowCount, this.varyHeights = false});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: rowCount,
|
||||||
|
itemBuilder: (_, index) => SizedBox(height: varyHeights ? 40 + (index % 3) * 24 : 40, child: Text('row $index')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pumpHostedSheet(WidgetTester tester, Widget content) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: ThemeData(platform: TargetPlatform.android),
|
||||||
|
home: OverlaySheetHost(
|
||||||
|
child: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) => ElevatedButton(
|
||||||
|
onPressed: () => OverlaySheetController.of(context).show<void>(builder: (_) => content),
|
||||||
|
child: const Text('Open'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Distance the vertical drag recogniser swallows before it starts reporting
|
||||||
|
/// updates, in 10px steps: two steps to clear `kTouchSlop`.
|
||||||
|
const _dragSlop = 20.0;
|
||||||
|
|
||||||
|
/// Drags the sheet down by [distance] in steps, so only the distance threshold
|
||||||
|
/// can decide. A single-move `tester.drag` would never emit `onUpdate` past the
|
||||||
|
/// recogniser's slop, which is why this steps by hand; the fling-velocity
|
||||||
|
/// escape hatch cannot fire either way, because `TestGesture.moveBy` stamps
|
||||||
|
/// every pointer event with `Duration.zero`.
|
||||||
|
Future<void> _slowDragDown(WidgetTester tester, double distance) async {
|
||||||
|
final gesture = await tester.startGesture(tester.getCenter(find.byType(BottomSheetHeader)));
|
||||||
|
for (var moved = 0.0; moved < distance; moved += 10) {
|
||||||
|
await gesture.moveBy(const Offset(0, 10));
|
||||||
|
await tester.pump(const Duration(milliseconds: 40));
|
||||||
|
}
|
||||||
|
await gesture.up();
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<OverlaySheetController> _pumpIdleHost(WidgetTester tester) async {
|
||||||
|
late OverlaySheetController controller;
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: ThemeData(platform: TargetPlatform.android),
|
||||||
|
home: OverlaySheetHost(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) {
|
||||||
|
controller = OverlaySheetController.of(context);
|
||||||
|
return const Scaffold(body: SizedBox.expand());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
return controller;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,190 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:plezy/i18n/strings.g.dart';
|
||||||
|
import 'package:plezy/media/ids.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/media/media_server_client.dart';
|
||||||
|
import 'package:plezy/media/server_capabilities.dart';
|
||||||
|
import 'package:plezy/providers/trackers_provider.dart';
|
||||||
|
import 'package:plezy/services/base_shared_preferences_service.dart';
|
||||||
|
import 'package:plezy/services/trackers/anilist/anilist_tracker.dart';
|
||||||
|
import 'package:plezy/services/trackers/mal/mal_tracker.dart';
|
||||||
|
import 'package:plezy/services/trackers/mdblist/mdblist_tracker.dart';
|
||||||
|
import 'package:plezy/services/trackers/simkl/simkl_tracker.dart';
|
||||||
|
import 'package:plezy/services/trackers/tracker_account_store.dart';
|
||||||
|
import 'package:plezy/services/trackers/tracker_constants.dart';
|
||||||
|
import 'package:plezy/services/trackers/tracker_session.dart';
|
||||||
|
import 'package:plezy/services/trackers/trakt/trakt_tracker.dart';
|
||||||
|
import 'package:plezy/utils/platform_detector.dart';
|
||||||
|
import 'package:plezy/widgets/overlay_sheet.dart';
|
||||||
|
import 'package:plezy/widgets/rating_bottom_sheet.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
|
||||||
|
import '../test_helpers/io_fakes.dart';
|
||||||
|
import '../test_helpers/media_items.dart';
|
||||||
|
import '../test_helpers/prefs.dart';
|
||||||
|
import '../test_helpers/theme.dart';
|
||||||
|
|
||||||
|
/// Sizing suite for [RatingBottomSheet]. The sheet no longer carries its own
|
||||||
|
/// `ConstrainedBox(maxHeight: height * 0.64/0.74)`: it is a
|
||||||
|
/// `Column(mainAxisSize: .min)` + `Flexible` + `ListView(shrinkWrap: true)`
|
||||||
|
/// whose only ceiling is the [OverlaySheetHost] cap (`viewportHeight * 0.75`,
|
||||||
|
/// itself capped at 720 on a desktop OS). Every test drives the real host so
|
||||||
|
/// the cap under test is the production one.
|
||||||
|
void main() {
|
||||||
|
setUp(() {
|
||||||
|
resetSharedPreferencesForTest();
|
||||||
|
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||||
|
_resetTrackerBindings();
|
||||||
|
});
|
||||||
|
tearDown(_resetTrackerBindings);
|
||||||
|
|
||||||
|
testWidgets('an unratable tracker keeps its row so the sheet cannot resize under the user', (tester) async {
|
||||||
|
// The score load resolves asynchronously and finds every tracker unratable
|
||||||
|
// for this item. Removing those rows would shorten the sheet hundreds of ms
|
||||||
|
// after it opens, and because sheets are bottom-anchored that slides the
|
||||||
|
// rows above them — live rating controls — out from under the user.
|
||||||
|
await _seedAllTrackerSessions(_profileUuid);
|
||||||
|
await _pumpRatingSheet(
|
||||||
|
tester,
|
||||||
|
viewport: const Size(1280, 800),
|
||||||
|
serverClient: _StubServerClient(ServerCapabilities.plex),
|
||||||
|
profileUuid: _profileUuid,
|
||||||
|
item: testMediaItem(id: 'ep-1', kind: MediaKind.episode, serverId: 'server-1', serverName: 'Living Room'),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Post-frame resolve has already run and reported every tracker unavailable.
|
||||||
|
expect(find.text(t.rateSheet.notAvailable), findsNWidgets(5));
|
||||||
|
expect(_listContentExtent(tester), _tallListExtent, reason: 'no row may be dropped');
|
||||||
|
|
||||||
|
final heightAfterResolve = tester.getSize(_sheetFinder).height;
|
||||||
|
final serverRowTop = tester.getRect(find.text(t.rateSheet.server)).top;
|
||||||
|
|
||||||
|
// Pump well past any further async settling: nothing may move.
|
||||||
|
await tester.pump(const Duration(seconds: 2));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(tester.getSize(_sheetFinder).height, heightAfterResolve);
|
||||||
|
expect(tester.getRect(find.text(t.rateSheet.server)).top, serverRowTop);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sheet body itself — the direct child of the host's capping
|
||||||
|
/// `ConstrainedBox`, so its height *is* the clamped sheet height.
|
||||||
|
final Finder _sheetFinder = find.byType(RatingBottomSheet);
|
||||||
|
|
||||||
|
/// Six 54px rows (48px min-height row + 6px gap) plus the list's 4/12 padding.
|
||||||
|
const double _tallListExtent = 6 * 54 + 16;
|
||||||
|
|
||||||
|
double _listContentExtent(WidgetTester tester) {
|
||||||
|
final position = tester
|
||||||
|
.state<ScrollableState>(find.descendant(of: _sheetFinder, matching: find.byType(Scrollable)))
|
||||||
|
.position;
|
||||||
|
return position.maxScrollExtent + position.viewportDimension;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _profileUuid = 'profile-1';
|
||||||
|
|
||||||
|
Future<void> _pumpRatingSheet(
|
||||||
|
WidgetTester tester, {
|
||||||
|
required Size viewport,
|
||||||
|
MediaServerClient? serverClient,
|
||||||
|
String? profileUuid,
|
||||||
|
MediaItem? item,
|
||||||
|
}) async {
|
||||||
|
tester.view.physicalSize = viewport;
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
// Pin the two platform gates the host's cap consults, so the 0.75/720 math
|
||||||
|
// is the same on every machine running the suite.
|
||||||
|
PlatformDetector.debugSetIsDesktopOSOverride(true);
|
||||||
|
addTearDown(() => PlatformDetector.debugSetIsDesktopOSOverride(null));
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(false);
|
||||||
|
addTearDown(() => TvDetectionService.debugSetAppleTVOverride(null));
|
||||||
|
|
||||||
|
final trackers = TrackersProvider(httpClientFactory: () => FakeHttpClient(200, const <int>[]));
|
||||||
|
addTearDown(trackers.dispose);
|
||||||
|
if (profileUuid != null) {
|
||||||
|
await trackers.onActiveProfileChanged(profileUuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
final resolvedItem = item ?? testMediaItem(id: 'item-1', serverId: 'server-1', serverName: 'Living Room');
|
||||||
|
|
||||||
|
await tester.pumpWidget(
|
||||||
|
ChangeNotifierProvider<TrackersProvider>.value(
|
||||||
|
value: trackers,
|
||||||
|
child: MaterialApp(
|
||||||
|
theme: ThemeData(platform: TargetPlatform.macOS, extensions: const [testMonoTokens]),
|
||||||
|
home: OverlaySheetHost(
|
||||||
|
child: Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) => ElevatedButton(
|
||||||
|
onPressed: () => OverlaySheetController.of(context).show<void>(
|
||||||
|
builder: (_) => RatingBottomSheet(item: resolvedItem, serverClient: serverClient),
|
||||||
|
),
|
||||||
|
child: const Text('Open'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _seedAllTrackerSessions(String uuid) async {
|
||||||
|
final now = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||||
|
for (final service in TrackerService.values) {
|
||||||
|
await trackerAccountStore(service).save(
|
||||||
|
uuid,
|
||||||
|
TrackerSession(
|
||||||
|
accessToken: '${service.name}-at',
|
||||||
|
refreshToken: '${service.name}-rt',
|
||||||
|
expiresAt: now + 3600,
|
||||||
|
createdAt: now,
|
||||||
|
username: 'tester',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
BaseSharedPreferencesService.resetForTesting();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The tracker singletons outlive a test; unbind the seeded sessions so the
|
||||||
|
/// next case starts disconnected.
|
||||||
|
void _resetTrackerBindings() {
|
||||||
|
MalTracker.instance.rebindSession(null, onSessionInvalidated: () {});
|
||||||
|
AnilistTracker.instance.rebindSession(null, onSessionInvalidated: () {});
|
||||||
|
SimklTracker.instance.rebindSession(null, onSessionInvalidated: () {});
|
||||||
|
TraktTracker.instance.rebindSession(null, onSessionInvalidated: () {});
|
||||||
|
MdblistTracker.instance.rebindSession(null, onSessionInvalidated: () {});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Narrow stand-in for the media server: while laying out, the sheet reads only
|
||||||
|
/// [capabilities] (which decides whether the server row renders at all),
|
||||||
|
/// [backend], [serverName], and [serverId].
|
||||||
|
class _StubServerClient implements MediaServerClient {
|
||||||
|
_StubServerClient(this.capabilities);
|
||||||
|
|
||||||
|
@override
|
||||||
|
final ServerCapabilities capabilities;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ServerId get serverId => ServerId('server-1');
|
||||||
|
|
||||||
|
@override
|
||||||
|
String? get serverName => 'Living Room';
|
||||||
|
|
||||||
|
@override
|
||||||
|
MediaBackend get backend => MediaBackend.plex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
|
||||||
|
}
|
||||||
@@ -1,7 +1,12 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:plezy/i18n/strings.g.dart';
|
||||||
|
import 'package:plezy/utils/platform_detector.dart';
|
||||||
|
import 'package:plezy/widgets/overlay_sheet.dart';
|
||||||
import 'package:plezy/widgets/video_controls/sheets/subtitle_search_sheet.dart';
|
import 'package:plezy/widgets/video_controls/sheets/subtitle_search_sheet.dart';
|
||||||
|
|
||||||
|
import '../test_helpers/theme.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
group('resolveSubtitleSearchLanguageCode', () {
|
group('resolveSubtitleSearchLanguageCode', () {
|
||||||
test('prefers saved language over system language', () {
|
test('prefers saved language over system language', () {
|
||||||
@@ -21,4 +26,72 @@ void main() {
|
|||||||
expect(resolveSubtitleSearchLanguageCode(savedLanguageCode: 'zz', systemLocale: const Locale('xx')), 'en');
|
expect(resolveSubtitleSearchLanguageCode(savedLanguageCode: 'zz', systemLocale: const Locale('xx')), 'en');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('sheet geometry', () {
|
||||||
|
setUp(() {
|
||||||
|
LocaleSettings.setLocaleSync(AppLocale.en);
|
||||||
|
// The 720 assertion below is the desktop-window ceiling, so pin both
|
||||||
|
// gates it reads rather than inheriting the host OS.
|
||||||
|
PlatformDetector.debugSetIsDesktopOSOverride(true);
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
tearDown(() {
|
||||||
|
PlatformDetector.debugSetIsDesktopOSOverride(null);
|
||||||
|
TvDetectionService.debugSetAppleTVOverride(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('search body keeps a stable height so the focused field cannot slide', (tester) async {
|
||||||
|
tester.view.physicalSize = const Size(1280, 1400);
|
||||||
|
tester.view.devicePixelRatio = 1.0;
|
||||||
|
addTearDown(tester.view.reset);
|
||||||
|
|
||||||
|
await _pumpSearchSheet(tester);
|
||||||
|
|
||||||
|
// No MultiServerProvider, so the search resolves to no client and the
|
||||||
|
// results area is empty. A shrink-wrapping body would collapse here.
|
||||||
|
final emptyHeight = _sheetHeight(tester);
|
||||||
|
final fieldTop = tester.getTopLeft(find.byType(TextField)).dy;
|
||||||
|
expect(emptyHeight, 720, reason: 'search surface fills the windowed height ceiling');
|
||||||
|
|
||||||
|
// Switching to the language picker and filtering it down to a couple of
|
||||||
|
// matches must not move the geometry either.
|
||||||
|
await tester.tap(find.text('English'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(_sheetHeight(tester), emptyHeight);
|
||||||
|
|
||||||
|
await tester.enterText(find.byType(TextField), 'zulu');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.text('Zulu'), findsOneWidget);
|
||||||
|
expect(_sheetHeight(tester), emptyHeight, reason: 'per-keystroke match count must not resize the sheet');
|
||||||
|
expect(tester.getTopLeft(find.byType(TextField)).dy, fieldTop);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Height of the sheet box the host lays out, i.e. what the user sees.
|
||||||
|
double _sheetHeight(WidgetTester tester) {
|
||||||
|
return tester.getSize(find.descendant(of: find.byType(OverlaySheetHost), matching: find.byType(AnimatedSize))).height;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _pumpSearchSheet(WidgetTester tester) async {
|
||||||
|
await tester.pumpWidget(
|
||||||
|
MaterialApp(
|
||||||
|
theme: ThemeData(extensions: const [testMonoTokens]),
|
||||||
|
home: OverlaySheetHost(
|
||||||
|
child: Builder(
|
||||||
|
builder: (context) => Scaffold(
|
||||||
|
body: ElevatedButton(
|
||||||
|
onPressed: () => OverlaySheetController.of(context).show<void>(
|
||||||
|
builder: (_) => const SubtitleSearchSheet(ratingKey: '1', serverId: 'server'),
|
||||||
|
),
|
||||||
|
child: const Text('Open'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await tester.tap(find.text('Open'));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user