From ec61b056231d82230e2876ccf07959bc7f830377 Mon Sep 17 00:00:00 2001 From: l3gitpanda Date: Fri, 10 Jul 2026 15:48:51 -0700 Subject: [PATCH 1/3] feat(livetv): add guide search with jump-to-channel/program Search icon on the guide tab opens a sheet that filters channels and the next 24h of programs in memory; selecting a result closes the sheet, shifts the guide window if needed, scrolls to the channel row and lands d-pad focus on the match. While the sheet is open it takes over the companion-remote search callback (save/restore, same idiom as the video player overrides) so queries typed on the phone land in guide search with the TV keyboard dismissed. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UnEMtLJYbRim4k7YLCwvyZ --- lib/i18n/bg.i18n.json | 5 + lib/i18n/da.i18n.json | 5 + lib/i18n/de.i18n.json | 5 + lib/i18n/en.i18n.json | 5 + lib/i18n/es.i18n.json | 5 + lib/i18n/fr.i18n.json | 5 + lib/i18n/it.i18n.json | 5 + lib/i18n/ja.i18n.json | 5 + lib/i18n/ko.i18n.json | 5 + lib/i18n/nb.i18n.json | 5 + lib/i18n/nl.i18n.json | 5 + lib/i18n/pl.i18n.json | 5 + lib/i18n/pt.i18n.json | 5 + lib/i18n/ru.i18n.json | 5 + lib/i18n/strings.g.dart | 2 +- lib/i18n/strings_bg.g.dart | 14 +- lib/i18n/strings_da.g.dart | 14 +- lib/i18n/strings_de.g.dart | 14 +- lib/i18n/strings_en.g.dart | 24 +- lib/i18n/strings_es.g.dart | 14 +- lib/i18n/strings_fr.g.dart | 14 +- lib/i18n/strings_it.g.dart | 14 +- lib/i18n/strings_ja.g.dart | 14 +- lib/i18n/strings_ko.g.dart | 14 +- lib/i18n/strings_nb.g.dart | 14 +- lib/i18n/strings_nl.g.dart | 14 +- lib/i18n/strings_pl.g.dart | 14 +- lib/i18n/strings_pt.g.dart | 14 +- lib/i18n/strings_ru.g.dart | 14 +- lib/i18n/strings_sv.g.dart | 14 +- lib/i18n/strings_zh.g.dart | 14 +- lib/i18n/sv.i18n.json | 5 + lib/i18n/zh.i18n.json | 5 + lib/screens/livetv/guide_search_sheet.dart | 317 +++++++++++++++++++++ lib/screens/livetv/live_tv_screen.dart | 36 +++ lib/screens/livetv/tabs/guide_tab.dart | 69 +++++ 36 files changed, 705 insertions(+), 33 deletions(-) create mode 100644 lib/screens/livetv/guide_search_sheet.dart diff --git a/lib/i18n/bg.i18n.json b/lib/i18n/bg.i18n.json index f12902425..f867a7c2c 100644 --- a/lib/i18n/bg.i18n.json +++ b/lib/i18n/bg.i18n.json @@ -904,6 +904,11 @@ "unknownChannel": "Неизвестен канал", "live": "НА ЖИВО", "reloadGuide": "Презареди ТВ програмата", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Сега", "today": "Днес", "tomorrow": "Утре", diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index cf681e741..287b5dd4a 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -904,6 +904,11 @@ "unknownChannel": "Ukendt kanal", "live": "LIVE", "reloadGuide": "Genindlæs guide", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Nu", "today": "I dag", "tomorrow": "I morgen", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 8f8e5caed..7231d7d4b 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -904,6 +904,11 @@ "unknownChannel": "Unbekannter Kanal", "live": "LIVE", "reloadGuide": "Programmführer neu laden", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Jetzt", "today": "Heute", "tomorrow": "Morgen", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index e74b8fbd6..ae5c15845 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -904,6 +904,11 @@ "unknownChannel": "Unknown channel", "live": "LIVE", "reloadGuide": "Reload Guide", + "searchGuide": "Search Guide", + "searchHint": "Search channels & programs", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "Channels", + "programsSection": "Programs", "now": "Now", "today": "Today", "tomorrow": "Tomorrow", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index d02f375f1..6c19c5c2e 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -904,6 +904,11 @@ "unknownChannel": "Canal desconocido", "live": "EN VIVO", "reloadGuide": "Recargar guía", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Ahora", "today": "Hoy", "tomorrow": "Mañana", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 9462563a4..f0f3e8bc0 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -904,6 +904,11 @@ "unknownChannel": "Chaîne inconnue", "live": "EN DIRECT", "reloadGuide": "Recharger le guide", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Maintenant", "today": "Aujourd'hui", "tomorrow": "Demain", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 3fef8d8cd..5cf6f989e 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -904,6 +904,11 @@ "unknownChannel": "Canale sconosciuto", "live": "IN DIRETTA", "reloadGuide": "Ricarica guida", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Ora", "today": "Oggi", "tomorrow": "Domani", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index 541a667ae..6d304ce2a 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -903,6 +903,11 @@ "unknownChannel": "不明なチャンネル", "live": "ライブ", "reloadGuide": "番組表を再読込", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "現在", "today": "今日", "tomorrow": "明日", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 2e37f6b61..f68c30bc6 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -903,6 +903,11 @@ "unknownChannel": "알 수 없는 채널", "live": "실시간", "reloadGuide": "편성표 새로고침", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "지금", "today": "오늘", "tomorrow": "내일", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index 223f5db77..930cb30da 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -904,6 +904,11 @@ "unknownChannel": "Ukjent kanal", "live": "DIREKTE", "reloadGuide": "Last inn programguide på nytt", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Nå", "today": "I dag", "tomorrow": "I morgen", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 607558bd9..04cf88949 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -904,6 +904,11 @@ "unknownChannel": "Onbekend kanaal", "live": "LIVE", "reloadGuide": "Gids herladen", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Nu", "today": "Vandaag", "tomorrow": "Morgen", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index 773104273..f8a4702ca 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -906,6 +906,11 @@ "unknownChannel": "Nieznany kanał", "live": "NA ŻYWO", "reloadGuide": "Odśwież przewodnik", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Teraz", "today": "Dzisiaj", "tomorrow": "Jutro", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index e3f325aa4..216d55c67 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -904,6 +904,11 @@ "unknownChannel": "Canal desconhecido", "live": "AO VIVO", "reloadGuide": "Recarregar Guia", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Agora", "today": "Hoje", "tomorrow": "Amanhã", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index 661048401..87a6bf82d 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -906,6 +906,11 @@ "unknownChannel": "Неизвестный канал", "live": "ЭФИР", "reloadGuide": "Перезагрузить программу", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Сейчас", "today": "Сегодня", "tomorrow": "Завтра", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 23300cee2..26ee57dab 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,7 +4,7 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 16 -/// Strings: 22514 (1407 per locale) +/// Strings: 22594 (1412 per locale) // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_bg.g.dart b/lib/i18n/strings_bg.g.dart index f7e7b7b04..6e146d504 100644 --- a/lib/i18n/strings_bg.g.dart +++ b/lib/i18n/strings_bg.g.dart @@ -1112,6 +1112,11 @@ class _TranslationsLiveTvBg extends TranslationsLiveTvEn { @override String get unknownChannel => 'Неизвестен канал'; @override String get live => 'НА ЖИВО'; @override String get reloadGuide => 'Презареди ТВ програмата'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Сега'; @override String get today => 'Днес'; @override String get tomorrow => 'Утре'; @@ -2926,6 +2931,11 @@ extension on TranslationsBg { 'liveTv.unknownChannel' => 'Неизвестен канал', 'liveTv.live' => 'НА ЖИВО', 'liveTv.reloadGuide' => 'Презареди ТВ програмата', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Сега', 'liveTv.today' => 'Днес', 'liveTv.tomorrow' => 'Утре', @@ -3121,13 +3131,13 @@ extension on TranslationsBg { 'downloads.deleteDownload' => 'Изтрий изтегляне', 'downloads.retryDownload' => 'Опитай изтеглянето отново', 'downloads.downloadQueued' => 'Изтеглянето е добавено в опашката', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Изтеглянето е възобновено', 'downloads.serverErrorBitrate' => 'Грешка на сървъра: файлът може да надвишава лимита за отдалечен битрейт', 'downloads.episodesQueued' => ({required Object count}) => '${count} епизода са добавени в опашката за изтегляне', 'downloads.downloadDeleted' => 'Изтеглянето е изтрито', 'downloads.deleteConfirm' => ({required Object title}) => 'Да се изтрие ли "${title}" от това устройство?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Отменено изтегляне', 'downloads.cancelledDownloadMessage' => 'Това изтегляне беше отменено. Какво искате да направите?', 'downloads.allEpisodesAlreadyDownloaded' => 'Всички епизоди вече са изтеглени', diff --git a/lib/i18n/strings_da.g.dart b/lib/i18n/strings_da.g.dart index f16c7fda8..bc2c3aaee 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -1112,6 +1112,11 @@ class _TranslationsLiveTvDa extends TranslationsLiveTvEn { @override String get unknownChannel => 'Ukendt kanal'; @override String get live => 'LIVE'; @override String get reloadGuide => 'Genindlæs guide'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Nu'; @override String get today => 'I dag'; @override String get tomorrow => 'I morgen'; @@ -2926,6 +2931,11 @@ extension on TranslationsDa { 'liveTv.unknownChannel' => 'Ukendt kanal', 'liveTv.live' => 'LIVE', 'liveTv.reloadGuide' => 'Genindlæs guide', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Nu', 'liveTv.today' => 'I dag', 'liveTv.tomorrow' => 'I morgen', @@ -3121,13 +3131,13 @@ extension on TranslationsDa { 'downloads.deleteDownload' => 'Slet download', 'downloads.retryDownload' => 'Prøv download igen', 'downloads.downloadQueued' => 'Download i kø', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Download genoptaget', 'downloads.serverErrorBitrate' => 'Serverfejl: filen overskrider muligvis grænsen for ekstern bitrate', 'downloads.episodesQueued' => ({required Object count}) => '${count} episoder i downloadkø', 'downloads.downloadDeleted' => 'Download slettet', 'downloads.deleteConfirm' => ({required Object title}) => 'Slet "${title}" fra denne enhed?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Annulleret download', 'downloads.cancelledDownloadMessage' => 'Denne download blev annulleret. Hvad vil du gøre?', 'downloads.allEpisodesAlreadyDownloaded' => 'Alle episoder er allerede downloadet', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index a1dffa701..20acb993a 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -1112,6 +1112,11 @@ class _TranslationsLiveTvDe extends TranslationsLiveTvEn { @override String get unknownChannel => 'Unbekannter Kanal'; @override String get live => 'LIVE'; @override String get reloadGuide => 'Programmführer neu laden'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Jetzt'; @override String get today => 'Heute'; @override String get tomorrow => 'Morgen'; @@ -2926,6 +2931,11 @@ extension on TranslationsDe { 'liveTv.unknownChannel' => 'Unbekannter Kanal', 'liveTv.live' => 'LIVE', 'liveTv.reloadGuide' => 'Programmführer neu laden', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Jetzt', 'liveTv.today' => 'Heute', 'liveTv.tomorrow' => 'Morgen', @@ -3121,13 +3131,13 @@ extension on TranslationsDe { 'downloads.deleteDownload' => 'Download löschen', 'downloads.retryDownload' => 'Download wiederholen', 'downloads.downloadQueued' => 'Download in Warteschlange', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Download fortgesetzt', 'downloads.serverErrorBitrate' => 'Serverfehler: Datei überschreitet möglicherweise das Remote-Bitrate-Limit', 'downloads.episodesQueued' => ({required Object count}) => '${count} Episoden zum Download hinzugefügt', 'downloads.downloadDeleted' => 'Download gelöscht', 'downloads.deleteConfirm' => ({required Object title}) => '"${title}" von diesem Gerät löschen?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Abgebrochener Download', 'downloads.cancelledDownloadMessage' => 'Dieser Download wurde abgebrochen. Was möchtest du tun?', 'downloads.allEpisodesAlreadyDownloaded' => 'Alle Episoden sind bereits heruntergeladen', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 496c16136..ffd389e04 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -2588,6 +2588,21 @@ class TranslationsLiveTvEn { /// en: 'Reload Guide' String get reloadGuide => 'Reload Guide'; + /// en: 'Search Guide' + String get searchGuide => 'Search Guide'; + + /// en: 'Search channels & programs' + String get searchHint => 'Search channels & programs'; + + /// en: 'No matches for "${query}"' + String searchNoResults({required Object query}) => 'No matches for "${query}"'; + + /// en: 'Channels' + String get channelsSection => 'Channels'; + + /// en: 'Programs' + String get programsSection => 'Programs'; + /// en: 'Now' String get now => 'Now'; @@ -5743,6 +5758,11 @@ extension on Translations { 'liveTv.unknownChannel' => 'Unknown channel', 'liveTv.live' => 'LIVE', 'liveTv.reloadGuide' => 'Reload Guide', + 'liveTv.searchGuide' => 'Search Guide', + 'liveTv.searchHint' => 'Search channels & programs', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => 'Channels', + 'liveTv.programsSection' => 'Programs', 'liveTv.now' => 'Now', 'liveTv.today' => 'Today', 'liveTv.tomorrow' => 'Tomorrow', @@ -5938,13 +5958,13 @@ extension on Translations { 'downloads.deleteDownload' => 'Delete download', 'downloads.retryDownload' => 'Retry download', 'downloads.downloadQueued' => 'Download queued', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Download resumed', 'downloads.serverErrorBitrate' => 'Server error: file may exceed the remote bitrate limit', 'downloads.episodesQueued' => ({required Object count}) => '${count} episodes queued for download', 'downloads.downloadDeleted' => 'Download deleted', 'downloads.deleteConfirm' => ({required Object title}) => 'Delete "${title}" from this device?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Cancelled Download', 'downloads.cancelledDownloadMessage' => 'This download was cancelled. What would you like to do?', 'downloads.allEpisodesAlreadyDownloaded' => 'All episodes already downloaded', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index d243c1bb2..6fd33b9af 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -1112,6 +1112,11 @@ class _TranslationsLiveTvEs extends TranslationsLiveTvEn { @override String get unknownChannel => 'Canal desconocido'; @override String get live => 'EN VIVO'; @override String get reloadGuide => 'Recargar guía'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Ahora'; @override String get today => 'Hoy'; @override String get tomorrow => 'Mañana'; @@ -2926,6 +2931,11 @@ extension on TranslationsEs { 'liveTv.unknownChannel' => 'Canal desconocido', 'liveTv.live' => 'EN VIVO', 'liveTv.reloadGuide' => 'Recargar guía', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Ahora', 'liveTv.today' => 'Hoy', 'liveTv.tomorrow' => 'Mañana', @@ -3121,13 +3131,13 @@ extension on TranslationsEs { 'downloads.deleteDownload' => 'Eliminar descarga', 'downloads.retryDownload' => 'Reintentar descarga', 'downloads.downloadQueued' => 'Descarga en cola', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Descarga reanudada', 'downloads.serverErrorBitrate' => 'Error del servidor: el archivo puede superar el límite de bitrate remoto', 'downloads.episodesQueued' => ({required Object count}) => '${count} episodios en cola para descargar', 'downloads.downloadDeleted' => 'Descarga eliminada', 'downloads.deleteConfirm' => ({required Object title}) => '¿Eliminar "${title}" de este dispositivo?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Descarga cancelada', 'downloads.cancelledDownloadMessage' => 'Esta descarga se canceló. ¿Qué quieres hacer?', 'downloads.allEpisodesAlreadyDownloaded' => 'Todos los episodios ya están descargados', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 4b0340cc5..fa8833ba9 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -1112,6 +1112,11 @@ class _TranslationsLiveTvFr extends TranslationsLiveTvEn { @override String get unknownChannel => 'Chaîne inconnue'; @override String get live => 'EN DIRECT'; @override String get reloadGuide => 'Recharger le guide'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Maintenant'; @override String get today => 'Aujourd\'hui'; @override String get tomorrow => 'Demain'; @@ -2926,6 +2931,11 @@ extension on TranslationsFr { 'liveTv.unknownChannel' => 'Chaîne inconnue', 'liveTv.live' => 'EN DIRECT', 'liveTv.reloadGuide' => 'Recharger le guide', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Maintenant', 'liveTv.today' => 'Aujourd\'hui', 'liveTv.tomorrow' => 'Demain', @@ -3121,13 +3131,13 @@ extension on TranslationsFr { 'downloads.deleteDownload' => 'Supprimer le téléchargement', 'downloads.retryDownload' => 'Réessayer le téléchargement', 'downloads.downloadQueued' => 'Téléchargement en attente', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Téléchargement repris', 'downloads.serverErrorBitrate' => 'Erreur serveur : le fichier peut dépasser la limite de bitrate distant', 'downloads.episodesQueued' => ({required Object count}) => '${count} épisodes en attente de téléchargement', 'downloads.downloadDeleted' => 'Télécharger supprimé', 'downloads.deleteConfirm' => ({required Object title}) => 'Supprimer "${title}" de cet appareil ?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Téléchargement annulé', 'downloads.cancelledDownloadMessage' => 'Ce téléchargement a été annulé. Que voulez-vous faire ?', 'downloads.allEpisodesAlreadyDownloaded' => 'Tous les épisodes sont déjà téléchargés', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 872f0900f..a3f56fb5a 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -1112,6 +1112,11 @@ class _TranslationsLiveTvIt extends TranslationsLiveTvEn { @override String get unknownChannel => 'Canale sconosciuto'; @override String get live => 'IN DIRETTA'; @override String get reloadGuide => 'Ricarica guida'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Ora'; @override String get today => 'Oggi'; @override String get tomorrow => 'Domani'; @@ -2926,6 +2931,11 @@ extension on TranslationsIt { 'liveTv.unknownChannel' => 'Canale sconosciuto', 'liveTv.live' => 'IN DIRETTA', 'liveTv.reloadGuide' => 'Ricarica guida', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Ora', 'liveTv.today' => 'Oggi', 'liveTv.tomorrow' => 'Domani', @@ -3121,13 +3131,13 @@ extension on TranslationsIt { 'downloads.deleteDownload' => 'Elimina download', 'downloads.retryDownload' => 'Riprova download', 'downloads.downloadQueued' => 'Download in coda', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Download ripreso', 'downloads.serverErrorBitrate' => 'Errore server: il file può superare il limite di bitrate remoto', 'downloads.episodesQueued' => ({required Object count}) => '${count} episodi in coda per il download', 'downloads.downloadDeleted' => 'Download eliminato', 'downloads.deleteConfirm' => ({required Object title}) => 'Eliminare "${title}" da questo dispositivo?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Download annullato', 'downloads.cancelledDownloadMessage' => 'Questo download è stato annullato. Cosa vuoi fare?', 'downloads.allEpisodesAlreadyDownloaded' => 'Tutti gli episodi sono già stati scaricati', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index 0100204ab..1f235e8c6 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -1111,6 +1111,11 @@ class _TranslationsLiveTvJa extends TranslationsLiveTvEn { @override String get unknownChannel => '不明なチャンネル'; @override String get live => 'ライブ'; @override String get reloadGuide => '番組表を再読込'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => '現在'; @override String get today => '今日'; @override String get tomorrow => '明日'; @@ -2924,6 +2929,11 @@ extension on TranslationsJa { 'liveTv.unknownChannel' => '不明なチャンネル', 'liveTv.live' => 'ライブ', 'liveTv.reloadGuide' => '番組表を再読込', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => '現在', 'liveTv.today' => '今日', 'liveTv.tomorrow' => '明日', @@ -3119,13 +3129,13 @@ extension on TranslationsJa { 'downloads.deleteDownload' => 'ダウンロードを削除', 'downloads.retryDownload' => 'ダウンロードを再試行', 'downloads.downloadQueued' => 'ダウンロードをキューに追加しました', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'ダウンロードを再開しました', 'downloads.serverErrorBitrate' => 'サーバーエラー: ファイルがリモートビットレート制限を超えている可能性があります', 'downloads.episodesQueued' => ({required Object count}) => '${count}エピソードをダウンロードキューに追加しました', 'downloads.downloadDeleted' => 'ダウンロードを削除しました', 'downloads.deleteConfirm' => ({required Object title}) => 'このデバイスから「${title}」を削除しますか?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'キャンセルされたダウンロード', 'downloads.cancelledDownloadMessage' => 'このダウンロードはキャンセルされました。どうしますか?', 'downloads.allEpisodesAlreadyDownloaded' => 'すべてのエピソードはすでにダウンロード済みです', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 4b56b0386..b55f25c51 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -1111,6 +1111,11 @@ class _TranslationsLiveTvKo extends TranslationsLiveTvEn { @override String get unknownChannel => '알 수 없는 채널'; @override String get live => '실시간'; @override String get reloadGuide => '편성표 새로고침'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => '지금'; @override String get today => '오늘'; @override String get tomorrow => '내일'; @@ -2924,6 +2929,11 @@ extension on TranslationsKo { 'liveTv.unknownChannel' => '알 수 없는 채널', 'liveTv.live' => '실시간', 'liveTv.reloadGuide' => '편성표 새로고침', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => '지금', 'liveTv.today' => '오늘', 'liveTv.tomorrow' => '내일', @@ -3119,13 +3129,13 @@ extension on TranslationsKo { 'downloads.deleteDownload' => '다운로드 삭제', 'downloads.retryDownload' => '다운로드 재시도', 'downloads.downloadQueued' => '다운로드 대기 중', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => '다운로드를 재개했습니다', 'downloads.serverErrorBitrate' => '서버 오류: 파일이 원격 비트레이트 제한을 초과할 수 있습니다', 'downloads.episodesQueued' => ({required Object count}) => '${count} 에피소드가 다운로드 대기열에 추가 되었습니다', 'downloads.downloadDeleted' => '다운로드 삭제됨', 'downloads.deleteConfirm' => ({required Object title}) => '이 기기에서 "${title}"을(를) 삭제할까요?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => '취소된 다운로드', 'downloads.cancelledDownloadMessage' => '이 다운로드가 취소되었습니다. 어떻게 하시겠습니까?', 'downloads.allEpisodesAlreadyDownloaded' => '모든 에피소드가 이미 다운로드되었습니다', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index 871cc55f8..560d459fb 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -1112,6 +1112,11 @@ class _TranslationsLiveTvNb extends TranslationsLiveTvEn { @override String get unknownChannel => 'Ukjent kanal'; @override String get live => 'DIREKTE'; @override String get reloadGuide => 'Last inn programguide på nytt'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Nå'; @override String get today => 'I dag'; @override String get tomorrow => 'I morgen'; @@ -2926,6 +2931,11 @@ extension on TranslationsNb { 'liveTv.unknownChannel' => 'Ukjent kanal', 'liveTv.live' => 'DIREKTE', 'liveTv.reloadGuide' => 'Last inn programguide på nytt', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Nå', 'liveTv.today' => 'I dag', 'liveTv.tomorrow' => 'I morgen', @@ -3121,13 +3131,13 @@ extension on TranslationsNb { 'downloads.deleteDownload' => 'Slett nedlasting', 'downloads.retryDownload' => 'Prøv nedlasting på nytt', 'downloads.downloadQueued' => 'Nedlasting i kø', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Nedlasting gjenopptatt', 'downloads.serverErrorBitrate' => 'Serverfeil: filen kan overskride grensen for ekstern bitrate', 'downloads.episodesQueued' => ({required Object count}) => '${count} episoder i nedlastingskø', 'downloads.downloadDeleted' => 'Nedlasting slettet', 'downloads.deleteConfirm' => ({required Object title}) => 'Slette "${title}" fra denne enheten?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Avbrutt nedlasting', 'downloads.cancelledDownloadMessage' => 'Denne nedlastingen ble avbrutt. Hva vil du gjøre?', 'downloads.allEpisodesAlreadyDownloaded' => 'Alle episoder er allerede lastet ned', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index a5f605235..52b93de69 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -1112,6 +1112,11 @@ class _TranslationsLiveTvNl extends TranslationsLiveTvEn { @override String get unknownChannel => 'Onbekend kanaal'; @override String get live => 'LIVE'; @override String get reloadGuide => 'Gids herladen'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Nu'; @override String get today => 'Vandaag'; @override String get tomorrow => 'Morgen'; @@ -2926,6 +2931,11 @@ extension on TranslationsNl { 'liveTv.unknownChannel' => 'Onbekend kanaal', 'liveTv.live' => 'LIVE', 'liveTv.reloadGuide' => 'Gids herladen', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Nu', 'liveTv.today' => 'Vandaag', 'liveTv.tomorrow' => 'Morgen', @@ -3121,13 +3131,13 @@ extension on TranslationsNl { 'downloads.deleteDownload' => 'Download verwijderen', 'downloads.retryDownload' => 'Download opnieuw proberen', 'downloads.downloadQueued' => 'Download in wachtrij', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Download hervat', 'downloads.serverErrorBitrate' => 'Serverfout: bestand overschrijdt mogelijk de externe bitrate-limiet', 'downloads.episodesQueued' => ({required Object count}) => '${count} afleveringen in wachtrij voor download', 'downloads.downloadDeleted' => 'Download verwijderd', 'downloads.deleteConfirm' => ({required Object title}) => '"${title}" van dit apparaat verwijderen?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Geannuleerde download', 'downloads.cancelledDownloadMessage' => 'Deze download is geannuleerd. Wat wil je doen?', 'downloads.allEpisodesAlreadyDownloaded' => 'Alle afleveringen zijn al gedownload', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index f5feb8407..9561f7138 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -1114,6 +1114,11 @@ class _TranslationsLiveTvPl extends TranslationsLiveTvEn { @override String get unknownChannel => 'Nieznany kanał'; @override String get live => 'NA ŻYWO'; @override String get reloadGuide => 'Odśwież przewodnik'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Teraz'; @override String get today => 'Dzisiaj'; @override String get tomorrow => 'Jutro'; @@ -2930,6 +2935,11 @@ extension on TranslationsPl { 'liveTv.unknownChannel' => 'Nieznany kanał', 'liveTv.live' => 'NA ŻYWO', 'liveTv.reloadGuide' => 'Odśwież przewodnik', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Teraz', 'liveTv.today' => 'Dzisiaj', 'liveTv.tomorrow' => 'Jutro', @@ -3125,13 +3135,13 @@ extension on TranslationsPl { 'downloads.deleteDownload' => 'Usuń pobranie', 'downloads.retryDownload' => 'Ponów pobieranie', 'downloads.downloadQueued' => 'Pobranie w kolejce', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Pobieranie wznowione', 'downloads.serverErrorBitrate' => 'Błąd serwera: plik może przekraczać zdalny limit bitrate', 'downloads.episodesQueued' => ({required Object count}) => '${count} odcinków w kolejce pobierania', 'downloads.downloadDeleted' => 'Pobranie usunięte', 'downloads.deleteConfirm' => ({required Object title}) => 'Usunąć "${title}" z tego urządzenia?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Anulowane pobieranie', 'downloads.cancelledDownloadMessage' => 'To pobieranie zostało anulowane. Co chcesz zrobić?', 'downloads.allEpisodesAlreadyDownloaded' => 'Wszystkie odcinki są już pobrane', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index e7fa7395c..d475bd1ee 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -1112,6 +1112,11 @@ class _TranslationsLiveTvPt extends TranslationsLiveTvEn { @override String get unknownChannel => 'Canal desconhecido'; @override String get live => 'AO VIVO'; @override String get reloadGuide => 'Recarregar Guia'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Agora'; @override String get today => 'Hoje'; @override String get tomorrow => 'Amanhã'; @@ -2926,6 +2931,11 @@ extension on TranslationsPt { 'liveTv.unknownChannel' => 'Canal desconhecido', 'liveTv.live' => 'AO VIVO', 'liveTv.reloadGuide' => 'Recarregar Guia', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Agora', 'liveTv.today' => 'Hoje', 'liveTv.tomorrow' => 'Amanhã', @@ -3121,13 +3131,13 @@ extension on TranslationsPt { 'downloads.deleteDownload' => 'Excluir download', 'downloads.retryDownload' => 'Tentar download novamente', 'downloads.downloadQueued' => 'Download na fila', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Download retomado', 'downloads.serverErrorBitrate' => 'Erro do servidor: o arquivo pode exceder o limite remoto de bitrate', 'downloads.episodesQueued' => ({required Object count}) => '${count} episódios na fila de download', 'downloads.downloadDeleted' => 'Download excluído', 'downloads.deleteConfirm' => ({required Object title}) => 'Excluir "${title}" deste dispositivo?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Download cancelado', 'downloads.cancelledDownloadMessage' => 'Este download foi cancelado. O que você deseja fazer?', 'downloads.allEpisodesAlreadyDownloaded' => 'Todos os episódios já foram baixados', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index 92d7fc485..9bc31bd43 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -1114,6 +1114,11 @@ class _TranslationsLiveTvRu extends TranslationsLiveTvEn { @override String get unknownChannel => 'Неизвестный канал'; @override String get live => 'ЭФИР'; @override String get reloadGuide => 'Перезагрузить программу'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Сейчас'; @override String get today => 'Сегодня'; @override String get tomorrow => 'Завтра'; @@ -2930,6 +2935,11 @@ extension on TranslationsRu { 'liveTv.unknownChannel' => 'Неизвестный канал', 'liveTv.live' => 'ЭФИР', 'liveTv.reloadGuide' => 'Перезагрузить программу', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Сейчас', 'liveTv.today' => 'Сегодня', 'liveTv.tomorrow' => 'Завтра', @@ -3125,13 +3135,13 @@ extension on TranslationsRu { 'downloads.deleteDownload' => 'Удалить загрузку', 'downloads.retryDownload' => 'Повторить загрузку', 'downloads.downloadQueued' => 'Загрузка поставлена в очередь', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Загрузка возобновлена', 'downloads.serverErrorBitrate' => 'Ошибка сервера: файл может превышать удаленный лимит bitrate', 'downloads.episodesQueued' => ({required Object count}) => '${count} эпизодов поставлено в очередь загрузки', 'downloads.downloadDeleted' => 'Загрузка удалена', 'downloads.deleteConfirm' => ({required Object title}) => 'Удалить "${title}" с этого устройства?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Загрузка отменена', 'downloads.cancelledDownloadMessage' => 'Эта загрузка была отменена. Что вы хотите сделать?', 'downloads.allEpisodesAlreadyDownloaded' => 'Все эпизоды уже загружены', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 35653441f..4c2a44bf0 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -1112,6 +1112,11 @@ class _TranslationsLiveTvSv extends TranslationsLiveTvEn { @override String get unknownChannel => 'Okänd kanal'; @override String get live => 'LIVE'; @override String get reloadGuide => 'Ladda om programguide'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => 'Nu'; @override String get today => 'Idag'; @override String get tomorrow => 'I morgon'; @@ -2926,6 +2931,11 @@ extension on TranslationsSv { 'liveTv.unknownChannel' => 'Okänd kanal', 'liveTv.live' => 'LIVE', 'liveTv.reloadGuide' => 'Ladda om programguide', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => 'Nu', 'liveTv.today' => 'Idag', 'liveTv.tomorrow' => 'I morgon', @@ -3121,13 +3131,13 @@ extension on TranslationsSv { 'downloads.deleteDownload' => 'Ta bort nedladdning', 'downloads.retryDownload' => 'Försök igen', 'downloads.downloadQueued' => 'Nedladdning köad', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => 'Nedladdning återupptagen', 'downloads.serverErrorBitrate' => 'Serverfel: filen kan överskrida gränsen för fjärrbitrate', 'downloads.episodesQueued' => ({required Object count}) => '${count} avsnitt köade för nedladdning', 'downloads.downloadDeleted' => 'Nedladdning borttagen', 'downloads.deleteConfirm' => ({required Object title}) => 'Ta bort "${title}" från den här enheten?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => 'Avbruten nedladdning', 'downloads.cancelledDownloadMessage' => 'Den här nedladdningen avbröts. Vad vill du göra?', 'downloads.allEpisodesAlreadyDownloaded' => 'Alla avsnitt är redan nedladdade', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 4b00d9aad..002287d16 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -1111,6 +1111,11 @@ class _TranslationsLiveTvZh extends TranslationsLiveTvEn { @override String get unknownChannel => '未知频道'; @override String get live => '直播'; @override String get reloadGuide => '重新加载节目指南'; + @override String get searchGuide => ''; + @override String get searchHint => ''; + @override String searchNoResults({required Object query}) => 'No matches for "${query}"'; + @override String get channelsSection => ''; + @override String get programsSection => ''; @override String get now => '现在'; @override String get today => '今天'; @override String get tomorrow => '明天'; @@ -2924,6 +2929,11 @@ extension on TranslationsZh { 'liveTv.unknownChannel' => '未知频道', 'liveTv.live' => '直播', 'liveTv.reloadGuide' => '重新加载节目指南', + 'liveTv.searchGuide' => '', + 'liveTv.searchHint' => '', + 'liveTv.searchNoResults' => ({required Object query}) => 'No matches for "${query}"', + 'liveTv.channelsSection' => '', + 'liveTv.programsSection' => '', 'liveTv.now' => '现在', 'liveTv.today' => '今天', 'liveTv.tomorrow' => '明天', @@ -3119,13 +3129,13 @@ extension on TranslationsZh { 'downloads.deleteDownload' => '删除下载', 'downloads.retryDownload' => '重试下载', 'downloads.downloadQueued' => '下载已排队', + _ => null, + } ?? switch (path) { 'downloads.downloadResumed' => '下载已继续', 'downloads.serverErrorBitrate' => '服务器错误:文件可能超过远程比特率限制', 'downloads.episodesQueued' => ({required Object count}) => '${count} 集已加入下载队列', 'downloads.downloadDeleted' => '下载已删除', 'downloads.deleteConfirm' => ({required Object title}) => '要从此设备删除“${title}”吗?', - _ => null, - } ?? switch (path) { 'downloads.cancelledDownloadTitle' => '已取消的下载', 'downloads.cancelledDownloadMessage' => '此下载已取消。你想怎么做?', 'downloads.allEpisodesAlreadyDownloaded' => '所有剧集均已下载', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 616f7f319..458dea4ad 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -904,6 +904,11 @@ "unknownChannel": "Okänd kanal", "live": "LIVE", "reloadGuide": "Ladda om programguide", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "Nu", "today": "Idag", "tomorrow": "I morgon", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 212c44b08..ab7e28dee 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -903,6 +903,11 @@ "unknownChannel": "未知频道", "live": "直播", "reloadGuide": "重新加载节目指南", + "searchGuide": "", + "searchHint": "", + "searchNoResults": "No matches for \"${query}\"", + "channelsSection": "", + "programsSection": "", "now": "现在", "today": "今天", "tomorrow": "明天", diff --git a/lib/screens/livetv/guide_search_sheet.dart b/lib/screens/livetv/guide_search_sheet.dart new file mode 100644 index 000000000..7d59f49da --- /dev/null +++ b/lib/screens/livetv/guide_search_sheet.dart @@ -0,0 +1,317 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import 'package:provider/provider.dart'; + +import '../../focus/focusable_text_field.dart'; +import '../../focus/input_mode_tracker.dart'; +import '../../i18n/strings.g.dart'; +import '../../media/ids.dart'; +import '../../mixins/controller_disposer_mixin.dart'; +import '../../models/livetv_channel.dart'; +import '../../models/livetv_program.dart'; +import '../../providers/multi_server_provider.dart'; +import '../../services/companion_remote/companion_remote_receiver.dart'; +import '../../utils/app_logger.dart'; +import '../../utils/formatters.dart'; +import '../../utils/live_tv_matching.dart'; +import '../../widgets/app_icon.dart'; +import '../../widgets/bottom_sheet_header.dart'; +import '../../widgets/focusable_list_tile.dart'; +import '../../widgets/optimized_media_image.dart'; +import '../../widgets/overlay_sheet.dart'; +import '../../widgets/pill_input_decoration.dart'; + +/// Search sheet for the Live TV guide: filters channels and the next 24 hours +/// of programs in memory; selecting a result jumps to it in the guide grid. +class GuideSearchSheet extends StatefulWidget { + final List channels; + final void Function(LiveTvChannel channel) onChannelSelected; + final void Function(LiveTvChannel channel, LiveTvProgram program) onProgramSelected; + + const GuideSearchSheet({ + super.key, + required this.channels, + required this.onChannelSelected, + required this.onProgramSelected, + }); + + @override + State createState() => _GuideSearchSheetState(); +} + +class _GuideSearchSheetState extends State with ControllerDisposerMixin { + static const _scheduleWindow = Duration(hours: 24); + static const _maxProgramResults = 30; + static const _minProgramQueryLength = 2; + + late final _searchController = createTextEditingController(); + final _searchFocusNode = FocusNode(debugLabel: 'GuideSearch_input'); + final _firstResultFocusNode = FocusNode(debugLabel: 'GuideSearch_firstResult'); + final _tvKeyboardController = TvKeyboardController(); + + List<({LiveTvProgram program, LiveTvChannel channel})> _allPrograms = const []; + bool _isLoadingPrograms = true; + + String _query = ''; + List _channelResults = const []; + List<({LiveTvProgram program, LiveTvChannel channel})> _programResults = const []; + + void Function(String? query)? _savedOnSearchAction; + + @override + void initState() { + super.initState(); + _channelResults = widget.channels; + + // While the sheet is open, companion-remote search queries land here + // instead of the global Search tab (save/restore idiom, see the video + // player's companion remote overrides). + final receiver = CompanionRemoteReceiver.instance; + _savedOnSearchAction = receiver.onSearchAction; + receiver.onSearchAction = _handleRemoteQuery; + + unawaited(_loadPrograms()); + } + + @override + void dispose() { + CompanionRemoteReceiver.instance.onSearchAction = _savedOnSearchAction; + _savedOnSearchAction = null; + _searchFocusNode.dispose(); + _firstResultFocusNode.dispose(); + super.dispose(); + } + + Future _loadPrograms() async { + final multiServer = context.read(); + final from = DateTime.now().toUtc(); + final to = from.add(_scheduleWindow); + final fetched = []; + final queriedServers = {}; + + for (final serverInfo in multiServer.liveTvServers) { + if (!queriedServers.add(serverInfo.serverId)) continue; + try { + final client = multiServer.getClientForServer(ServerId(serverInfo.serverId)); + if (client == null) continue; + fetched.addAll(await client.liveTv.fetchSchedule(from: from, to: to)); + } catch (e) { + appLogger.e('Guide search: failed to load programs from server ${serverInfo.serverId}', error: e); + } + } + + if (!mounted) return; + + // Resolve each program's channel once so per-keystroke filtering never + // pays the programs × channels matching cost. + final resolved = <({LiveTvProgram program, LiveTvChannel channel})>[]; + for (final program in fetched) { + for (final channel in widget.channels) { + if (liveTvProgramMatchesChannel(program, channel)) { + resolved.add((program: program, channel: channel)); + break; + } + } + } + resolved.sort((a, b) => (a.program.beginsAt ?? 0).compareTo(b.program.beginsAt ?? 0)); + + setState(() { + _allPrograms = resolved; + _isLoadingPrograms = false; + _recomputeResults(); + }); + } + + void _applyFilter(String query) { + setState(() { + _query = query.trim(); + _recomputeResults(); + }); + } + + void _recomputeResults() { + final lower = _query.toLowerCase(); + if (lower.isEmpty) { + _channelResults = widget.channels; + _programResults = const []; + return; + } + + _channelResults = widget.channels.where((channel) { + return (channel.title?.toLowerCase().contains(lower) ?? false) || + (channel.callSign?.toLowerCase().contains(lower) ?? false) || + (channel.number?.toLowerCase().contains(lower) ?? false); + }).toList(); + + _programResults = lower.length < _minProgramQueryLength + ? const [] + : _allPrograms + .where( + (entry) => + entry.program.title.toLowerCase().contains(lower) || + (entry.program.grandparentTitle?.toLowerCase().contains(lower) ?? false), + ) + .take(_maxProgramResults) + .toList(); + } + + bool get _hasAnyResult => _channelResults.isNotEmpty || _programResults.isNotEmpty; + + void _focusFirstResult() { + if (_hasAnyResult) _firstResultFocusNode.requestFocus(); + } + + void _focusFirstResultAfterSubmit() { + if (!InputModeTracker.isKeyboardMode(context)) return; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _focusFirstResult(); + }); + } + + /// Apply a complete query submitted from the companion remote: set the text, + /// dismiss any open on-screen keyboard, and land focus on the results. A + /// programmatic controller write does not fire onChanged, so filter directly. + void _handleRemoteQuery(String? query) { + if (!mounted) return; + final trimmed = query?.trim() ?? ''; + _searchController.text = trimmed; + _applyFilter(trimmed); + _tvKeyboardController.closeKeyboard(); + if (trimmed.isEmpty) return; + _tvKeyboardController.focusInputWithoutKeyboard(); + _focusFirstResultAfterSubmit(); + } + + void _selectChannel(LiveTvChannel channel) { + OverlaySheetController.popAdaptive(context); + widget.onChannelSelected(channel); + } + + void _selectProgram(({LiveTvProgram program, LiveTvChannel channel}) entry) { + OverlaySheetController.popAdaptive(context); + widget.onProgramSelected(entry.channel, entry.program); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisSize: .min, + children: [ + BottomSheetHeader(title: t.liveTv.searchGuide, icon: Symbols.search_rounded), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: FocusableTextField( + controller: _searchController, + focusNode: _searchFocusNode, + autofocus: true, + tvKeyboardController: _tvKeyboardController, + textInputAction: TextInputAction.search, + decoration: pillInputDecoration( + context, + hintText: t.liveTv.searchHint, + prefixIcon: const Icon(Symbols.search_rounded, size: 20), + ), + onChanged: _applyFilter, + onSubmitted: (_) => _focusFirstResultAfterSubmit(), + onSelect: _focusFirstResultAfterSubmit, + onNavigateDown: _hasAnyResult ? _focusFirstResult : null, + ), + ), + Expanded(child: _buildResults(context)), + ], + ); + } + + Widget _buildResults(BuildContext context) { + final showPrograms = _query.length >= _minProgramQueryLength; + if (!_hasAnyResult && !(showPrograms && _isLoadingPrograms)) { + return Center( + child: Padding(padding: const EdgeInsets.all(24), child: Text(t.liveTv.searchNoResults(query: _query))), + ); + } + + final children = [ + if (_channelResults.isNotEmpty) ...[ + _buildSectionHeader(context, t.liveTv.channelsSection), + for (var i = 0; i < _channelResults.length; i++) _buildChannelTile(_channelResults[i], firstResult: i == 0), + ], + if (showPrograms && (_programResults.isNotEmpty || _isLoadingPrograms)) ...[ + _buildSectionHeader(context, t.liveTv.programsSection), + if (_isLoadingPrograms) + const Padding( + padding: EdgeInsets.all(16), + child: Center( + child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)), + ), + ) + else + for (var i = 0; i < _programResults.length; i++) + _buildProgramTile(_programResults[i], firstResult: _channelResults.isEmpty && i == 0), + ], + ]; + + return ListView(padding: const EdgeInsets.only(bottom: 8), children: children); + } + + Widget _buildSectionHeader(BuildContext context, String title) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 4), + child: Text( + title, + style: Theme.of(context).textTheme.titleSmall?.copyWith(color: Theme.of(context).colorScheme.primary), + ), + ); + } + + Widget _buildChannelTile(LiveTvChannel channel, {required bool firstResult}) { + final multiServer = context.read(); + final serverId = serverIdOrNull(channel.serverId); + final client = serverId == null ? null : multiServer.getClientForServer(serverId); + + return FocusableListTile( + focusNode: firstResult ? _firstResultFocusNode : null, + leading: SizedBox( + width: 40, + height: 40, + child: channel.thumb != null && client != null + ? OptimizedMediaImage.thumb(client: client, imagePath: channel.thumb, width: 40, height: 40, fit: .contain) + : Center( + child: AppIcon(Symbols.live_tv_rounded, fill: 1, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ), + title: Text(channel.displayName, maxLines: 1, overflow: .ellipsis), + subtitle: channel.number != null + ? Text(t.liveTv.channelNumber(number: channel.number!), maxLines: 1, overflow: .ellipsis) + : null, + onTap: () => _selectChannel(channel), + ); + } + + Widget _buildProgramTile(({LiveTvProgram program, LiveTvChannel channel}) entry, {required bool firstResult}) { + final start = entry.program.startTime; + final airTime = start == null + ? null + : '${formatRelativeDayLabel(start)} ${formatClockTime(start, is24Hour: MediaQuery.alwaysUse24HourFormatOf(context))}'; + + return FocusableListTile( + focusNode: firstResult ? _firstResultFocusNode : null, + leading: SizedBox( + width: 40, + height: 40, + child: Center( + child: AppIcon(Symbols.schedule_rounded, fill: 1, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ), + title: Text(entry.program.displayTitle, maxLines: 1, overflow: .ellipsis), + subtitle: Text( + toBulletedString([entry.channel.displayName, ?airTime]), + maxLines: 1, + overflow: .ellipsis, + ), + onTap: () => _selectProgram(entry), + ); + } +} diff --git a/lib/screens/livetv/live_tv_screen.dart b/lib/screens/livetv/live_tv_screen.dart index 449c2f71d..5deaa592c 100644 --- a/lib/screens/livetv/live_tv_screen.dart +++ b/lib/screens/livetv/live_tv_screen.dart @@ -13,6 +13,7 @@ import '../../media/live_tv_support.dart'; import '../../media/media_server_client.dart'; import '../../models/livetv_channel.dart'; import '../../models/livetv_dvr.dart'; +import '../../models/livetv_program.dart'; import '../../mixins/refreshable.dart'; import '../../mixins/tab_navigation_mixin.dart'; import '../../providers/multi_server_provider.dart'; @@ -26,6 +27,7 @@ import '../../utils/snackbar_helper.dart'; import '../../widgets/app_icon.dart'; import '../../widgets/focusable_tab_chip.dart'; import '../../widgets/overlay_sheet.dart'; +import 'guide_search_sheet.dart'; import 'reorder_favorites_sheet.dart'; import 'tabs/guide_tab.dart'; import 'tabs/recordings_tab.dart'; @@ -530,6 +532,34 @@ class _LiveTvScreenState extends State ); } + void _showGuideSearch() { + OverlaySheetController.showAdaptive( + context, + builder: (sheetContext) => GuideSearchSheet( + channels: _channels, + onChannelSelected: _jumpToGuideChannel, + onProgramSelected: (channel, program) => _jumpToGuideChannel(channel, program: program), + ), + ); + } + + void _jumpToGuideChannel(LiveTvChannel channel, {LiveTvProgram? program}) { + // The guide only shows favorite rows while the filter is on — drop it so + // the target channel's row exists to land on. + if (_showFavoritesOnly && !_isFavoriteChannel(channel)) { + setState(() => _showFavoritesOnly = false); + } + WidgetsBinding.instance.addPostFrameCallback((_) { + final guide = _guideTabKey.currentState; + if (guide == null) return; + if (program != null) { + unawaited(guide.jumpToProgram(channel, program)); + } else { + guide.jumpToChannel(channel); + } + }); + } + void _showReorderFavorites() { final channelMap = {for (final c in _channels) _favoriteKeyForChannel(c): c}; @@ -644,6 +674,12 @@ class _LiveTvScreenState extends State onNavigateLeft: () => getTabChipFocusNode(tabCount - 1).requestFocus(), onNavigateDown: _focusCurrentTab, actions: [ + if (_currentTab == LiveTvTab.guide) + FocusableAction( + icon: Symbols.search_rounded, + tooltip: t.liveTv.searchGuide, + onPressed: _showGuideSearch, + ), if (!isRecordings) FocusableAction( icon: _showFavoritesOnly ? Symbols.star_rounded : Symbols.star_outline_rounded, diff --git a/lib/screens/livetv/tabs/guide_tab.dart b/lib/screens/livetv/tabs/guide_tab.dart index 349abdb58..554775ef3 100644 --- a/lib/screens/livetv/tabs/guide_tab.dart +++ b/lib/screens/livetv/tabs/guide_tab.dart @@ -146,6 +146,75 @@ class GuideTabState extends State with MountedSetStateMixin, WidgetsBi }); } + /// Jump the guide to [channel] (from guide search): scroll to its row and + /// land d-pad focus on the channel cell in keyboard mode. + void jumpToChannel(LiveTvChannel channel) { + final index = _channelIndexFor(channel); + if (index == null) return; + + setState(() { + _focusZone = _GuideZone.grid; + _gridChannelIndex = index; + _gridColumn = 0; + _focusedProgram = null; + }); + if (InputModeTracker.isKeyboardMode(context)) _guideFocusNode.requestFocus(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) _scrollToChannel(index); + }); + } + + /// Jump the guide to [program] on [channel] (from guide search), shifting + /// the time window first when the airing isn't visible in the current one. + Future jumpToProgram(LiveTvChannel channel, LiveTvProgram program) async { + final index = _channelIndexFor(channel); + if (index == null) return; + + final begin = program.startTime; + final end = program.endTime ?? begin; + final intersectsWindow = begin != null && end != null && begin.isBefore(_gridEnd) && end.isAfter(_gridStart); + if (begin != null && !intersectsWindow) { + // Same window mechanics as the day/time-slot picker: anchor a fresh + // 6-hour window one slot before the program and reload. + var start = DateTime(begin.year, begin.month, begin.day, begin.hour, begin.minute >= 30 ? 30 : 0); + start = start.subtract(const Duration(minutes: 30)); + setState(() { + _gridStart = start; + _gridEnd = start.add(const Duration(hours: 6)); + _nowWasInWindow = _nowInWindow(DateTime.now()); + }); + await _loadPrograms(); + if (!mounted) return; + } + + // Re-resolve against the loaded list — the search sheet's program objects + // come from its own fetch and never match _programs by identity. + final target = _getProgramsForChannel(widget.channels[index]) + .where((p) => p.beginsAt == program.beginsAt) + .firstOrNull; + + setState(() { + _focusZone = _GuideZone.grid; + _gridChannelIndex = index; + _gridColumn = target != null ? 1 : 0; + _focusedProgram = target; + }); + if (InputModeTracker.isKeyboardMode(context)) _guideFocusNode.requestFocus(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _scrollToChannel(index); + if (target != null) _scrollToProgramTime(target); + }); + } + + int? _channelIndexFor(LiveTvChannel channel) { + final scopeKey = liveTvChannelScopeKey(channel); + for (var i = 0; i < widget.channels.length; i++) { + if (liveTvChannelScopeKey(widget.channels[i]) == scopeKey) return i; + } + return null; + } + @override void initState() { super.initState(); From 1adcb26b192d9dc3874d4deb6d4925d5947b5267 Mon Sep 17 00:00:00 2001 From: l3gitpanda Date: Fri, 10 Jul 2026 16:14:44 -0700 Subject: [PATCH 2/3] style(livetv): dart format guide search files Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01UnEMtLJYbRim4k7YLCwvyZ --- lib/screens/livetv/guide_search_sheet.dart | 15 ++++++--------- lib/screens/livetv/tabs/guide_tab.dart | 6 +++--- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/lib/screens/livetv/guide_search_sheet.dart b/lib/screens/livetv/guide_search_sheet.dart index 7d59f49da..b65fddd1c 100644 --- a/lib/screens/livetv/guide_search_sheet.dart +++ b/lib/screens/livetv/guide_search_sheet.dart @@ -229,7 +229,10 @@ class _GuideSearchSheetState extends State with ControllerDisp final showPrograms = _query.length >= _minProgramQueryLength; if (!_hasAnyResult && !(showPrograms && _isLoadingPrograms)) { return Center( - child: Padding(padding: const EdgeInsets.all(24), child: Text(t.liveTv.searchNoResults(query: _query))), + child: Padding( + padding: const EdgeInsets.all(24), + child: Text(t.liveTv.searchNoResults(query: _query)), + ), ); } @@ -243,9 +246,7 @@ class _GuideSearchSheetState extends State with ControllerDisp if (_isLoadingPrograms) const Padding( padding: EdgeInsets.all(16), - child: Center( - child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)), - ), + child: Center(child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2))), ) else for (var i = 0; i < _programResults.length; i++) @@ -306,11 +307,7 @@ class _GuideSearchSheetState extends State with ControllerDisp ), ), title: Text(entry.program.displayTitle, maxLines: 1, overflow: .ellipsis), - subtitle: Text( - toBulletedString([entry.channel.displayName, ?airTime]), - maxLines: 1, - overflow: .ellipsis, - ), + subtitle: Text(toBulletedString([entry.channel.displayName, ?airTime]), maxLines: 1, overflow: .ellipsis), onTap: () => _selectProgram(entry), ); } diff --git a/lib/screens/livetv/tabs/guide_tab.dart b/lib/screens/livetv/tabs/guide_tab.dart index 554775ef3..63084ab89 100644 --- a/lib/screens/livetv/tabs/guide_tab.dart +++ b/lib/screens/livetv/tabs/guide_tab.dart @@ -189,9 +189,9 @@ class GuideTabState extends State with MountedSetStateMixin, WidgetsBi // Re-resolve against the loaded list — the search sheet's program objects // come from its own fetch and never match _programs by identity. - final target = _getProgramsForChannel(widget.channels[index]) - .where((p) => p.beginsAt == program.beginsAt) - .firstOrNull; + final target = _getProgramsForChannel( + widget.channels[index], + ).where((p) => p.beginsAt == program.beginsAt).firstOrNull; setState(() { _focusZone = _GuideZone.grid; From dd69af00b14e6fab27ccf3de61ec0ccf64f9347a Mon Sep 17 00:00:00 2001 From: l3gitpanda Date: Fri, 10 Jul 2026 17:16:38 -0700 Subject: [PATCH 3/3] fix(livetv): keep guide search reachable with d-pad navigation The search action was only rendered while the guide tab was selected, but the d-pad route into the app-bar action bar traverses the tab chips, and RIGHT selects each tab it crosses - so focus could only enter the bar with What's On (or Recordings) active, after the search button had already been unmounted. Show the search action on every Live TV tab instead, and switch back to the guide tab when a result is picked. Since a tab switch builds GuideTab fresh, its jump methods now stash a jump requested while programs are still loading and replay it once the load completes, in place of the default scroll-to-now anchoring. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01QBt1MzBDJXssuRNH1CBGRC --- lib/screens/livetv/live_tv_screen.dart | 18 ++++++++++----- lib/screens/livetv/tabs/guide_tab.dart | 31 ++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 6 deletions(-) diff --git a/lib/screens/livetv/live_tv_screen.dart b/lib/screens/livetv/live_tv_screen.dart index 5deaa592c..09bdf3d5f 100644 --- a/lib/screens/livetv/live_tv_screen.dart +++ b/lib/screens/livetv/live_tv_screen.dart @@ -549,6 +549,13 @@ class _LiveTvScreenState extends State if (_showFavoritesOnly && !_isFavoriteChannel(channel)) { setState(() => _showFavoritesOnly = false); } + // Search opens from any tab, but results live in the guide grid. A tab + // switch builds GuideTab fresh (no keep-alive); its jump methods stash + // the request until the initial program load completes. + final guideIndex = _visibleTabs.indexOf(LiveTvTab.guide); + if (guideIndex >= 0 && tabController.index != guideIndex) { + setState(() => tabController.index = guideIndex); + } WidgetsBinding.instance.addPostFrameCallback((_) { final guide = _guideTabKey.currentState; if (guide == null) return; @@ -674,12 +681,11 @@ class _LiveTvScreenState extends State onNavigateLeft: () => getTabChipFocusNode(tabCount - 1).requestFocus(), onNavigateDown: _focusCurrentTab, actions: [ - if (_currentTab == LiveTvTab.guide) - FocusableAction( - icon: Symbols.search_rounded, - tooltip: t.liveTv.searchGuide, - onPressed: _showGuideSearch, - ), + // Shown on every tab: the d-pad route into this bar traverses + // the tab chips, and RIGHT selects each tab it crosses — a + // guide-only action would be unmounted before focus could ever + // reach it. Selecting a result switches back to the guide tab. + FocusableAction(icon: Symbols.search_rounded, tooltip: t.liveTv.searchGuide, onPressed: _showGuideSearch), if (!isRecordings) FocusableAction( icon: _showFavoritesOnly ? Symbols.star_rounded : Symbols.star_outline_rounded, diff --git a/lib/screens/livetv/tabs/guide_tab.dart b/lib/screens/livetv/tabs/guide_tab.dart index 63084ab89..69b804bff 100644 --- a/lib/screens/livetv/tabs/guide_tab.dart +++ b/lib/screens/livetv/tabs/guide_tab.dart @@ -123,6 +123,11 @@ class GuideTabState extends State with MountedSetStateMixin, WidgetsBi bool _pendingFocus = false; bool _isProgramSelectKeyDown = false; + // Jump requested while programs were still loading (guide search from + // another tab lands on a freshly built guide) — replayed by _loadPrograms. + LiveTvChannel? _pendingJumpChannel; + LiveTvProgram? _pendingJumpProgram; + /// Focus into the guide content (called from tab bar navigation or initial load). void focusContent() { if (!InputModeTracker.isKeyboardMode(context)) return; @@ -149,6 +154,11 @@ class GuideTabState extends State with MountedSetStateMixin, WidgetsBi /// Jump the guide to [channel] (from guide search): scroll to its row and /// land d-pad focus on the channel cell in keyboard mode. void jumpToChannel(LiveTvChannel channel) { + if (_isLoading) { + _pendingJumpChannel = channel; + _pendingJumpProgram = null; + return; + } final index = _channelIndexFor(channel); if (index == null) return; @@ -167,6 +177,11 @@ class GuideTabState extends State with MountedSetStateMixin, WidgetsBi /// Jump the guide to [program] on [channel] (from guide search), shifting /// the time window first when the airing isn't visible in the current one. Future jumpToProgram(LiveTvChannel channel, LiveTvProgram program) async { + if (_isLoading) { + _pendingJumpChannel = channel; + _pendingJumpProgram = program; + return; + } final index = _channelIndexFor(channel); if (index == null) return; @@ -414,6 +429,10 @@ class GuideTabState extends State with MountedSetStateMixin, WidgetsBi if (!mounted) return; final shouldFocus = _pendingFocus; + final pendingJumpChannel = _pendingJumpChannel; + final pendingJumpProgram = _pendingJumpProgram; + _pendingJumpChannel = null; + _pendingJumpProgram = null; setState(() { _programs = allPrograms; @@ -429,6 +448,18 @@ class GuideTabState extends State with MountedSetStateMixin, WidgetsBi } }); + if (pendingJumpChannel != null) { + // A stashed search jump wins over the default live-line anchoring and + // over any focus request queued during the load. + _pendingFocus = false; + if (pendingJumpProgram != null) { + unawaited(jumpToProgram(pendingJumpChannel, pendingJumpProgram)); + } else { + jumpToChannel(pendingJumpChannel); + } + return; + } + _scrollToNow(); if (shouldFocus) {