@@ -374,7 +374,13 @@
|
||||
"chapters": "Kapitler",
|
||||
"noChaptersAvailable": "Ingen kapitler tilgængelige",
|
||||
"queue": "Kø",
|
||||
"noQueueItems": "Ingen elementer i køen"
|
||||
"noQueueItems": "Ingen elementer i køen",
|
||||
"searchSubtitles": "Søg undertekster",
|
||||
"language": "Sprog",
|
||||
"noSubtitlesFound": "Ingen undertekster fundet",
|
||||
"subtitleDownloaded": "Undertekst downloadet",
|
||||
"subtitleDownloadFailed": "Kunne ikke downloade undertekst",
|
||||
"searchLanguages": "Søg sprog..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Administrator",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "Kapitel",
|
||||
"noChaptersAvailable": "Keine Kapitel verfügbar",
|
||||
"queue": "Warteschlange",
|
||||
"noQueueItems": "Keine Elemente in der Warteschlange"
|
||||
"noQueueItems": "Keine Elemente in der Warteschlange",
|
||||
"searchSubtitles": "Untertitel suchen",
|
||||
"language": "Sprache",
|
||||
"noSubtitlesFound": "Keine Untertitel gefunden",
|
||||
"subtitleDownloaded": "Untertitel heruntergeladen",
|
||||
"subtitleDownloadFailed": "Untertitel konnte nicht heruntergeladen werden",
|
||||
"searchLanguages": "Sprachen suchen..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Eigentümer",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "Chapters",
|
||||
"noChaptersAvailable": "No chapters available",
|
||||
"queue": "Queue",
|
||||
"noQueueItems": "No items in queue"
|
||||
"noQueueItems": "No items in queue",
|
||||
"searchSubtitles": "Search Subtitles",
|
||||
"language": "Language",
|
||||
"noSubtitlesFound": "No subtitles found",
|
||||
"subtitleDownloaded": "Subtitle downloaded",
|
||||
"subtitleDownloadFailed": "Failed to download subtitle",
|
||||
"searchLanguages": "Search languages..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Admin",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "Capítulos",
|
||||
"noChaptersAvailable": "No hay capítulos disponibles",
|
||||
"queue": "Cola",
|
||||
"noQueueItems": "No hay elementos en la cola"
|
||||
"noQueueItems": "No hay elementos en la cola",
|
||||
"searchSubtitles": "Buscar subtítulos",
|
||||
"language": "Idioma",
|
||||
"noSubtitlesFound": "No se encontraron subtítulos",
|
||||
"subtitleDownloaded": "Subtítulo descargado",
|
||||
"subtitleDownloadFailed": "Error al descargar subtítulo",
|
||||
"searchLanguages": "Buscar idiomas..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Administrador",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "Chapitres",
|
||||
"noChaptersAvailable": "Aucun chapitre disponible",
|
||||
"queue": "File d'attente",
|
||||
"noQueueItems": "Aucun élément dans la file d'attente"
|
||||
"noQueueItems": "Aucun élément dans la file d'attente",
|
||||
"searchSubtitles": "Rechercher des sous-titres",
|
||||
"language": "Langue",
|
||||
"noSubtitlesFound": "Aucun sous-titre trouvé",
|
||||
"subtitleDownloaded": "Sous-titre téléchargé",
|
||||
"subtitleDownloadFailed": "Échec du téléchargement du sous-titre",
|
||||
"searchLanguages": "Rechercher des langues..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Admin",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "Capitoli",
|
||||
"noChaptersAvailable": "Nessun capitolo disponibile",
|
||||
"queue": "Coda",
|
||||
"noQueueItems": "Nessun elemento in coda"
|
||||
"noQueueItems": "Nessun elemento in coda",
|
||||
"searchSubtitles": "Cerca sottotitoli",
|
||||
"language": "Lingua",
|
||||
"noSubtitlesFound": "Nessun sottotitolo trovato",
|
||||
"subtitleDownloaded": "Sottotitolo scaricato",
|
||||
"subtitleDownloadFailed": "Impossibile scaricare il sottotitolo",
|
||||
"searchLanguages": "Cerca lingue..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Admin",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "チャプター",
|
||||
"noChaptersAvailable": "チャプターがありません",
|
||||
"queue": "キュー",
|
||||
"noQueueItems": "キューにアイテムがありません"
|
||||
"noQueueItems": "キューにアイテムがありません",
|
||||
"searchSubtitles": "字幕を検索",
|
||||
"language": "言語",
|
||||
"noSubtitlesFound": "字幕が見つかりません",
|
||||
"subtitleDownloaded": "字幕をダウンロードしました",
|
||||
"subtitleDownloadFailed": "字幕のダウンロードに失敗しました",
|
||||
"searchLanguages": "言語を検索..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "管理者",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "챕터",
|
||||
"noChaptersAvailable": "사용 가능한 챕터가 없습니다",
|
||||
"queue": "재생 대기열",
|
||||
"noQueueItems": "대기열에 항목이 없습니다"
|
||||
"noQueueItems": "대기열에 항목이 없습니다",
|
||||
"searchSubtitles": "자막 검색",
|
||||
"language": "언어",
|
||||
"noSubtitlesFound": "자막을 찾을 수 없습니다",
|
||||
"subtitleDownloaded": "자막이 다운로드되었습니다",
|
||||
"subtitleDownloadFailed": "자막 다운로드에 실패했습니다",
|
||||
"searchLanguages": "언어 검색..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "관리자",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "Kapitler",
|
||||
"noChaptersAvailable": "Ingen kapitler tilgjengelig",
|
||||
"queue": "Kø",
|
||||
"noQueueItems": "Ingen elementer i kø"
|
||||
"noQueueItems": "Ingen elementer i kø",
|
||||
"searchSubtitles": "Søk etter undertekster",
|
||||
"language": "Språk",
|
||||
"noSubtitlesFound": "Ingen undertekster funnet",
|
||||
"subtitleDownloaded": "Undertekst lastet ned",
|
||||
"subtitleDownloadFailed": "Kunne ikke laste ned undertekst",
|
||||
"searchLanguages": "Søk etter språk..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Administrator",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "Hoofdstukken",
|
||||
"noChaptersAvailable": "Geen hoofdstukken beschikbaar",
|
||||
"queue": "Wachtrij",
|
||||
"noQueueItems": "Geen items in de wachtrij"
|
||||
"noQueueItems": "Geen items in de wachtrij",
|
||||
"searchSubtitles": "Ondertitels zoeken",
|
||||
"language": "Taal",
|
||||
"noSubtitlesFound": "Geen ondertitels gevonden",
|
||||
"subtitleDownloaded": "Ondertitel gedownload",
|
||||
"subtitleDownloadFailed": "Ondertitel downloaden mislukt",
|
||||
"searchLanguages": "Talen zoeken..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Beheerder",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "Rozdziały",
|
||||
"noChaptersAvailable": "Brak dostępnych rozdziałów",
|
||||
"queue": "Kolejka",
|
||||
"noQueueItems": "Brak elementów w kolejce"
|
||||
"noQueueItems": "Brak elementów w kolejce",
|
||||
"searchSubtitles": "Szukaj napisów",
|
||||
"language": "Język",
|
||||
"noSubtitlesFound": "Nie znaleziono napisów",
|
||||
"subtitleDownloaded": "Napisy pobrane",
|
||||
"subtitleDownloadFailed": "Nie udało się pobrać napisów",
|
||||
"searchLanguages": "Szukaj języków..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Administrator",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "Capítulos",
|
||||
"noChaptersAvailable": "Nenhum capítulo disponível",
|
||||
"queue": "Fila",
|
||||
"noQueueItems": "Nenhum item na fila"
|
||||
"noQueueItems": "Nenhum item na fila",
|
||||
"searchSubtitles": "Pesquisar legendas",
|
||||
"language": "Idioma",
|
||||
"noSubtitlesFound": "Nenhuma legenda encontrada",
|
||||
"subtitleDownloaded": "Legenda baixada",
|
||||
"subtitleDownloadFailed": "Falha ao baixar legenda",
|
||||
"searchLanguages": "Pesquisar idiomas..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Admin",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "Главы",
|
||||
"noChaptersAvailable": "Главы недоступны",
|
||||
"queue": "Очередь",
|
||||
"noQueueItems": "В очереди нет элементов"
|
||||
"noQueueItems": "В очереди нет элементов",
|
||||
"searchSubtitles": "Поиск субтитров",
|
||||
"language": "Язык",
|
||||
"noSubtitlesFound": "Субтитры не найдены",
|
||||
"subtitleDownloaded": "Субтитры загружены",
|
||||
"subtitleDownloadFailed": "Не удалось загрузить субтитры",
|
||||
"searchLanguages": "Поиск языков..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Администратор",
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
/// To regenerate, run: `dart run slang`
|
||||
///
|
||||
/// Locales: 15
|
||||
/// Strings: 12375 (825 per locale)
|
||||
/// Strings: 12465 (831 per locale)
|
||||
///
|
||||
/// Built on 2026-04-01 at 09:43 UTC
|
||||
/// Built on 2026-04-01 at 15:18 UTC
|
||||
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint, unused_import
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsDa implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'Ingen kapitler tilgængelige';
|
||||
@override String get queue => 'Kø';
|
||||
@override String get noQueueItems => 'Ingen elementer i køen';
|
||||
@override String get searchSubtitles => 'Søg undertekster';
|
||||
@override String get language => 'Sprog';
|
||||
@override String get noSubtitlesFound => 'Ingen undertekster fundet';
|
||||
@override String get subtitleDownloaded => 'Undertekst downloadet';
|
||||
@override String get subtitleDownloadFailed => 'Kunne ikke downloade undertekst';
|
||||
@override String get searchLanguages => 'Søg sprog...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsDa {
|
||||
'videoControls.noChaptersAvailable' => 'Ingen kapitler tilgængelige',
|
||||
'videoControls.queue' => 'Kø',
|
||||
'videoControls.noQueueItems' => 'Ingen elementer i køen',
|
||||
'videoControls.searchSubtitles' => 'Søg undertekster',
|
||||
'videoControls.language' => 'Sprog',
|
||||
'videoControls.noSubtitlesFound' => 'Ingen undertekster fundet',
|
||||
'videoControls.subtitleDownloaded' => 'Undertekst downloadet',
|
||||
'videoControls.subtitleDownloadFailed' => 'Kunne ikke downloade undertekst',
|
||||
'videoControls.searchLanguages' => 'Søg sprog...',
|
||||
'userStatus.admin' => 'Administrator',
|
||||
'userStatus.restricted' => 'Begrænset',
|
||||
'userStatus.protected' => 'Beskyttet',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsDa {
|
||||
'liveTv.guide' => 'Guide',
|
||||
'liveTv.noChannels' => 'Ingen kanaler tilgængelige',
|
||||
'liveTv.noDvr' => 'Ingen DVR konfigureret på nogen server',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'Ingen programdata tilgængelig',
|
||||
'liveTv.live' => 'LIVE',
|
||||
'liveTv.reloadGuide' => 'Genindlæs guide',
|
||||
'liveTv.now' => 'Nu',
|
||||
'liveTv.today' => 'I dag',
|
||||
'liveTv.midnight' => 'Midnat',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Nat',
|
||||
'liveTv.morning' => 'Morgen',
|
||||
'liveTv.daytime' => 'Dagtid',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsDe implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'Keine Kapitel verfügbar';
|
||||
@override String get queue => 'Warteschlange';
|
||||
@override String get noQueueItems => 'Keine Elemente in der Warteschlange';
|
||||
@override String get searchSubtitles => 'Untertitel suchen';
|
||||
@override String get language => 'Sprache';
|
||||
@override String get noSubtitlesFound => 'Keine Untertitel gefunden';
|
||||
@override String get subtitleDownloaded => 'Untertitel heruntergeladen';
|
||||
@override String get subtitleDownloadFailed => 'Untertitel konnte nicht heruntergeladen werden';
|
||||
@override String get searchLanguages => 'Sprachen suchen...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsDe {
|
||||
'videoControls.noChaptersAvailable' => 'Keine Kapitel verfügbar',
|
||||
'videoControls.queue' => 'Warteschlange',
|
||||
'videoControls.noQueueItems' => 'Keine Elemente in der Warteschlange',
|
||||
'videoControls.searchSubtitles' => 'Untertitel suchen',
|
||||
'videoControls.language' => 'Sprache',
|
||||
'videoControls.noSubtitlesFound' => 'Keine Untertitel gefunden',
|
||||
'videoControls.subtitleDownloaded' => 'Untertitel heruntergeladen',
|
||||
'videoControls.subtitleDownloadFailed' => 'Untertitel konnte nicht heruntergeladen werden',
|
||||
'videoControls.searchLanguages' => 'Sprachen suchen...',
|
||||
'userStatus.admin' => 'Eigentümer',
|
||||
'userStatus.restricted' => 'Eingeschränkt',
|
||||
'userStatus.protected' => 'Geschützt',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsDe {
|
||||
'liveTv.guide' => 'Programmführer',
|
||||
'liveTv.noChannels' => 'Keine Kanäle verfügbar',
|
||||
'liveTv.noDvr' => 'Kein DVR auf einem Server konfiguriert',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'Keine Programmdaten verfügbar',
|
||||
'liveTv.live' => 'LIVE',
|
||||
'liveTv.reloadGuide' => 'Programmführer neu laden',
|
||||
'liveTv.now' => 'Jetzt',
|
||||
'liveTv.today' => 'Heute',
|
||||
'liveTv.midnight' => 'Mitternacht',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Nacht',
|
||||
'liveTv.morning' => 'Morgen',
|
||||
'liveTv.daytime' => 'Tagsüber',
|
||||
|
||||
@@ -1162,6 +1162,24 @@ class TranslationsVideoControlsEn {
|
||||
|
||||
/// en: 'No items in queue'
|
||||
String get noQueueItems => 'No items in queue';
|
||||
|
||||
/// en: 'Search Subtitles'
|
||||
String get searchSubtitles => 'Search Subtitles';
|
||||
|
||||
/// en: 'Language'
|
||||
String get language => 'Language';
|
||||
|
||||
/// en: 'No subtitles found'
|
||||
String get noSubtitlesFound => 'No subtitles found';
|
||||
|
||||
/// en: 'Subtitle downloaded'
|
||||
String get subtitleDownloaded => 'Subtitle downloaded';
|
||||
|
||||
/// en: 'Failed to download subtitle'
|
||||
String get subtitleDownloadFailed => 'Failed to download subtitle';
|
||||
|
||||
/// en: 'Search languages...'
|
||||
String get searchLanguages => 'Search languages...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -3326,6 +3344,12 @@ extension on Translations {
|
||||
'videoControls.noChaptersAvailable' => 'No chapters available',
|
||||
'videoControls.queue' => 'Queue',
|
||||
'videoControls.noQueueItems' => 'No items in queue',
|
||||
'videoControls.searchSubtitles' => 'Search Subtitles',
|
||||
'videoControls.language' => 'Language',
|
||||
'videoControls.noSubtitlesFound' => 'No subtitles found',
|
||||
'videoControls.subtitleDownloaded' => 'Subtitle downloaded',
|
||||
'videoControls.subtitleDownloadFailed' => 'Failed to download subtitle',
|
||||
'videoControls.searchLanguages' => 'Search languages...',
|
||||
'userStatus.admin' => 'Admin',
|
||||
'userStatus.restricted' => 'Restricted',
|
||||
'userStatus.protected' => 'Protected',
|
||||
@@ -3485,14 +3509,14 @@ extension on Translations {
|
||||
'liveTv.guide' => 'Guide',
|
||||
'liveTv.noChannels' => 'No channels available',
|
||||
'liveTv.noDvr' => 'No DVR configured on any server',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'No program data available',
|
||||
'liveTv.live' => 'LIVE',
|
||||
'liveTv.reloadGuide' => 'Reload Guide',
|
||||
'liveTv.now' => 'Now',
|
||||
'liveTv.today' => 'Today',
|
||||
'liveTv.midnight' => 'Midnight',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Overnight',
|
||||
'liveTv.morning' => 'Morning',
|
||||
'liveTv.daytime' => 'Daytime',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsEs implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'No hay capítulos disponibles';
|
||||
@override String get queue => 'Cola';
|
||||
@override String get noQueueItems => 'No hay elementos en la cola';
|
||||
@override String get searchSubtitles => 'Buscar subtítulos';
|
||||
@override String get language => 'Idioma';
|
||||
@override String get noSubtitlesFound => 'No se encontraron subtítulos';
|
||||
@override String get subtitleDownloaded => 'Subtítulo descargado';
|
||||
@override String get subtitleDownloadFailed => 'Error al descargar subtítulo';
|
||||
@override String get searchLanguages => 'Buscar idiomas...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsEs {
|
||||
'videoControls.noChaptersAvailable' => 'No hay capítulos disponibles',
|
||||
'videoControls.queue' => 'Cola',
|
||||
'videoControls.noQueueItems' => 'No hay elementos en la cola',
|
||||
'videoControls.searchSubtitles' => 'Buscar subtítulos',
|
||||
'videoControls.language' => 'Idioma',
|
||||
'videoControls.noSubtitlesFound' => 'No se encontraron subtítulos',
|
||||
'videoControls.subtitleDownloaded' => 'Subtítulo descargado',
|
||||
'videoControls.subtitleDownloadFailed' => 'Error al descargar subtítulo',
|
||||
'videoControls.searchLanguages' => 'Buscar idiomas...',
|
||||
'userStatus.admin' => 'Administrador',
|
||||
'userStatus.restricted' => 'Restringido',
|
||||
'userStatus.protected' => 'Protegido',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsEs {
|
||||
'liveTv.guide' => 'Guía',
|
||||
'liveTv.noChannels' => 'No hay canales disponibles',
|
||||
'liveTv.noDvr' => 'No hay DVR configurado en ningún servidor',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'No hay datos de programación disponibles',
|
||||
'liveTv.live' => 'EN VIVO',
|
||||
'liveTv.reloadGuide' => 'Recargar guía',
|
||||
'liveTv.now' => 'Ahora',
|
||||
'liveTv.today' => 'Hoy',
|
||||
'liveTv.midnight' => 'Medianoche',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Madrugada',
|
||||
'liveTv.morning' => 'Mañana',
|
||||
'liveTv.daytime' => 'Día',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsFr implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'Aucun chapitre disponible';
|
||||
@override String get queue => 'File d\'attente';
|
||||
@override String get noQueueItems => 'Aucun élément dans la file d\'attente';
|
||||
@override String get searchSubtitles => 'Rechercher des sous-titres';
|
||||
@override String get language => 'Langue';
|
||||
@override String get noSubtitlesFound => 'Aucun sous-titre trouvé';
|
||||
@override String get subtitleDownloaded => 'Sous-titre téléchargé';
|
||||
@override String get subtitleDownloadFailed => 'Échec du téléchargement du sous-titre';
|
||||
@override String get searchLanguages => 'Rechercher des langues...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsFr {
|
||||
'videoControls.noChaptersAvailable' => 'Aucun chapitre disponible',
|
||||
'videoControls.queue' => 'File d\'attente',
|
||||
'videoControls.noQueueItems' => 'Aucun élément dans la file d\'attente',
|
||||
'videoControls.searchSubtitles' => 'Rechercher des sous-titres',
|
||||
'videoControls.language' => 'Langue',
|
||||
'videoControls.noSubtitlesFound' => 'Aucun sous-titre trouvé',
|
||||
'videoControls.subtitleDownloaded' => 'Sous-titre téléchargé',
|
||||
'videoControls.subtitleDownloadFailed' => 'Échec du téléchargement du sous-titre',
|
||||
'videoControls.searchLanguages' => 'Rechercher des langues...',
|
||||
'userStatus.admin' => 'Admin',
|
||||
'userStatus.restricted' => 'Restreint',
|
||||
'userStatus.protected' => 'Protégé',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsFr {
|
||||
'liveTv.guide' => 'Guide',
|
||||
'liveTv.noChannels' => 'Aucune chaîne disponible',
|
||||
'liveTv.noDvr' => 'Aucun DVR configuré sur les serveurs',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'Aucune donnée de programme disponible',
|
||||
'liveTv.live' => 'EN DIRECT',
|
||||
'liveTv.reloadGuide' => 'Recharger le guide',
|
||||
'liveTv.now' => 'Maintenant',
|
||||
'liveTv.today' => 'Aujourd\'hui',
|
||||
'liveTv.midnight' => 'Minuit',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Nuit',
|
||||
'liveTv.morning' => 'Matin',
|
||||
'liveTv.daytime' => 'Journée',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsIt implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'Nessun capitolo disponibile';
|
||||
@override String get queue => 'Coda';
|
||||
@override String get noQueueItems => 'Nessun elemento in coda';
|
||||
@override String get searchSubtitles => 'Cerca sottotitoli';
|
||||
@override String get language => 'Lingua';
|
||||
@override String get noSubtitlesFound => 'Nessun sottotitolo trovato';
|
||||
@override String get subtitleDownloaded => 'Sottotitolo scaricato';
|
||||
@override String get subtitleDownloadFailed => 'Impossibile scaricare il sottotitolo';
|
||||
@override String get searchLanguages => 'Cerca lingue...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsIt {
|
||||
'videoControls.noChaptersAvailable' => 'Nessun capitolo disponibile',
|
||||
'videoControls.queue' => 'Coda',
|
||||
'videoControls.noQueueItems' => 'Nessun elemento in coda',
|
||||
'videoControls.searchSubtitles' => 'Cerca sottotitoli',
|
||||
'videoControls.language' => 'Lingua',
|
||||
'videoControls.noSubtitlesFound' => 'Nessun sottotitolo trovato',
|
||||
'videoControls.subtitleDownloaded' => 'Sottotitolo scaricato',
|
||||
'videoControls.subtitleDownloadFailed' => 'Impossibile scaricare il sottotitolo',
|
||||
'videoControls.searchLanguages' => 'Cerca lingue...',
|
||||
'userStatus.admin' => 'Admin',
|
||||
'userStatus.restricted' => 'Limitato',
|
||||
'userStatus.protected' => 'Protetto',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsIt {
|
||||
'liveTv.guide' => 'Guida',
|
||||
'liveTv.noChannels' => 'Nessun canale disponibile',
|
||||
'liveTv.noDvr' => 'Nessun DVR configurato su nessun server',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'Nessun dato di programma disponibile',
|
||||
'liveTv.live' => 'IN DIRETTA',
|
||||
'liveTv.reloadGuide' => 'Ricarica guida',
|
||||
'liveTv.now' => 'Ora',
|
||||
'liveTv.today' => 'Oggi',
|
||||
'liveTv.midnight' => 'Mezzanotte',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Notte',
|
||||
'liveTv.morning' => 'Mattina',
|
||||
'liveTv.daytime' => 'Giorno',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsJa implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'チャプターがありません';
|
||||
@override String get queue => 'キュー';
|
||||
@override String get noQueueItems => 'キューにアイテムがありません';
|
||||
@override String get searchSubtitles => '字幕を検索';
|
||||
@override String get language => '言語';
|
||||
@override String get noSubtitlesFound => '字幕が見つかりません';
|
||||
@override String get subtitleDownloaded => '字幕をダウンロードしました';
|
||||
@override String get subtitleDownloadFailed => '字幕のダウンロードに失敗しました';
|
||||
@override String get searchLanguages => '言語を検索...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsJa {
|
||||
'videoControls.noChaptersAvailable' => 'チャプターがありません',
|
||||
'videoControls.queue' => 'キュー',
|
||||
'videoControls.noQueueItems' => 'キューにアイテムがありません',
|
||||
'videoControls.searchSubtitles' => '字幕を検索',
|
||||
'videoControls.language' => '言語',
|
||||
'videoControls.noSubtitlesFound' => '字幕が見つかりません',
|
||||
'videoControls.subtitleDownloaded' => '字幕をダウンロードしました',
|
||||
'videoControls.subtitleDownloadFailed' => '字幕のダウンロードに失敗しました',
|
||||
'videoControls.searchLanguages' => '言語を検索...',
|
||||
'userStatus.admin' => '管理者',
|
||||
'userStatus.restricted' => '制限付き',
|
||||
'userStatus.protected' => '保護済み',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsJa {
|
||||
'liveTv.guide' => '番組表',
|
||||
'liveTv.noChannels' => 'チャンネルがありません',
|
||||
'liveTv.noDvr' => 'どのサーバーにもDVRが設定されていません',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => '番組データがありません',
|
||||
'liveTv.live' => 'ライブ',
|
||||
'liveTv.reloadGuide' => '番組表を再読込',
|
||||
'liveTv.now' => '現在',
|
||||
'liveTv.today' => '今日',
|
||||
'liveTv.midnight' => '深夜',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => '深夜',
|
||||
'liveTv.morning' => '朝',
|
||||
'liveTv.daytime' => '昼',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsKo implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => '사용 가능한 챕터가 없습니다';
|
||||
@override String get queue => '재생 대기열';
|
||||
@override String get noQueueItems => '대기열에 항목이 없습니다';
|
||||
@override String get searchSubtitles => '자막 검색';
|
||||
@override String get language => '언어';
|
||||
@override String get noSubtitlesFound => '자막을 찾을 수 없습니다';
|
||||
@override String get subtitleDownloaded => '자막이 다운로드되었습니다';
|
||||
@override String get subtitleDownloadFailed => '자막 다운로드에 실패했습니다';
|
||||
@override String get searchLanguages => '언어 검색...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsKo {
|
||||
'videoControls.noChaptersAvailable' => '사용 가능한 챕터가 없습니다',
|
||||
'videoControls.queue' => '재생 대기열',
|
||||
'videoControls.noQueueItems' => '대기열에 항목이 없습니다',
|
||||
'videoControls.searchSubtitles' => '자막 검색',
|
||||
'videoControls.language' => '언어',
|
||||
'videoControls.noSubtitlesFound' => '자막을 찾을 수 없습니다',
|
||||
'videoControls.subtitleDownloaded' => '자막이 다운로드되었습니다',
|
||||
'videoControls.subtitleDownloadFailed' => '자막 다운로드에 실패했습니다',
|
||||
'videoControls.searchLanguages' => '언어 검색...',
|
||||
'userStatus.admin' => '관리자',
|
||||
'userStatus.restricted' => '제한됨',
|
||||
'userStatus.protected' => '보호됨',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsKo {
|
||||
'liveTv.guide' => '편성표',
|
||||
'liveTv.noChannels' => '사용 가능한 채널이 없습니다',
|
||||
'liveTv.noDvr' => '서버에 DVR이 구성되어 있지 않습니다',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => '프로그램 데이터가 없습니다',
|
||||
'liveTv.live' => '실시간',
|
||||
'liveTv.reloadGuide' => '편성표 새로고침',
|
||||
'liveTv.now' => '지금',
|
||||
'liveTv.today' => '오늘',
|
||||
'liveTv.midnight' => '자정',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => '심야',
|
||||
'liveTv.morning' => '아침',
|
||||
'liveTv.daytime' => '낮',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsNb implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'Ingen kapitler tilgjengelig';
|
||||
@override String get queue => 'Kø';
|
||||
@override String get noQueueItems => 'Ingen elementer i kø';
|
||||
@override String get searchSubtitles => 'Søk etter undertekster';
|
||||
@override String get language => 'Språk';
|
||||
@override String get noSubtitlesFound => 'Ingen undertekster funnet';
|
||||
@override String get subtitleDownloaded => 'Undertekst lastet ned';
|
||||
@override String get subtitleDownloadFailed => 'Kunne ikke laste ned undertekst';
|
||||
@override String get searchLanguages => 'Søk etter språk...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsNb {
|
||||
'videoControls.noChaptersAvailable' => 'Ingen kapitler tilgjengelig',
|
||||
'videoControls.queue' => 'Kø',
|
||||
'videoControls.noQueueItems' => 'Ingen elementer i kø',
|
||||
'videoControls.searchSubtitles' => 'Søk etter undertekster',
|
||||
'videoControls.language' => 'Språk',
|
||||
'videoControls.noSubtitlesFound' => 'Ingen undertekster funnet',
|
||||
'videoControls.subtitleDownloaded' => 'Undertekst lastet ned',
|
||||
'videoControls.subtitleDownloadFailed' => 'Kunne ikke laste ned undertekst',
|
||||
'videoControls.searchLanguages' => 'Søk etter språk...',
|
||||
'userStatus.admin' => 'Administrator',
|
||||
'userStatus.restricted' => 'Begrenset',
|
||||
'userStatus.protected' => 'Beskyttet',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsNb {
|
||||
'liveTv.guide' => 'Programguide',
|
||||
'liveTv.noChannels' => 'Ingen kanaler tilgjengelig',
|
||||
'liveTv.noDvr' => 'Ingen DVR konfigurert på noen server',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'Ingen programdata tilgjengelig',
|
||||
'liveTv.live' => 'DIREKTE',
|
||||
'liveTv.reloadGuide' => 'Last inn programguide på nytt',
|
||||
'liveTv.now' => 'Nå',
|
||||
'liveTv.today' => 'I dag',
|
||||
'liveTv.midnight' => 'Midnatt',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Natt',
|
||||
'liveTv.morning' => 'Morgen',
|
||||
'liveTv.daytime' => 'Dagtid',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsNl implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'Geen hoofdstukken beschikbaar';
|
||||
@override String get queue => 'Wachtrij';
|
||||
@override String get noQueueItems => 'Geen items in de wachtrij';
|
||||
@override String get searchSubtitles => 'Ondertitels zoeken';
|
||||
@override String get language => 'Taal';
|
||||
@override String get noSubtitlesFound => 'Geen ondertitels gevonden';
|
||||
@override String get subtitleDownloaded => 'Ondertitel gedownload';
|
||||
@override String get subtitleDownloadFailed => 'Ondertitel downloaden mislukt';
|
||||
@override String get searchLanguages => 'Talen zoeken...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsNl {
|
||||
'videoControls.noChaptersAvailable' => 'Geen hoofdstukken beschikbaar',
|
||||
'videoControls.queue' => 'Wachtrij',
|
||||
'videoControls.noQueueItems' => 'Geen items in de wachtrij',
|
||||
'videoControls.searchSubtitles' => 'Ondertitels zoeken',
|
||||
'videoControls.language' => 'Taal',
|
||||
'videoControls.noSubtitlesFound' => 'Geen ondertitels gevonden',
|
||||
'videoControls.subtitleDownloaded' => 'Ondertitel gedownload',
|
||||
'videoControls.subtitleDownloadFailed' => 'Ondertitel downloaden mislukt',
|
||||
'videoControls.searchLanguages' => 'Talen zoeken...',
|
||||
'userStatus.admin' => 'Beheerder',
|
||||
'userStatus.restricted' => 'Beperkt',
|
||||
'userStatus.protected' => 'Beschermd',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsNl {
|
||||
'liveTv.guide' => 'Gids',
|
||||
'liveTv.noChannels' => 'Geen zenders beschikbaar',
|
||||
'liveTv.noDvr' => 'Geen DVR geconfigureerd op een server',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'Geen programmagegevens beschikbaar',
|
||||
'liveTv.live' => 'LIVE',
|
||||
'liveTv.reloadGuide' => 'Gids herladen',
|
||||
'liveTv.now' => 'Nu',
|
||||
'liveTv.today' => 'Vandaag',
|
||||
'liveTv.midnight' => 'Middernacht',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Nacht',
|
||||
'liveTv.morning' => 'Ochtend',
|
||||
'liveTv.daytime' => 'Overdag',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsPl implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'Brak dostępnych rozdziałów';
|
||||
@override String get queue => 'Kolejka';
|
||||
@override String get noQueueItems => 'Brak elementów w kolejce';
|
||||
@override String get searchSubtitles => 'Szukaj napisów';
|
||||
@override String get language => 'Język';
|
||||
@override String get noSubtitlesFound => 'Nie znaleziono napisów';
|
||||
@override String get subtitleDownloaded => 'Napisy pobrane';
|
||||
@override String get subtitleDownloadFailed => 'Nie udało się pobrać napisów';
|
||||
@override String get searchLanguages => 'Szukaj języków...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsPl {
|
||||
'videoControls.noChaptersAvailable' => 'Brak dostępnych rozdziałów',
|
||||
'videoControls.queue' => 'Kolejka',
|
||||
'videoControls.noQueueItems' => 'Brak elementów w kolejce',
|
||||
'videoControls.searchSubtitles' => 'Szukaj napisów',
|
||||
'videoControls.language' => 'Język',
|
||||
'videoControls.noSubtitlesFound' => 'Nie znaleziono napisów',
|
||||
'videoControls.subtitleDownloaded' => 'Napisy pobrane',
|
||||
'videoControls.subtitleDownloadFailed' => 'Nie udało się pobrać napisów',
|
||||
'videoControls.searchLanguages' => 'Szukaj języków...',
|
||||
'userStatus.admin' => 'Administrator',
|
||||
'userStatus.restricted' => 'Ograniczony',
|
||||
'userStatus.protected' => 'Chroniony',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsPl {
|
||||
'liveTv.guide' => 'Przewodnik',
|
||||
'liveTv.noChannels' => 'Brak dostępnych kanałów',
|
||||
'liveTv.noDvr' => 'Brak skonfigurowanego DVR na żadnym serwerze',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'Brak danych o programach',
|
||||
'liveTv.live' => 'NA ŻYWO',
|
||||
'liveTv.reloadGuide' => 'Odśwież przewodnik',
|
||||
'liveTv.now' => 'Teraz',
|
||||
'liveTv.today' => 'Dzisiaj',
|
||||
'liveTv.midnight' => 'Północ',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Nocą',
|
||||
'liveTv.morning' => 'Rano',
|
||||
'liveTv.daytime' => 'W ciągu dnia',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsPt implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'Nenhum capítulo disponível';
|
||||
@override String get queue => 'Fila';
|
||||
@override String get noQueueItems => 'Nenhum item na fila';
|
||||
@override String get searchSubtitles => 'Pesquisar legendas';
|
||||
@override String get language => 'Idioma';
|
||||
@override String get noSubtitlesFound => 'Nenhuma legenda encontrada';
|
||||
@override String get subtitleDownloaded => 'Legenda baixada';
|
||||
@override String get subtitleDownloadFailed => 'Falha ao baixar legenda';
|
||||
@override String get searchLanguages => 'Pesquisar idiomas...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsPt {
|
||||
'videoControls.noChaptersAvailable' => 'Nenhum capítulo disponível',
|
||||
'videoControls.queue' => 'Fila',
|
||||
'videoControls.noQueueItems' => 'Nenhum item na fila',
|
||||
'videoControls.searchSubtitles' => 'Pesquisar legendas',
|
||||
'videoControls.language' => 'Idioma',
|
||||
'videoControls.noSubtitlesFound' => 'Nenhuma legenda encontrada',
|
||||
'videoControls.subtitleDownloaded' => 'Legenda baixada',
|
||||
'videoControls.subtitleDownloadFailed' => 'Falha ao baixar legenda',
|
||||
'videoControls.searchLanguages' => 'Pesquisar idiomas...',
|
||||
'userStatus.admin' => 'Admin',
|
||||
'userStatus.restricted' => 'Restrito',
|
||||
'userStatus.protected' => 'Protegido',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsPt {
|
||||
'liveTv.guide' => 'Guia',
|
||||
'liveTv.noChannels' => 'Nenhum canal disponível',
|
||||
'liveTv.noDvr' => 'Nenhum DVR configurado em nenhum servidor',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'Nenhum dado de programação disponível',
|
||||
'liveTv.live' => 'AO VIVO',
|
||||
'liveTv.reloadGuide' => 'Recarregar Guia',
|
||||
'liveTv.now' => 'Agora',
|
||||
'liveTv.today' => 'Hoje',
|
||||
'liveTv.midnight' => 'Meia-noite',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Madrugada',
|
||||
'liveTv.morning' => 'Manhã',
|
||||
'liveTv.daytime' => 'Dia',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsRu implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'Главы недоступны';
|
||||
@override String get queue => 'Очередь';
|
||||
@override String get noQueueItems => 'В очереди нет элементов';
|
||||
@override String get searchSubtitles => 'Поиск субтитров';
|
||||
@override String get language => 'Язык';
|
||||
@override String get noSubtitlesFound => 'Субтитры не найдены';
|
||||
@override String get subtitleDownloaded => 'Субтитры загружены';
|
||||
@override String get subtitleDownloadFailed => 'Не удалось загрузить субтитры';
|
||||
@override String get searchLanguages => 'Поиск языков...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsRu {
|
||||
'videoControls.noChaptersAvailable' => 'Главы недоступны',
|
||||
'videoControls.queue' => 'Очередь',
|
||||
'videoControls.noQueueItems' => 'В очереди нет элементов',
|
||||
'videoControls.searchSubtitles' => 'Поиск субтитров',
|
||||
'videoControls.language' => 'Язык',
|
||||
'videoControls.noSubtitlesFound' => 'Субтитры не найдены',
|
||||
'videoControls.subtitleDownloaded' => 'Субтитры загружены',
|
||||
'videoControls.subtitleDownloadFailed' => 'Не удалось загрузить субтитры',
|
||||
'videoControls.searchLanguages' => 'Поиск языков...',
|
||||
'userStatus.admin' => 'Администратор',
|
||||
'userStatus.restricted' => 'Ограниченный',
|
||||
'userStatus.protected' => 'Защищённый',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsRu {
|
||||
'liveTv.guide' => 'Программа',
|
||||
'liveTv.noChannels' => 'Нет доступных каналов',
|
||||
'liveTv.noDvr' => 'DVR не настроен ни на одном сервере',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'Нет данных о программах',
|
||||
'liveTv.live' => 'ЭФИР',
|
||||
'liveTv.reloadGuide' => 'Перезагрузить программу',
|
||||
'liveTv.now' => 'Сейчас',
|
||||
'liveTv.today' => 'Сегодня',
|
||||
'liveTv.midnight' => 'Полночь',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Ночь',
|
||||
'liveTv.morning' => 'Утро',
|
||||
'liveTv.daytime' => 'День',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsSv implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => 'Inga kapitel tillgängliga';
|
||||
@override String get queue => 'Kö';
|
||||
@override String get noQueueItems => 'Inga objekt i kön';
|
||||
@override String get searchSubtitles => 'Sök undertexter';
|
||||
@override String get language => 'Språk';
|
||||
@override String get noSubtitlesFound => 'Inga undertexter hittades';
|
||||
@override String get subtitleDownloaded => 'Undertext nedladdad';
|
||||
@override String get subtitleDownloadFailed => 'Kunde inte ladda ner undertext';
|
||||
@override String get searchLanguages => 'Sök språk...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsSv {
|
||||
'videoControls.noChaptersAvailable' => 'Inga kapitel tillgängliga',
|
||||
'videoControls.queue' => 'Kö',
|
||||
'videoControls.noQueueItems' => 'Inga objekt i kön',
|
||||
'videoControls.searchSubtitles' => 'Sök undertexter',
|
||||
'videoControls.language' => 'Språk',
|
||||
'videoControls.noSubtitlesFound' => 'Inga undertexter hittades',
|
||||
'videoControls.subtitleDownloaded' => 'Undertext nedladdad',
|
||||
'videoControls.subtitleDownloadFailed' => 'Kunde inte ladda ner undertext',
|
||||
'videoControls.searchLanguages' => 'Sök språk...',
|
||||
'userStatus.admin' => 'Admin',
|
||||
'userStatus.restricted' => 'Begränsad',
|
||||
'userStatus.protected' => 'Skyddad',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsSv {
|
||||
'liveTv.guide' => 'Programguide',
|
||||
'liveTv.noChannels' => 'Inga kanaler tillgängliga',
|
||||
'liveTv.noDvr' => 'Ingen DVR konfigurerad på någon server',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => 'Ingen programdata tillgänglig',
|
||||
'liveTv.live' => 'LIVE',
|
||||
'liveTv.reloadGuide' => 'Ladda om programguide',
|
||||
'liveTv.now' => 'Nu',
|
||||
'liveTv.today' => 'Idag',
|
||||
'liveTv.midnight' => 'Midnatt',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => 'Natt',
|
||||
'liveTv.morning' => 'Morgon',
|
||||
'liveTv.daytime' => 'Dagtid',
|
||||
|
||||
@@ -515,6 +515,12 @@ class _TranslationsVideoControlsZh implements TranslationsVideoControlsEn {
|
||||
@override String get noChaptersAvailable => '没有可用的章节';
|
||||
@override String get queue => '播放队列';
|
||||
@override String get noQueueItems => '队列中没有项目';
|
||||
@override String get searchSubtitles => '搜索字幕';
|
||||
@override String get language => '语言';
|
||||
@override String get noSubtitlesFound => '未找到字幕';
|
||||
@override String get subtitleDownloaded => '字幕已下载';
|
||||
@override String get subtitleDownloadFailed => '字幕下载失败';
|
||||
@override String get searchLanguages => '搜索语言...';
|
||||
}
|
||||
|
||||
// Path: userStatus
|
||||
@@ -1669,6 +1675,12 @@ extension on TranslationsZh {
|
||||
'videoControls.noChaptersAvailable' => '没有可用的章节',
|
||||
'videoControls.queue' => '播放队列',
|
||||
'videoControls.noQueueItems' => '队列中没有项目',
|
||||
'videoControls.searchSubtitles' => '搜索字幕',
|
||||
'videoControls.language' => '语言',
|
||||
'videoControls.noSubtitlesFound' => '未找到字幕',
|
||||
'videoControls.subtitleDownloaded' => '字幕已下载',
|
||||
'videoControls.subtitleDownloadFailed' => '字幕下载失败',
|
||||
'videoControls.searchLanguages' => '搜索语言...',
|
||||
'userStatus.admin' => '管理员',
|
||||
'userStatus.restricted' => '受限',
|
||||
'userStatus.protected' => '受保护',
|
||||
@@ -1828,14 +1840,14 @@ extension on TranslationsZh {
|
||||
'liveTv.guide' => '节目指南',
|
||||
'liveTv.noChannels' => '没有可用的频道',
|
||||
'liveTv.noDvr' => '没有服务器配置了DVR',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.noPrograms' => '没有可用的节目数据',
|
||||
'liveTv.live' => '直播',
|
||||
'liveTv.reloadGuide' => '重新加载节目指南',
|
||||
'liveTv.now' => '现在',
|
||||
'liveTv.today' => '今天',
|
||||
'liveTv.midnight' => '午夜',
|
||||
_ => null,
|
||||
} ?? switch (path) {
|
||||
'liveTv.overnight' => '凌晨',
|
||||
'liveTv.morning' => '上午',
|
||||
'liveTv.daytime' => '白天',
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "Kapitel",
|
||||
"noChaptersAvailable": "Inga kapitel tillgängliga",
|
||||
"queue": "Kö",
|
||||
"noQueueItems": "Inga objekt i kön"
|
||||
"noQueueItems": "Inga objekt i kön",
|
||||
"searchSubtitles": "Sök undertexter",
|
||||
"language": "Språk",
|
||||
"noSubtitlesFound": "Inga undertexter hittades",
|
||||
"subtitleDownloaded": "Undertext nedladdad",
|
||||
"subtitleDownloadFailed": "Kunde inte ladda ner undertext",
|
||||
"searchLanguages": "Sök språk..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "Admin",
|
||||
|
||||
@@ -374,7 +374,13 @@
|
||||
"chapters": "章节",
|
||||
"noChaptersAvailable": "没有可用的章节",
|
||||
"queue": "播放队列",
|
||||
"noQueueItems": "队列中没有项目"
|
||||
"noQueueItems": "队列中没有项目",
|
||||
"searchSubtitles": "搜索字幕",
|
||||
"language": "语言",
|
||||
"noSubtitlesFound": "未找到字幕",
|
||||
"subtitleDownloaded": "字幕已下载",
|
||||
"subtitleDownloadFailed": "字幕下载失败",
|
||||
"searchLanguages": "搜索语言..."
|
||||
},
|
||||
"userStatus": {
|
||||
"admin": "管理员",
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
class PlexSubtitleSearchResult {
|
||||
final int id;
|
||||
final String key;
|
||||
final String? codec;
|
||||
final String? language;
|
||||
final String? languageCode;
|
||||
final double? score;
|
||||
final String? providerTitle;
|
||||
final String? title;
|
||||
final String? displayTitle;
|
||||
final bool hearingImpaired;
|
||||
final bool perfectMatch;
|
||||
final bool downloaded;
|
||||
final bool forced;
|
||||
|
||||
PlexSubtitleSearchResult({
|
||||
required this.id,
|
||||
required this.key,
|
||||
this.codec,
|
||||
this.language,
|
||||
this.languageCode,
|
||||
this.score,
|
||||
this.providerTitle,
|
||||
this.title,
|
||||
this.displayTitle,
|
||||
this.hearingImpaired = false,
|
||||
this.perfectMatch = false,
|
||||
this.downloaded = false,
|
||||
this.forced = false,
|
||||
});
|
||||
|
||||
factory PlexSubtitleSearchResult.fromJson(Map<String, dynamic> json) {
|
||||
return PlexSubtitleSearchResult(
|
||||
id: _parseInt(json['id']),
|
||||
key: json['key']?.toString() ?? '',
|
||||
codec: json['codec']?.toString(),
|
||||
language: json['language']?.toString(),
|
||||
languageCode: json['languageCode']?.toString(),
|
||||
score: _parseDouble(json['score']),
|
||||
providerTitle: json['providerTitle']?.toString(),
|
||||
title: json['title']?.toString(),
|
||||
displayTitle: json['displayTitle']?.toString(),
|
||||
hearingImpaired: json['hearingImpaired'] == 1 || json['hearingImpaired'] == true,
|
||||
perfectMatch: json['perfectMatch'] == 1 || json['perfectMatch'] == true,
|
||||
downloaded: json['downloaded'] == 1 || json['downloaded'] == true,
|
||||
forced: json['forced'] == 1 || json['forced'] == true,
|
||||
);
|
||||
}
|
||||
|
||||
static int _parseInt(dynamic v) {
|
||||
if (v is int) return v;
|
||||
if (v is num) return v.toInt();
|
||||
if (v is String) return int.tryParse(v) ?? 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
static double? _parseDouble(dynamic v) {
|
||||
if (v is double) return v;
|
||||
if (v is num) return v.toDouble();
|
||||
if (v is String) return double.tryParse(v);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import '../models/plex_first_character.dart';
|
||||
import '../models/plex_hub.dart';
|
||||
import '../models/plex_library.dart';
|
||||
import '../models/plex_media_info.dart';
|
||||
import '../models/plex_subtitle_search_result.dart';
|
||||
import '../models/plex_media_version.dart';
|
||||
import '../models/plex_metadata.dart';
|
||||
import '../utils/content_utils.dart';
|
||||
@@ -931,6 +932,55 @@ class PlexClient {
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Search for subtitles from external providers (e.g. OpenSubtitles) via the Plex server.
|
||||
/// [language] is an ISO 639-1 two-letter code (e.g. "en", "es").
|
||||
Future<List<PlexSubtitleSearchResult>> searchSubtitles(
|
||||
String ratingKey, {
|
||||
required String language,
|
||||
String? title,
|
||||
int hearingImpaired = 0,
|
||||
int forced = 0,
|
||||
}) async {
|
||||
return _wrapListApiCall<PlexSubtitleSearchResult>(
|
||||
() => _dio.get('/library/metadata/$ratingKey/subtitles', queryParameters: {
|
||||
'language': language,
|
||||
if (title != null && title.isNotEmpty) 'title': title,
|
||||
'hearingImpaired': hearingImpaired,
|
||||
'forced': forced,
|
||||
}),
|
||||
(response) {
|
||||
final container = _getMediaContainer(response);
|
||||
final streams = container?['Stream'] as List? ?? [];
|
||||
return streams.map((s) => PlexSubtitleSearchResult.fromJson(s as Map<String, dynamic>)).toList();
|
||||
},
|
||||
'Failed to search subtitles',
|
||||
);
|
||||
}
|
||||
|
||||
/// Download a subtitle from an external provider and add it to the media item.
|
||||
/// The server downloads the file asynchronously; the new stream appears after a short delay.
|
||||
Future<bool> downloadSubtitle(
|
||||
String ratingKey, {
|
||||
required String key,
|
||||
required String codec,
|
||||
required String language,
|
||||
required bool hearingImpaired,
|
||||
required bool forced,
|
||||
required String providerTitle,
|
||||
}) async {
|
||||
return _wrapBoolApiCall(
|
||||
() => _dio.put('/library/metadata/$ratingKey/subtitles', queryParameters: {
|
||||
'key': key,
|
||||
'codec': codec,
|
||||
'language': language,
|
||||
'hearingImpaired': hearingImpaired ? 1 : 0,
|
||||
'forced': forced ? 1 : 0,
|
||||
'providerTitle': providerTitle,
|
||||
}),
|
||||
'Failed to download subtitle',
|
||||
);
|
||||
}
|
||||
|
||||
/// Search across all libraries including individually shared items.
|
||||
/// Uses /library/search (same endpoint as Plex Web) which finds shared content.
|
||||
/// Only returns movies and shows, filtering out other types.
|
||||
|
||||
@@ -102,6 +102,25 @@ class LanguageCodes {
|
||||
'TW': 'Taiwan',
|
||||
};
|
||||
|
||||
/// Get all available languages as (code, name) pairs, sorted by name.
|
||||
/// Returns ISO 639-1 two-letter codes with their English names.
|
||||
static List<({String code, String name})> getAllLanguages() {
|
||||
if (_codes == null) return [];
|
||||
final languages = <({String code, String name})>[];
|
||||
for (final entry in _codes!.entries) {
|
||||
final code = entry.key;
|
||||
// Skip locale variants like "en-GB" — only include base 2-letter codes
|
||||
if (code.contains('-')) continue;
|
||||
final map = entry.value as Map<String, dynamic>;
|
||||
final name = map['name'] as String?;
|
||||
if (name != null) {
|
||||
languages.add((code: code, name: name));
|
||||
}
|
||||
}
|
||||
languages.sort((a, b) => a.name.compareTo(b.name));
|
||||
return languages;
|
||||
}
|
||||
|
||||
/// Get the English name of a language from its code
|
||||
static String? getLanguageName(String languageCode) {
|
||||
if (_codes == null) return null;
|
||||
|
||||
@@ -40,6 +40,9 @@ class TrackControlsState {
|
||||
final bool subtitlesVisible;
|
||||
final bool showQueueButton;
|
||||
final Function(PlexMetadata)? onQueueItemSelected;
|
||||
final String ratingKey;
|
||||
final String? mediaTitle;
|
||||
final Future<void> Function()? onSubtitleDownloaded;
|
||||
|
||||
const TrackControlsState({
|
||||
this.availableVersions = const [],
|
||||
@@ -75,5 +78,8 @@ class TrackControlsState {
|
||||
this.subtitlesVisible = true,
|
||||
this.showQueueButton = false,
|
||||
this.onQueueItemSelected,
|
||||
this.ratingKey = '',
|
||||
this.mediaTitle,
|
||||
this.onSubtitleDownloaded,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import '../../../models/plex_subtitle_search_result.dart';
|
||||
import '../../../utils/language_codes.dart';
|
||||
import '../../../utils/provider_extensions.dart';
|
||||
import '../../../utils/snackbar_helper.dart';
|
||||
import '../../../widgets/app_icon.dart';
|
||||
import '../../../widgets/focusable_list_tile.dart';
|
||||
import '../../../widgets/overlay_sheet.dart';
|
||||
import 'base_video_control_sheet.dart';
|
||||
|
||||
class SubtitleSearchSheet extends StatefulWidget {
|
||||
final String ratingKey;
|
||||
final String serverId;
|
||||
final String? mediaTitle;
|
||||
final Future<void> Function()? onSubtitleDownloaded;
|
||||
|
||||
const SubtitleSearchSheet({
|
||||
super.key,
|
||||
required this.ratingKey,
|
||||
required this.serverId,
|
||||
this.mediaTitle,
|
||||
this.onSubtitleDownloaded,
|
||||
});
|
||||
|
||||
@override
|
||||
State<SubtitleSearchSheet> createState() => _SubtitleSearchSheetState();
|
||||
}
|
||||
|
||||
class _SubtitleSearchSheetState extends State<SubtitleSearchSheet> {
|
||||
String _languageCode = 'en';
|
||||
String _languageName = 'English';
|
||||
final _titleController = TextEditingController();
|
||||
Timer? _debounceTimer;
|
||||
|
||||
List<PlexSubtitleSearchResult>? _results;
|
||||
bool _isSearching = false;
|
||||
String? _error;
|
||||
String? _downloadingKey;
|
||||
|
||||
// Internal view switching instead of push/pop
|
||||
bool _showLanguagePicker = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initDefaultLanguage();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _search());
|
||||
}
|
||||
|
||||
void _initDefaultLanguage() {
|
||||
final locale = WidgetsBinding.instance.platformDispatcher.locale;
|
||||
final code = locale.languageCode;
|
||||
final name = LanguageCodes.getLanguageName(code);
|
||||
if (name != null) {
|
||||
_languageCode = code;
|
||||
_languageName = name;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_debounceTimer?.cancel();
|
||||
_titleController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> _search() async {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_isSearching = true;
|
||||
_error = null;
|
||||
});
|
||||
|
||||
try {
|
||||
final client = context.getClientForServer(widget.serverId);
|
||||
final title = _titleController.text.trim();
|
||||
final results = await client.searchSubtitles(
|
||||
widget.ratingKey,
|
||||
language: _languageCode,
|
||||
title: title.isEmpty ? null : title,
|
||||
);
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_results = results;
|
||||
_isSearching = false;
|
||||
});
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
setState(() {
|
||||
_error = e.toString();
|
||||
_isSearching = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void _onTitleChanged(String _) {
|
||||
_debounceTimer?.cancel();
|
||||
_debounceTimer = Timer(const Duration(milliseconds: 500), _search);
|
||||
}
|
||||
|
||||
void _onLanguageSelected(String code, String name) {
|
||||
setState(() {
|
||||
_languageCode = code;
|
||||
_languageName = name;
|
||||
_showLanguagePicker = false;
|
||||
});
|
||||
_search();
|
||||
}
|
||||
|
||||
Future<void> _downloadSubtitle(PlexSubtitleSearchResult result) async {
|
||||
if (_downloadingKey != null) return;
|
||||
setState(() => _downloadingKey = result.key);
|
||||
|
||||
try {
|
||||
final client = context.getClientForServer(widget.serverId);
|
||||
final success = await client.downloadSubtitle(
|
||||
widget.ratingKey,
|
||||
key: result.key,
|
||||
codec: result.codec ?? 'srt',
|
||||
language: result.languageCode ?? _languageCode,
|
||||
hearingImpaired: result.hearingImpaired,
|
||||
forced: result.forced,
|
||||
providerTitle: result.providerTitle ?? '',
|
||||
);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
if (success) {
|
||||
await widget.onSubtitleDownloaded?.call();
|
||||
if (!mounted) return;
|
||||
showSuccessSnackBar(context, t.videoControls.subtitleDownloaded);
|
||||
OverlaySheetController.of(context).close();
|
||||
} else {
|
||||
showErrorSnackBar(context, t.videoControls.subtitleDownloadFailed);
|
||||
setState(() => _downloadingKey = null);
|
||||
}
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
showErrorSnackBar(context, t.videoControls.subtitleDownloadFailed);
|
||||
setState(() => _downloadingKey = null);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_showLanguagePicker) {
|
||||
return _LanguagePickerView(
|
||||
currentCode: _languageCode,
|
||||
onSelected: _onLanguageSelected,
|
||||
onBack: () => setState(() => _showLanguagePicker = false),
|
||||
);
|
||||
}
|
||||
|
||||
return BaseVideoControlSheet(
|
||||
title: t.videoControls.searchSubtitles,
|
||||
icon: Symbols.search_rounded,
|
||||
onBack: () => OverlaySheetController.of(context).pop(),
|
||||
child: Column(
|
||||
children: [
|
||||
FocusableListTile(
|
||||
leading: const AppIcon(Symbols.language_rounded),
|
||||
title: Text(t.videoControls.language),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
_languageName,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
const AppIcon(Symbols.arrow_drop_down_rounded),
|
||||
],
|
||||
),
|
||||
onTap: () => setState(() => _showLanguagePicker = true),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: TextField(
|
||||
controller: _titleController,
|
||||
decoration: InputDecoration(
|
||||
hintText: widget.mediaTitle ?? 'Title',
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
),
|
||||
onChanged: _onTitleChanged,
|
||||
textInputAction: TextInputAction.search,
|
||||
onSubmitted: (_) => _search(),
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||
Expanded(child: _buildResults()),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildResults() {
|
||||
if (_isSearching) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
if (_error != null) {
|
||||
return Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Text(_error!, style: TextStyle(color: Theme.of(context).colorScheme.error)),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (_results == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (_results!.isEmpty) {
|
||||
return Center(
|
||||
child: Text(
|
||||
t.videoControls.noSubtitlesFound,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.builder(
|
||||
itemCount: _results!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final result = _results![index];
|
||||
final isDownloading = _downloadingKey == result.key;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
Widget? trailing;
|
||||
if (isDownloading) {
|
||||
trailing = const SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
);
|
||||
} else {
|
||||
final trailingChildren = <Widget>[];
|
||||
if (result.perfectMatch) {
|
||||
trailingChildren.add(
|
||||
const AppIcon(Symbols.star_rounded, fill: 1, color: Color(0xFFCC7B19), size: 16),
|
||||
);
|
||||
}
|
||||
if (result.score != null) {
|
||||
trailingChildren.add(Text(
|
||||
result.score!.toInt().toString(),
|
||||
style: TextStyle(fontSize: 12, color: colorScheme.onSurfaceVariant),
|
||||
));
|
||||
}
|
||||
if (trailingChildren.isNotEmpty) {
|
||||
trailing = Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 4,
|
||||
children: trailingChildren,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return FocusableListTile(
|
||||
title: Text(
|
||||
result.title ?? result.displayTitle ?? 'Unknown',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
result.displayTitle ?? '',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: TextStyle(color: colorScheme.onSurfaceVariant),
|
||||
),
|
||||
trailing: trailing,
|
||||
onTap: isDownloading ? null : () => _downloadSubtitle(result),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Language picker rendered as an internal view, not a pushed page.
|
||||
class _LanguagePickerView extends StatefulWidget {
|
||||
final String currentCode;
|
||||
final void Function(String code, String name) onSelected;
|
||||
final VoidCallback onBack;
|
||||
|
||||
const _LanguagePickerView({
|
||||
required this.currentCode,
|
||||
required this.onSelected,
|
||||
required this.onBack,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_LanguagePickerView> createState() => _LanguagePickerViewState();
|
||||
}
|
||||
|
||||
class _LanguagePickerViewState extends State<_LanguagePickerView> {
|
||||
final _filterController = TextEditingController();
|
||||
late List<({String code, String name})> _allLanguages;
|
||||
List<({String code, String name})> _filteredLanguages = [];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_allLanguages = LanguageCodes.getAllLanguages();
|
||||
_filteredLanguages = _allLanguages;
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_filterController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onFilterChanged(String query) {
|
||||
final lower = query.toLowerCase();
|
||||
setState(() {
|
||||
if (lower.isEmpty) {
|
||||
_filteredLanguages = _allLanguages;
|
||||
} else {
|
||||
_filteredLanguages = _allLanguages.where((l) {
|
||||
return l.name.toLowerCase().contains(lower) || l.code.toLowerCase().contains(lower);
|
||||
}).toList();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BaseVideoControlSheet(
|
||||
title: t.videoControls.language,
|
||||
icon: Symbols.language_rounded,
|
||||
onBack: widget.onBack,
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
child: TextField(
|
||||
controller: _filterController,
|
||||
autofocus: true,
|
||||
decoration: InputDecoration(
|
||||
hintText: t.videoControls.searchLanguages,
|
||||
isDense: true,
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||
border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)),
|
||||
prefixIcon: const Icon(Symbols.search_rounded, size: 20),
|
||||
),
|
||||
onChanged: _onFilterChanged,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: _filteredLanguages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final lang = _filteredLanguages[index];
|
||||
final isSelected = lang.code == widget.currentCode;
|
||||
return FocusableListTile(
|
||||
title: Text(
|
||||
lang.name,
|
||||
style: TextStyle(color: isSelected ? Theme.of(context).colorScheme.primary : null),
|
||||
),
|
||||
trailing: isSelected
|
||||
? AppIcon(Symbols.check_rounded, fill: 1, color: Theme.of(context).colorScheme.primary)
|
||||
: null,
|
||||
onTap: () => widget.onSelected(lang.code, lang.name),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,14 +5,21 @@ import '../../../mpv/mpv.dart';
|
||||
import '../../../i18n/strings.g.dart';
|
||||
import '../../../utils/scroll_utils.dart';
|
||||
import '../../../utils/track_label_builder.dart';
|
||||
import '../../../widgets/app_icon.dart';
|
||||
import '../../../widgets/focusable_list_tile.dart';
|
||||
import '../../../widgets/overlay_sheet.dart';
|
||||
import 'base_video_control_sheet.dart';
|
||||
import 'subtitle_search_sheet.dart';
|
||||
import '../helpers/track_filter_helper.dart';
|
||||
import '../helpers/track_selection_helper.dart';
|
||||
|
||||
/// Combined bottom sheet for selecting audio and subtitle tracks side-by-side.
|
||||
class TrackSheet extends StatelessWidget {
|
||||
final Player player;
|
||||
final String ratingKey;
|
||||
final String serverId;
|
||||
final String? mediaTitle;
|
||||
final Future<void> Function()? onSubtitleDownloaded;
|
||||
final Function(AudioTrack)? onAudioTrackChanged;
|
||||
final Function(SubtitleTrack)? onSubtitleTrackChanged;
|
||||
final Function(SubtitleTrack)? onSecondarySubtitleTrackChanged;
|
||||
@@ -20,6 +27,10 @@ class TrackSheet extends StatelessWidget {
|
||||
const TrackSheet({
|
||||
super.key,
|
||||
required this.player,
|
||||
this.ratingKey = '',
|
||||
this.serverId = '',
|
||||
this.mediaTitle,
|
||||
this.onSubtitleDownloaded,
|
||||
this.onAudioTrackChanged,
|
||||
this.onSubtitleTrackChanged,
|
||||
this.onSecondarySubtitleTrackChanged,
|
||||
@@ -88,6 +99,10 @@ class TrackSheet extends StatelessWidget {
|
||||
tracks: subtitleTracks,
|
||||
selection: selection,
|
||||
player: player,
|
||||
ratingKey: ratingKey,
|
||||
serverId: serverId,
|
||||
mediaTitle: mediaTitle,
|
||||
onSubtitleDownloaded: onSubtitleDownloaded,
|
||||
onTrackChanged: onSubtitleTrackChanged,
|
||||
onSecondaryTrackChanged: onSecondarySubtitleTrackChanged,
|
||||
supportsSecondary: supportsSecondary,
|
||||
@@ -113,6 +128,10 @@ class TrackSheet extends StatelessWidget {
|
||||
tracks: subtitleTracks,
|
||||
selection: selection,
|
||||
player: player,
|
||||
ratingKey: ratingKey,
|
||||
serverId: serverId,
|
||||
mediaTitle: mediaTitle,
|
||||
onSubtitleDownloaded: onSubtitleDownloaded,
|
||||
onTrackChanged: onSubtitleTrackChanged,
|
||||
onSecondaryTrackChanged: onSecondarySubtitleTrackChanged,
|
||||
supportsSecondary: supportsSecondary,
|
||||
@@ -207,6 +226,10 @@ class _SubtitleColumn extends StatefulWidget {
|
||||
final List<SubtitleTrack> tracks;
|
||||
final TrackSelection selection;
|
||||
final Player player;
|
||||
final String ratingKey;
|
||||
final String serverId;
|
||||
final String? mediaTitle;
|
||||
final Future<void> Function()? onSubtitleDownloaded;
|
||||
final Function(SubtitleTrack)? onTrackChanged;
|
||||
final Function(SubtitleTrack)? onSecondaryTrackChanged;
|
||||
final bool supportsSecondary;
|
||||
@@ -216,6 +239,10 @@ class _SubtitleColumn extends StatefulWidget {
|
||||
required this.tracks,
|
||||
required this.selection,
|
||||
required this.player,
|
||||
this.ratingKey = '',
|
||||
this.serverId = '',
|
||||
this.mediaTitle,
|
||||
this.onSubtitleDownloaded,
|
||||
this.onTrackChanged,
|
||||
this.onSecondaryTrackChanged,
|
||||
this.supportsSecondary = false,
|
||||
@@ -358,6 +385,23 @@ class _SubtitleColumnState extends State<_SubtitleColumn> {
|
||||
},
|
||||
),
|
||||
),
|
||||
if (widget.ratingKey.isNotEmpty) ...[
|
||||
Divider(height: 1, color: Theme.of(context).dividerColor),
|
||||
FocusableListTile(
|
||||
leading: const AppIcon(Symbols.search_rounded),
|
||||
title: Text(t.videoControls.searchSubtitles),
|
||||
onTap: () {
|
||||
OverlaySheetController.of(context).push(
|
||||
builder: (_) => SubtitleSearchSheet(
|
||||
ratingKey: widget.ratingKey,
|
||||
serverId: widget.serverId,
|
||||
mediaTitle: widget.mediaTitle,
|
||||
onSubtitleDownloaded: widget.onSubtitleDownloaded,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1170,6 +1170,9 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
subtitlesVisible: _subtitlesVisible,
|
||||
showQueueButton: playbackState.isQueueActive,
|
||||
onQueueItemSelected: playbackState.isQueueActive ? _onQueueItemSelected : null,
|
||||
ratingKey: widget.metadata.ratingKey,
|
||||
mediaTitle: widget.metadata.title,
|
||||
onSubtitleDownloaded: _onSubtitleDownloaded,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1631,6 +1634,12 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
bool _handleGlobalKeyEvent(KeyEvent event) {
|
||||
if (!mounted) return false;
|
||||
|
||||
// When an overlay sheet is open (e.g. subtitle search with text fields),
|
||||
// don't consume key events — let text input work normally.
|
||||
if (OverlaySheetController.maybeOf(context)?.isOpen ?? false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Back key fallback when _focusNode lost focus (TV, or desktop with nav on).
|
||||
// Focus.onKeyEvent won't fire if _focusNode lost focus, so handle ESC here.
|
||||
if ((_videoPlayerNavigationEnabled || PlatformDetector.isTV()) && event.logicalKey.isBackKey) {
|
||||
@@ -2428,6 +2437,51 @@ class _PlexVideoControlsState extends State<PlexVideoControls> with WindowListen
|
||||
videoPlayerState?.navigateToQueueItem(item);
|
||||
}
|
||||
|
||||
Future<void> _onSubtitleDownloaded() async {
|
||||
if (!mounted) return;
|
||||
// Wait for the server to finish downloading the subtitle file
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (!mounted) return;
|
||||
|
||||
try {
|
||||
final client = _getClientForMetadata();
|
||||
final data = await client.getVideoPlaybackData(widget.metadata.ratingKey);
|
||||
if (!mounted || data.mediaInfo == null) return;
|
||||
|
||||
final token = client.config.token;
|
||||
if (token == null) return;
|
||||
|
||||
// Find external subtitle tracks from the refreshed metadata
|
||||
final existingUris = widget.player.state.tracks.subtitle
|
||||
.where((t) => t.uri != null)
|
||||
.map((t) => t.uri!)
|
||||
.toSet();
|
||||
|
||||
for (final plexTrack in data.mediaInfo!.subtitleTracks) {
|
||||
if (!plexTrack.isExternal) continue;
|
||||
final url = plexTrack.getSubtitleUrl(client.config.baseUrl, token);
|
||||
if (url == null) continue;
|
||||
// Skip tracks already loaded in the player
|
||||
if (existingUris.any((uri) => uri.contains(plexTrack.key!))) continue;
|
||||
|
||||
await widget.player.addSubtitleTrack(
|
||||
uri: url,
|
||||
title: plexTrack.displayTitle ?? plexTrack.language ?? 'Downloaded',
|
||||
language: plexTrack.languageCode,
|
||||
select: true,
|
||||
);
|
||||
|
||||
// Save the selection on the server so it persists across sessions
|
||||
final partId = data.mediaInfo!.partId;
|
||||
if (partId != null) {
|
||||
await client.selectStreams(partId, subtitleStreamID: plexTrack.id);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
appLogger.w('Failed to refresh subtitles after download', error: e);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _switchMediaVersion(int newMediaIndex) async {
|
||||
if (newMediaIndex == widget.selectedMediaIndex) {
|
||||
return; // Already using this version
|
||||
|
||||
@@ -97,6 +97,9 @@ class TrackChapterControls extends StatelessWidget {
|
||||
bool get subtitlesVisible => trackControlsState.subtitlesVisible;
|
||||
bool get showQueueButton => trackControlsState.showQueueButton;
|
||||
Function(PlexMetadata)? get onQueueItemSelected => trackControlsState.onQueueItemSelected;
|
||||
String get ratingKey => trackControlsState.ratingKey;
|
||||
String? get mediaTitle => trackControlsState.mediaTitle;
|
||||
Future<void> Function()? get onSubtitleDownloaded => trackControlsState.onSubtitleDownloaded;
|
||||
|
||||
/// Handle key event for button navigation
|
||||
KeyEventResult _handleButtonKeyEvent(FocusNode _, KeyEvent event, int index, int totalButtons) {
|
||||
@@ -257,6 +260,10 @@ class TrackChapterControls extends StatelessWidget {
|
||||
.show(
|
||||
builder: (_) => TrackSheet(
|
||||
player: player,
|
||||
ratingKey: ratingKey,
|
||||
serverId: serverId,
|
||||
mediaTitle: mediaTitle,
|
||||
onSubtitleDownloaded: onSubtitleDownloaded,
|
||||
onAudioTrackChanged: onAudioTrackChanged,
|
||||
onSubtitleTrackChanged: onSubtitleTrackChanged,
|
||||
onSecondarySubtitleTrackChanged: onSecondarySubtitleTrackChanged,
|
||||
|
||||
Reference in New Issue
Block a user