fix: prevent async state and platform regressions

This commit is contained in:
edde746
2026-07-13 00:03:52 +02:00
parent 26d14f7cb0
commit e03edad824
77 changed files with 1210 additions and 270 deletions
+7
View File
@@ -49,6 +49,13 @@ jobs:
- name: Verify translation hygiene - name: Verify translation hygiene
run: python3 scripts/clean_translations.py --check --strict run: python3 scripts/clean_translations.py --check --strict
- name: Verify workflow and script guards
run: |
python3 scripts/check_build_workflow.py
python3 scripts/check_update_packages_workflow.py
python3 scripts/test_pubspec_version.py
python3 scripts/test_clean_translations.py
- name: Verify formatting - name: Verify formatting
run: | run: |
paths=(lib) paths=(lib)
+2
View File
@@ -18,6 +18,8 @@ linter:
avoid_print: true avoid_print: true
dart_code_linter: dart_code_linter:
rules-exclude:
- "test/**"
metrics-exclude: metrics-exclude:
- "test/**" - "test/**"
@@ -23,13 +23,20 @@ internal object MediaCodecQuery {
return null return null
} }
fun isHardwareAccelerated(info: MediaCodecInfo): Boolean { fun isHardwareAccelerated(info: MediaCodecInfo): Boolean = isHardwareAccelerated(Build.VERSION.SDK_INT, info.isHardwareAccelerated, info.name)
// API 29 added the manufacturer-provided classification. Older releases
// expose only component names, so retain the legacy software-name fallback. internal fun isHardwareAccelerated(
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { sdkInt: Int,
info.isHardwareAccelerated platformReportsHardware: Boolean,
name: String
): Boolean {
// API 29 added manufacturer-provided classification, but some Codec2
// builders still flag known software components as hardware. Require both
// signals there; older releases expose only component names.
return if (sdkInt >= Build.VERSION_CODES.Q) {
platformReportsHardware && !isSoftwareCodecName(name)
} else { } else {
!isSoftwareCodecName(info.name) !isSoftwareCodecName(name)
} }
} }
@@ -31,4 +31,17 @@ class MediaCodecQueryTest {
assertFalse("expected hardware codec: $name", MediaCodecQuery.isSoftwareCodecName(name)) assertFalse("expected hardware codec: $name", MediaCodecQuery.isSoftwareCodecName(name))
} }
} }
@Test
fun api29RequiresPlatformHardwareFlagAndNonSoftwareComponentName() {
assertFalse(MediaCodecQuery.isHardwareAccelerated(29, true, "c2.ffmpeg.aac.decoder"))
assertFalse(MediaCodecQuery.isHardwareAccelerated(29, false, "c2.qti.avc.decoder"))
assertTrue(MediaCodecQuery.isHardwareAccelerated(29, true, "c2.qti.avc.decoder"))
}
@Test
fun preApi29RetainsNameBasedFallback() {
assertFalse(MediaCodecQuery.isHardwareAccelerated(28, true, "OMX.google.h264.decoder"))
assertTrue(MediaCodecQuery.isHardwareAccelerated(28, false, "OMX.qcom.video.decoder.avc"))
}
} }
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Премахнато от продължаване на гледането", "removedFromContinueWatching": "Премахнато от продължаване на гледането",
"errorLoading": "Грешка: ${error}", "errorLoading": "Грешка: ${error}",
"streamInterrupted": "Потокът прекъсна. Натиснете „Пусни“ или превъртете, за да опитате отново.", "streamInterrupted": "Потокът прекъсна. Натиснете „Пусни“ или превъртете, за да опитате отново.",
"liveStreamInterrupted": "Потокът на живо прекъсна. Натиснете „Пусни“, за да опитате отново.",
"fileInfoNotAvailable": "Информацията за файла не е налична", "fileInfoNotAvailable": "Информацията за файла не е налична",
"errorLoadingFileInfo": "Грешка при зареждане на информация за файла: ${error}", "errorLoadingFileInfo": "Грешка при зареждане на информация за файла: ${error}",
"errorLoadingSeries": "Грешка при зареждане на сериала", "errorLoadingSeries": "Грешка при зареждане на сериала",
@@ -916,6 +917,7 @@
"watchChannel": "Гледай канал", "watchChannel": "Гледай канал",
"favorites": "Любими", "favorites": "Любими",
"reorderFavorites": "Пренареди любимите", "reorderFavorites": "Пренареди любимите",
"favoritesLoadFailed": "Любимите не можаха да се заредят. Проверете връзката си и опитайте отново.",
"joinSession": "Присъедини се към текуща сесия", "joinSession": "Присъедини се към текуща сесия",
"watchFromStart": "Гледай от началото (преди ${minutes} мин)", "watchFromStart": "Гледай от началото (преди ${minutes} мин)",
"watchLive": "Гледай на живо", "watchLive": "Гледай на живо",
@@ -1140,6 +1142,7 @@
"customAmount": "Персонален брой...", "customAmount": "Персонален брой...",
"includeSpecials": "Включи специалните", "includeSpecials": "Включи специалните",
"howManyEpisodes": "Колко епизода?", "howManyEpisodes": "Колко епизода?",
"invalidEpisodeCount": "Въведете валиден брой епизоди.",
"keepSynced": "Поддържай синхронизирано", "keepSynced": "Поддържай синхронизирано",
"downloadOnce": "Изтегли еднократно", "downloadOnce": "Изтегли еднократно",
"keepNUnwatched": "Пази ${count} негледани", "keepNUnwatched": "Пази ${count} негледани",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Fjernet fra Fortsæt med at se", "removedFromContinueWatching": "Fjernet fra Fortsæt med at se",
"errorLoading": "Fejl: ${error}", "errorLoading": "Fejl: ${error}",
"streamInterrupted": "Streamen blev afbrudt. Tryk på afspil, eller spol for at prøve igen.", "streamInterrupted": "Streamen blev afbrudt. Tryk på afspil, eller spol for at prøve igen.",
"liveStreamInterrupted": "Livestreamen blev afbrudt. Tryk på afspil for at prøve igen.",
"fileInfoNotAvailable": "Filinfo ikke tilgængelig", "fileInfoNotAvailable": "Filinfo ikke tilgængelig",
"errorLoadingFileInfo": "Fejl ved indlæsning af filinfo: ${error}", "errorLoadingFileInfo": "Fejl ved indlæsning af filinfo: ${error}",
"errorLoadingSeries": "Fejl ved indlæsning af serie", "errorLoadingSeries": "Fejl ved indlæsning af serie",
@@ -916,6 +917,7 @@
"watchChannel": "Se kanal", "watchChannel": "Se kanal",
"favorites": "Favoritter", "favorites": "Favoritter",
"reorderFavorites": "Omarranger favoritter", "reorderFavorites": "Omarranger favoritter",
"favoritesLoadFailed": "Favoritter kunne ikke indlæses. Kontrollér forbindelsen, og prøv igen.",
"joinSession": "Deltag i igangværende session", "joinSession": "Deltag i igangværende session",
"watchFromStart": "Se fra start (${minutes} min siden)", "watchFromStart": "Se fra start (${minutes} min siden)",
"watchLive": "Se live", "watchLive": "Se live",
@@ -1140,6 +1142,7 @@
"customAmount": "Angiv antal...", "customAmount": "Angiv antal...",
"includeSpecials": "Inkludér specials", "includeSpecials": "Inkludér specials",
"howManyEpisodes": "Hvor mange episoder?", "howManyEpisodes": "Hvor mange episoder?",
"invalidEpisodeCount": "Indtast et gyldigt antal episoder.",
"keepSynced": "Hold synkroniseret", "keepSynced": "Hold synkroniseret",
"downloadOnce": "Download én gang", "downloadOnce": "Download én gang",
"keepNUnwatched": "Behold ${count} usete", "keepNUnwatched": "Behold ${count} usete",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Aus Weiterschauen' entfernt", "removedFromContinueWatching": "Aus Weiterschauen' entfernt",
"errorLoading": "Fehler: ${error}", "errorLoading": "Fehler: ${error}",
"streamInterrupted": "Der Stream wurde unterbrochen. Drücke auf Wiedergabe oder spule, um es erneut zu versuchen.", "streamInterrupted": "Der Stream wurde unterbrochen. Drücke auf Wiedergabe oder spule, um es erneut zu versuchen.",
"liveStreamInterrupted": "Der Livestream wurde unterbrochen. Drücke auf Wiedergabe, um es erneut zu versuchen.",
"fileInfoNotAvailable": "Dateiinfo nicht verfügbar", "fileInfoNotAvailable": "Dateiinfo nicht verfügbar",
"errorLoadingFileInfo": "Fehler beim Laden der Dateiinfo: ${error}", "errorLoadingFileInfo": "Fehler beim Laden der Dateiinfo: ${error}",
"errorLoadingSeries": "Fehler beim Laden der Serie", "errorLoadingSeries": "Fehler beim Laden der Serie",
@@ -916,6 +917,7 @@
"watchChannel": "Kanal ansehen", "watchChannel": "Kanal ansehen",
"favorites": "Favoriten", "favorites": "Favoriten",
"reorderFavorites": "Favoriten sortieren", "reorderFavorites": "Favoriten sortieren",
"favoritesLoadFailed": "Favoriten konnten nicht geladen werden. Überprüfe deine Verbindung und versuche es erneut.",
"joinSession": "Laufender Sitzung beitreten", "joinSession": "Laufender Sitzung beitreten",
"watchFromStart": "Von Anfang an ansehen (vor ${minutes} Min.)", "watchFromStart": "Von Anfang an ansehen (vor ${minutes} Min.)",
"watchLive": "Live ansehen", "watchLive": "Live ansehen",
@@ -1140,6 +1142,7 @@
"customAmount": "Eigene Anzahl...", "customAmount": "Eigene Anzahl...",
"includeSpecials": "Specials einschließen", "includeSpecials": "Specials einschließen",
"howManyEpisodes": "Wie viele Episoden?", "howManyEpisodes": "Wie viele Episoden?",
"invalidEpisodeCount": "Gib eine gültige Episodenanzahl ein.",
"keepSynced": "Synchronisiert halten", "keepSynced": "Synchronisiert halten",
"downloadOnce": "Einmal herunterladen", "downloadOnce": "Einmal herunterladen",
"keepNUnwatched": "${count} ungesehene behalten", "keepNUnwatched": "${count} ungesehene behalten",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Removed from Continue Watching", "removedFromContinueWatching": "Removed from Continue Watching",
"errorLoading": "Error: ${error}", "errorLoading": "Error: ${error}",
"streamInterrupted": "The stream was interrupted. Press play or seek to retry.", "streamInterrupted": "The stream was interrupted. Press play or seek to retry.",
"liveStreamInterrupted": "The live stream was interrupted. Press play to retry.",
"fileInfoNotAvailable": "File information not available", "fileInfoNotAvailable": "File information not available",
"errorLoadingFileInfo": "Error loading file info: ${error}", "errorLoadingFileInfo": "Error loading file info: ${error}",
"errorLoadingSeries": "Error loading series", "errorLoadingSeries": "Error loading series",
@@ -916,6 +917,7 @@
"watchChannel": "Watch Channel", "watchChannel": "Watch Channel",
"favorites": "Favorites", "favorites": "Favorites",
"reorderFavorites": "Reorder Favorites", "reorderFavorites": "Reorder Favorites",
"favoritesLoadFailed": "Could not load favorites. Check your connection and try again.",
"joinSession": "Join Session in Progress", "joinSession": "Join Session in Progress",
"watchFromStart": "Watch from start (${minutes} min ago)", "watchFromStart": "Watch from start (${minutes} min ago)",
"watchLive": "Watch Live", "watchLive": "Watch Live",
@@ -1140,6 +1142,7 @@
"customAmount": "Custom amount...", "customAmount": "Custom amount...",
"includeSpecials": "Include Specials", "includeSpecials": "Include Specials",
"howManyEpisodes": "How many episodes?", "howManyEpisodes": "How many episodes?",
"invalidEpisodeCount": "Enter a valid episode count.",
"keepSynced": "Keep synced", "keepSynced": "Keep synced",
"downloadOnce": "Download once", "downloadOnce": "Download once",
"keepNUnwatched": "Keep ${count} unwatched", "keepNUnwatched": "Keep ${count} unwatched",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Eliminado de Seguir Viendo", "removedFromContinueWatching": "Eliminado de Seguir Viendo",
"errorLoading": "Error: ${error}", "errorLoading": "Error: ${error}",
"streamInterrupted": "La reproducción se interrumpió. Pulsa reproducir o avanza para volver a intentarlo.", "streamInterrupted": "La reproducción se interrumpió. Pulsa reproducir o avanza para volver a intentarlo.",
"liveStreamInterrupted": "La transmisión en vivo se interrumpió. Pulsa reproducir para volver a intentarlo.",
"fileInfoNotAvailable": "Información de archivo no disponible", "fileInfoNotAvailable": "Información de archivo no disponible",
"errorLoadingFileInfo": "Error al cargar info de archivo: ${error}", "errorLoadingFileInfo": "Error al cargar info de archivo: ${error}",
"errorLoadingSeries": "Error al cargar la serie", "errorLoadingSeries": "Error al cargar la serie",
@@ -916,6 +917,7 @@
"watchChannel": "Ver canal", "watchChannel": "Ver canal",
"favorites": "Favoritos", "favorites": "Favoritos",
"reorderFavorites": "Reordenar favoritos", "reorderFavorites": "Reordenar favoritos",
"favoritesLoadFailed": "No se pudieron cargar los favoritos. Comprueba tu conexión e inténtalo de nuevo.",
"joinSession": "Unirse a sesión en curso", "joinSession": "Unirse a sesión en curso",
"watchFromStart": "Ver desde el inicio (hace ${minutes} min)", "watchFromStart": "Ver desde el inicio (hace ${minutes} min)",
"watchLive": "Ver en vivo", "watchLive": "Ver en vivo",
@@ -1140,6 +1142,7 @@
"customAmount": "Cantidad personalizada...", "customAmount": "Cantidad personalizada...",
"includeSpecials": "Incluir especiales", "includeSpecials": "Incluir especiales",
"howManyEpisodes": "¿Cuántos episodios?", "howManyEpisodes": "¿Cuántos episodios?",
"invalidEpisodeCount": "Introduce un número de episodios válido.",
"keepSynced": "Mantener sincronizado", "keepSynced": "Mantener sincronizado",
"downloadOnce": "Descargar una vez", "downloadOnce": "Descargar una vez",
"keepNUnwatched": "Mantener ${count} sin ver", "keepNUnwatched": "Mantener ${count} sin ver",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Supprimer de \"Continuer à regarder\"", "removedFromContinueWatching": "Supprimer de \"Continuer à regarder\"",
"errorLoading": "Erreur: ${error}", "errorLoading": "Erreur: ${error}",
"streamInterrupted": "La lecture a été interrompue. Appuyez sur Lecture ou avancez pour réessayer.", "streamInterrupted": "La lecture a été interrompue. Appuyez sur Lecture ou avancez pour réessayer.",
"liveStreamInterrupted": "Le direct a été interrompu. Appuyez sur Lecture pour réessayer.",
"fileInfoNotAvailable": "Informations sur le fichier non disponibles", "fileInfoNotAvailable": "Informations sur le fichier non disponibles",
"errorLoadingFileInfo": "Erreur lors du chargement des informations sur le fichier: ${error}", "errorLoadingFileInfo": "Erreur lors du chargement des informations sur le fichier: ${error}",
"errorLoadingSeries": "Erreur lors du chargement de la série", "errorLoadingSeries": "Erreur lors du chargement de la série",
@@ -916,6 +917,7 @@
"watchChannel": "Regarder la chaîne", "watchChannel": "Regarder la chaîne",
"favorites": "Favoris", "favorites": "Favoris",
"reorderFavorites": "Réorganiser les favoris", "reorderFavorites": "Réorganiser les favoris",
"favoritesLoadFailed": "Impossible de charger les favoris. Vérifiez votre connexion et réessayez.",
"joinSession": "Rejoindre la session en cours", "joinSession": "Rejoindre la session en cours",
"watchFromStart": "Regarder depuis le début (il y a ${minutes} min)", "watchFromStart": "Regarder depuis le début (il y a ${minutes} min)",
"watchLive": "Regarder en direct", "watchLive": "Regarder en direct",
@@ -1140,6 +1142,7 @@
"customAmount": "Quantité personnalisée...", "customAmount": "Quantité personnalisée...",
"includeSpecials": "Inclure les spéciaux", "includeSpecials": "Inclure les spéciaux",
"howManyEpisodes": "Combien d'épisodes ?", "howManyEpisodes": "Combien d'épisodes ?",
"invalidEpisodeCount": "Saisissez un nombre d'épisodes valide.",
"keepSynced": "Garder synchronisé", "keepSynced": "Garder synchronisé",
"downloadOnce": "Télécharger une fois", "downloadOnce": "Télécharger une fois",
"keepNUnwatched": "Garder ${count} non vus", "keepNUnwatched": "Garder ${count} non vus",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Rimosso da Continua a guardare", "removedFromContinueWatching": "Rimosso da Continua a guardare",
"errorLoading": "Errore: ${error}", "errorLoading": "Errore: ${error}",
"streamInterrupted": "La riproduzione si è interrotta. Premi Riproduci o scorri per riprovare.", "streamInterrupted": "La riproduzione si è interrotta. Premi Riproduci o scorri per riprovare.",
"liveStreamInterrupted": "La diretta si è interrotta. Premi Riproduci per riprovare.",
"fileInfoNotAvailable": "Informazioni sul file non disponibili", "fileInfoNotAvailable": "Informazioni sul file non disponibili",
"errorLoadingFileInfo": "Errore caricamento informazioni sul file: ${error}", "errorLoadingFileInfo": "Errore caricamento informazioni sul file: ${error}",
"errorLoadingSeries": "Errore caricamento serie", "errorLoadingSeries": "Errore caricamento serie",
@@ -916,6 +917,7 @@
"watchChannel": "Guarda canale", "watchChannel": "Guarda canale",
"favorites": "Preferiti", "favorites": "Preferiti",
"reorderFavorites": "Riordina preferiti", "reorderFavorites": "Riordina preferiti",
"favoritesLoadFailed": "Impossibile caricare i preferiti. Controlla la connessione e riprova.",
"joinSession": "Partecipa alla sessione in corso", "joinSession": "Partecipa alla sessione in corso",
"watchFromStart": "Guarda dall'inizio (${minutes} min fa)", "watchFromStart": "Guarda dall'inizio (${minutes} min fa)",
"watchLive": "Guarda in diretta", "watchLive": "Guarda in diretta",
@@ -1140,6 +1142,7 @@
"customAmount": "Quantità personalizzata...", "customAmount": "Quantità personalizzata...",
"includeSpecials": "Includi gli speciali", "includeSpecials": "Includi gli speciali",
"howManyEpisodes": "Quanti episodi?", "howManyEpisodes": "Quanti episodi?",
"invalidEpisodeCount": "Inserisci un numero di episodi valido.",
"keepSynced": "Mantieni sincronizzato", "keepSynced": "Mantieni sincronizzato",
"downloadOnce": "Scarica una volta", "downloadOnce": "Scarica una volta",
"keepNUnwatched": "Mantieni ${count} non visti", "keepNUnwatched": "Mantieni ${count} non visti",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "視聴中から削除しました", "removedFromContinueWatching": "視聴中から削除しました",
"errorLoading": "エラー: ${error}", "errorLoading": "エラー: ${error}",
"streamInterrupted": "ストリームが中断されました。再生を押すかシークして再試行してください。", "streamInterrupted": "ストリームが中断されました。再生を押すかシークして再試行してください。",
"liveStreamInterrupted": "ライブストリームが中断されました。再生を押して再試行してください。",
"fileInfoNotAvailable": "ファイル情報が利用できません", "fileInfoNotAvailable": "ファイル情報が利用できません",
"errorLoadingFileInfo": "ファイル情報の読み込みエラー: ${error}", "errorLoadingFileInfo": "ファイル情報の読み込みエラー: ${error}",
"errorLoadingSeries": "シリーズの読み込みエラー", "errorLoadingSeries": "シリーズの読み込みエラー",
@@ -915,6 +916,7 @@
"watchChannel": "チャンネルを視聴", "watchChannel": "チャンネルを視聴",
"favorites": "お気に入り", "favorites": "お気に入り",
"reorderFavorites": "お気に入りを並べ替え", "reorderFavorites": "お気に入りを並べ替え",
"favoritesLoadFailed": "お気に入りを読み込めませんでした。接続を確認してもう一度お試しください。",
"joinSession": "進行中のセッションに参加", "joinSession": "進行中のセッションに参加",
"watchFromStart": "最初から視聴(${minutes}分前に開始)", "watchFromStart": "最初から視聴(${minutes}分前に開始)",
"watchLive": "ライブで視聴", "watchLive": "ライブで視聴",
@@ -1138,6 +1140,7 @@
"customAmount": "数を指定...", "customAmount": "数を指定...",
"includeSpecials": "スペシャルを含める", "includeSpecials": "スペシャルを含める",
"howManyEpisodes": "何エピソード?", "howManyEpisodes": "何エピソード?",
"invalidEpisodeCount": "有効なエピソード数を入力してください。",
"keepSynced": "同期を維持", "keepSynced": "同期を維持",
"downloadOnce": "一度だけダウンロード", "downloadOnce": "一度だけダウンロード",
"keepNUnwatched": "未視聴を${count}件保持", "keepNUnwatched": "未視聴を${count}件保持",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "계속 시청 목록에서 제거됨", "removedFromContinueWatching": "계속 시청 목록에서 제거됨",
"errorLoading": "오류: ${error}", "errorLoading": "오류: ${error}",
"streamInterrupted": "스트림이 중단되었습니다. 재생을 누르거나 탐색하여 다시 시도하세요.", "streamInterrupted": "스트림이 중단되었습니다. 재생을 누르거나 탐색하여 다시 시도하세요.",
"liveStreamInterrupted": "라이브 스트림이 중단되었습니다. 재생을 눌러 다시 시도하세요.",
"fileInfoNotAvailable": "파일 정보가 없습니다", "fileInfoNotAvailable": "파일 정보가 없습니다",
"errorLoadingFileInfo": "파일 정보 로딩 중 오류: ${error}", "errorLoadingFileInfo": "파일 정보 로딩 중 오류: ${error}",
"errorLoadingSeries": "시리즈 로딩 중 오류", "errorLoadingSeries": "시리즈 로딩 중 오류",
@@ -915,6 +916,7 @@
"watchChannel": "채널 시청", "watchChannel": "채널 시청",
"favorites": "즐겨찾기", "favorites": "즐겨찾기",
"reorderFavorites": "즐겨찾기 순서 변경", "reorderFavorites": "즐겨찾기 순서 변경",
"favoritesLoadFailed": "즐겨찾기를 불러올 수 없습니다. 연결을 확인하고 다시 시도하세요.",
"joinSession": "진행 중인 세션 참여", "joinSession": "진행 중인 세션 참여",
"watchFromStart": "처음부터 시청 (${minutes}분 전 시작)", "watchFromStart": "처음부터 시청 (${minutes}분 전 시작)",
"watchLive": "실시간 시청", "watchLive": "실시간 시청",
@@ -1138,6 +1140,7 @@
"customAmount": "직접 입력...", "customAmount": "직접 입력...",
"includeSpecials": "스페셜 포함", "includeSpecials": "스페셜 포함",
"howManyEpisodes": "몇 개의 에피소드?", "howManyEpisodes": "몇 개의 에피소드?",
"invalidEpisodeCount": "올바른 에피소드 수를 입력하세요.",
"keepSynced": "동기화 유지", "keepSynced": "동기화 유지",
"downloadOnce": "한 번만 다운로드", "downloadOnce": "한 번만 다운로드",
"keepNUnwatched": "미시청 ${count}개 유지", "keepNUnwatched": "미시청 ${count}개 유지",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Fjernet fra Fortsett å se", "removedFromContinueWatching": "Fjernet fra Fortsett å se",
"errorLoading": "Feil: ${error}", "errorLoading": "Feil: ${error}",
"streamInterrupted": "Avspillingen ble avbrutt. Trykk på Spill av eller spol for å prøve på nytt.", "streamInterrupted": "Avspillingen ble avbrutt. Trykk på Spill av eller spol for å prøve på nytt.",
"liveStreamInterrupted": "Direktesendingen ble avbrutt. Trykk på Spill av for å prøve på nytt.",
"fileInfoNotAvailable": "Filinformasjon ikke tilgjengelig", "fileInfoNotAvailable": "Filinformasjon ikke tilgjengelig",
"errorLoadingFileInfo": "Feil ved lasting av filinformasjon: ${error}", "errorLoadingFileInfo": "Feil ved lasting av filinformasjon: ${error}",
"errorLoadingSeries": "Feil ved lasting av serie", "errorLoadingSeries": "Feil ved lasting av serie",
@@ -916,6 +917,7 @@
"watchChannel": "Se kanal", "watchChannel": "Se kanal",
"favorites": "Favoritter", "favorites": "Favoritter",
"reorderFavorites": "Endre rekkefølge på favoritter", "reorderFavorites": "Endre rekkefølge på favoritter",
"favoritesLoadFailed": "Kunne ikke laste inn favoritter. Kontroller tilkoblingen og prøv på nytt.",
"joinSession": "Bli med i pågående økt", "joinSession": "Bli med i pågående økt",
"watchFromStart": "Se fra starten (${minutes} min siden)", "watchFromStart": "Se fra starten (${minutes} min siden)",
"watchLive": "Se direkte", "watchLive": "Se direkte",
@@ -1140,6 +1142,7 @@
"customAmount": "Egendefinert antall...", "customAmount": "Egendefinert antall...",
"includeSpecials": "Inkluder spesialepisoder", "includeSpecials": "Inkluder spesialepisoder",
"howManyEpisodes": "Hvor mange episoder?", "howManyEpisodes": "Hvor mange episoder?",
"invalidEpisodeCount": "Angi et gyldig antall episoder.",
"keepSynced": "Hold synkronisert", "keepSynced": "Hold synkronisert",
"downloadOnce": "Last ned én gang", "downloadOnce": "Last ned én gang",
"keepNUnwatched": "Behold ${count} usette", "keepNUnwatched": "Behold ${count} usette",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Verwijderd uit Doorgaan met kijken", "removedFromContinueWatching": "Verwijderd uit Doorgaan met kijken",
"errorLoading": "Fout: ${error}", "errorLoading": "Fout: ${error}",
"streamInterrupted": "De stream is onderbroken. Druk op afspelen of spoel om het opnieuw te proberen.", "streamInterrupted": "De stream is onderbroken. Druk op afspelen of spoel om het opnieuw te proberen.",
"liveStreamInterrupted": "De livestream is onderbroken. Druk op afspelen om het opnieuw te proberen.",
"fileInfoNotAvailable": "Bestand informatie niet beschikbaar", "fileInfoNotAvailable": "Bestand informatie niet beschikbaar",
"errorLoadingFileInfo": "Fout bij laden bestand info: ${error}", "errorLoadingFileInfo": "Fout bij laden bestand info: ${error}",
"errorLoadingSeries": "Fout bij laden serie", "errorLoadingSeries": "Fout bij laden serie",
@@ -916,6 +917,7 @@
"watchChannel": "Kanaal bekijken", "watchChannel": "Kanaal bekijken",
"favorites": "Favorieten", "favorites": "Favorieten",
"reorderFavorites": "Favorieten herordenen", "reorderFavorites": "Favorieten herordenen",
"favoritesLoadFailed": "Favorieten konden niet worden geladen. Controleer je verbinding en probeer het opnieuw.",
"joinSession": "Deelnemen aan lopende sessie", "joinSession": "Deelnemen aan lopende sessie",
"watchFromStart": "Kijk vanaf het begin (${minutes} min geleden)", "watchFromStart": "Kijk vanaf het begin (${minutes} min geleden)",
"watchLive": "Live kijken", "watchLive": "Live kijken",
@@ -1140,6 +1142,7 @@
"customAmount": "Aangepast aantal...", "customAmount": "Aangepast aantal...",
"includeSpecials": "Specials opnemen", "includeSpecials": "Specials opnemen",
"howManyEpisodes": "Hoeveel afleveringen?", "howManyEpisodes": "Hoeveel afleveringen?",
"invalidEpisodeCount": "Voer een geldig aantal afleveringen in.",
"keepSynced": "Gesynchroniseerd houden", "keepSynced": "Gesynchroniseerd houden",
"downloadOnce": "Eenmalig downloaden", "downloadOnce": "Eenmalig downloaden",
"keepNUnwatched": "${count} onbekeken behouden", "keepNUnwatched": "${count} onbekeken behouden",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Usunięto z kontynuowania oglądania", "removedFromContinueWatching": "Usunięto z kontynuowania oglądania",
"errorLoading": "Błąd: ${error}", "errorLoading": "Błąd: ${error}",
"streamInterrupted": "Strumień został przerwany. Naciśnij odtwarzanie lub przewiń, aby spróbować ponownie.", "streamInterrupted": "Strumień został przerwany. Naciśnij odtwarzanie lub przewiń, aby spróbować ponownie.",
"liveStreamInterrupted": "Transmisja na żywo została przerwana. Naciśnij odtwarzanie, aby spróbować ponownie.",
"fileInfoNotAvailable": "Informacje o pliku niedostępne", "fileInfoNotAvailable": "Informacje o pliku niedostępne",
"errorLoadingFileInfo": "Błąd ładowania informacji o pliku: ${error}", "errorLoadingFileInfo": "Błąd ładowania informacji o pliku: ${error}",
"errorLoadingSeries": "Błąd ładowania serialu", "errorLoadingSeries": "Błąd ładowania serialu",
@@ -918,6 +919,7 @@
"watchChannel": "Oglądaj kanał", "watchChannel": "Oglądaj kanał",
"favorites": "Ulubione", "favorites": "Ulubione",
"reorderFavorites": "Zmień kolejność ulubionych", "reorderFavorites": "Zmień kolejność ulubionych",
"favoritesLoadFailed": "Nie udało się wczytać ulubionych. Sprawdź połączenie i spróbuj ponownie.",
"joinSession": "Dołącz do trwającej sesji", "joinSession": "Dołącz do trwającej sesji",
"watchFromStart": "Oglądaj od początku (${minutes} min temu)", "watchFromStart": "Oglądaj od początku (${minutes} min temu)",
"watchLive": "Oglądaj na żywo", "watchLive": "Oglądaj na żywo",
@@ -1144,6 +1146,7 @@
"customAmount": "Własna ilość...", "customAmount": "Własna ilość...",
"includeSpecials": "Uwzględnij odcinki specjalne", "includeSpecials": "Uwzględnij odcinki specjalne",
"howManyEpisodes": "Ile odcinków?", "howManyEpisodes": "Ile odcinków?",
"invalidEpisodeCount": "Wprowadź prawidłową liczbę odcinków.",
"keepSynced": "Synchronizuj na bieżąco", "keepSynced": "Synchronizuj na bieżąco",
"downloadOnce": "Pobierz raz", "downloadOnce": "Pobierz raz",
"keepNUnwatched": "Zachowaj ${count} nieobejrzanych", "keepNUnwatched": "Zachowaj ${count} nieobejrzanych",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Removido de Continuar Assistindo", "removedFromContinueWatching": "Removido de Continuar Assistindo",
"errorLoading": "Erro: ${error}", "errorLoading": "Erro: ${error}",
"streamInterrupted": "A transmissão foi interrompida. Toque em reproduzir ou avance para tentar novamente.", "streamInterrupted": "A transmissão foi interrompida. Toque em reproduzir ou avance para tentar novamente.",
"liveStreamInterrupted": "A transmissão ao vivo foi interrompida. Toque em reproduzir para tentar novamente.",
"fileInfoNotAvailable": "Informações do arquivo não disponíveis", "fileInfoNotAvailable": "Informações do arquivo não disponíveis",
"errorLoadingFileInfo": "Erro ao carregar info do arquivo: ${error}", "errorLoadingFileInfo": "Erro ao carregar info do arquivo: ${error}",
"errorLoadingSeries": "Erro ao carregar série", "errorLoadingSeries": "Erro ao carregar série",
@@ -916,6 +917,7 @@
"watchChannel": "Assistir Canal", "watchChannel": "Assistir Canal",
"favorites": "Favoritos", "favorites": "Favoritos",
"reorderFavorites": "Reordenar favoritos", "reorderFavorites": "Reordenar favoritos",
"favoritesLoadFailed": "Não foi possível carregar os favoritos. Verifique sua conexão e tente novamente.",
"joinSession": "Entrar na sessão em andamento", "joinSession": "Entrar na sessão em andamento",
"watchFromStart": "Assistir do início (${minutes} min atrás)", "watchFromStart": "Assistir do início (${minutes} min atrás)",
"watchLive": "Assistir ao vivo", "watchLive": "Assistir ao vivo",
@@ -1140,6 +1142,7 @@
"customAmount": "Quantidade personalizada...", "customAmount": "Quantidade personalizada...",
"includeSpecials": "Incluir especiais", "includeSpecials": "Incluir especiais",
"howManyEpisodes": "Quantos episódios?", "howManyEpisodes": "Quantos episódios?",
"invalidEpisodeCount": "Insira uma quantidade válida de episódios.",
"keepSynced": "Manter sincronizado", "keepSynced": "Manter sincronizado",
"downloadOnce": "Baixar uma vez", "downloadOnce": "Baixar uma vez",
"keepNUnwatched": "Manter ${count} não assistidos", "keepNUnwatched": "Manter ${count} não assistidos",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Удалено из «Продолжить просмотр»", "removedFromContinueWatching": "Удалено из «Продолжить просмотр»",
"errorLoading": "Ошибка: ${error}", "errorLoading": "Ошибка: ${error}",
"streamInterrupted": "Поток прервался. Нажмите «Воспроизвести» или перемотайте, чтобы повторить попытку.", "streamInterrupted": "Поток прервался. Нажмите «Воспроизвести» или перемотайте, чтобы повторить попытку.",
"liveStreamInterrupted": "Прямая трансляция прервалась. Нажмите «Воспроизвести», чтобы повторить попытку.",
"fileInfoNotAvailable": "Информация о файле недоступна", "fileInfoNotAvailable": "Информация о файле недоступна",
"errorLoadingFileInfo": "Ошибка загрузки информации о файле: ${error}", "errorLoadingFileInfo": "Ошибка загрузки информации о файле: ${error}",
"errorLoadingSeries": "Ошибка загрузки сериала", "errorLoadingSeries": "Ошибка загрузки сериала",
@@ -918,6 +919,7 @@
"watchChannel": "Смотреть канал", "watchChannel": "Смотреть канал",
"favorites": "Избранное", "favorites": "Избранное",
"reorderFavorites": "Изменить порядок избранного", "reorderFavorites": "Изменить порядок избранного",
"favoritesLoadFailed": "Не удалось загрузить избранное. Проверьте подключение и повторите попытку.",
"joinSession": "Присоединиться к текущему сеансу", "joinSession": "Присоединиться к текущему сеансу",
"watchFromStart": "Смотреть сначала (${minutes} мин. назад)", "watchFromStart": "Смотреть сначала (${minutes} мин. назад)",
"watchLive": "Смотреть в прямом эфире", "watchLive": "Смотреть в прямом эфире",
@@ -1144,6 +1146,7 @@
"customAmount": "Указать количество...", "customAmount": "Указать количество...",
"includeSpecials": "Включить спецвыпуски", "includeSpecials": "Включить спецвыпуски",
"howManyEpisodes": "Сколько эпизодов?", "howManyEpisodes": "Сколько эпизодов?",
"invalidEpisodeCount": "Введите допустимое количество эпизодов.",
"keepSynced": "Синхронизировать", "keepSynced": "Синхронизировать",
"downloadOnce": "Скачать один раз", "downloadOnce": "Скачать один раз",
"keepNUnwatched": "Хранить ${count} непросмотренных", "keepNUnwatched": "Хранить ${count} непросмотренных",
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 16 /// Locales: 16
/// Strings: 22466 (1404 per locale) /// Strings: 22514 (1407 per locale)
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesBg extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Премахнато от продължаване на гледането'; @override String get removedFromContinueWatching => 'Премахнато от продължаване на гледането';
@override String errorLoading({required Object error}) => 'Грешка: ${error}'; @override String errorLoading({required Object error}) => 'Грешка: ${error}';
@override String get streamInterrupted => 'Потокът прекъсна. Натиснете „Пусни“ или превъртете, за да опитате отново.'; @override String get streamInterrupted => 'Потокът прекъсна. Натиснете „Пусни“ или превъртете, за да опитате отново.';
@override String get liveStreamInterrupted => 'Потокът на живо прекъсна. Натиснете „Пусни“, за да опитате отново.';
@override String get fileInfoNotAvailable => 'Информацията за файла не е налична'; @override String get fileInfoNotAvailable => 'Информацията за файла не е налична';
@override String errorLoadingFileInfo({required Object error}) => 'Грешка при зареждане на информация за файла: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Грешка при зареждане на информация за файла: ${error}';
@override String get errorLoadingSeries => 'Грешка при зареждане на сериала'; @override String get errorLoadingSeries => 'Грешка при зареждане на сериала';
@@ -1124,6 +1125,7 @@ class _TranslationsLiveTvBg extends TranslationsLiveTvEn {
@override String get watchChannel => 'Гледай канал'; @override String get watchChannel => 'Гледай канал';
@override String get favorites => 'Любими'; @override String get favorites => 'Любими';
@override String get reorderFavorites => 'Пренареди любимите'; @override String get reorderFavorites => 'Пренареди любимите';
@override String get favoritesLoadFailed => 'Любимите не можаха да се заредят. Проверете връзката си и опитайте отново.';
@override String get joinSession => 'Присъедини се към текуща сесия'; @override String get joinSession => 'Присъедини се към текуща сесия';
@override String watchFromStart({required Object minutes}) => 'Гледай от началото (преди ${minutes} мин)'; @override String watchFromStart({required Object minutes}) => 'Гледай от началото (преди ${minutes} мин)';
@override String get watchLive => 'Гледай на живо'; @override String get watchLive => 'Гледай на живо';
@@ -1383,6 +1385,7 @@ class _TranslationsDownloadsBg extends TranslationsDownloadsEn {
@override String get customAmount => 'Персонален брой...'; @override String get customAmount => 'Персонален брой...';
@override String get includeSpecials => 'Включи специалните'; @override String get includeSpecials => 'Включи специалните';
@override String get howManyEpisodes => 'Колко епизода?'; @override String get howManyEpisodes => 'Колко епизода?';
@override String get invalidEpisodeCount => 'Въведете валиден брой епизоди.';
@override String get keepSynced => 'Поддържай синхронизирано'; @override String get keepSynced => 'Поддържай синхронизирано';
@override String get downloadOnce => 'Изтегли еднократно'; @override String get downloadOnce => 'Изтегли еднократно';
@override String keepNUnwatched({required Object count}) => 'Пази ${count} негледани'; @override String keepNUnwatched({required Object count}) => 'Пази ${count} негледани';
@@ -2601,6 +2604,7 @@ extension on TranslationsBg {
'messages.removedFromContinueWatching' => 'Премахнато от продължаване на гледането', 'messages.removedFromContinueWatching' => 'Премахнато от продължаване на гледането',
'messages.errorLoading' => ({required Object error}) => 'Грешка: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Грешка: ${error}',
'messages.streamInterrupted' => 'Потокът прекъсна. Натиснете „Пусни“ или превъртете, за да опитате отново.', 'messages.streamInterrupted' => 'Потокът прекъсна. Натиснете „Пусни“ или превъртете, за да опитате отново.',
'messages.liveStreamInterrupted' => 'Потокът на живо прекъсна. Натиснете „Пусни“, за да опитате отново.',
'messages.fileInfoNotAvailable' => 'Информацията за файла не е налична', 'messages.fileInfoNotAvailable' => 'Информацията за файла не е налична',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Грешка при зареждане на информация за файла: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Грешка при зареждане на информация за файла: ${error}',
'messages.errorLoadingSeries' => 'Грешка при зареждане на сериала', 'messages.errorLoadingSeries' => 'Грешка при зареждане на сериала',
@@ -2608,9 +2612,9 @@ extension on TranslationsBg {
'messages.noDescriptionAvailable' => 'Няма налично описание', 'messages.noDescriptionAvailable' => 'Няма налично описание',
'messages.noProfilesAvailable' => 'Няма налични профили', 'messages.noProfilesAvailable' => 'Няма налични профили',
'messages.contactAdminForProfiles' => 'Свържете се с администратора на сървъра, за да добави профили', 'messages.contactAdminForProfiles' => 'Свържете се с администратора на сървъра, за да добави профили',
'messages.unableToDetermineLibrarySection' => 'Не може да се определи секцията на библиотеката за този елемент',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Не може да се определи секцията на библиотеката за този елемент',
'messages.logsCleared' => 'Логовете са изчистени', 'messages.logsCleared' => 'Логовете са изчистени',
'messages.logsCopied' => 'Логовете са копирани в клипборда', 'messages.logsCopied' => 'Логовете са копирани в клипборда',
'messages.noLogsAvailable' => 'Няма налични логове', 'messages.noLogsAvailable' => 'Няма налични логове',
@@ -2935,6 +2939,7 @@ extension on TranslationsBg {
'liveTv.watchChannel' => 'Гледай канал', 'liveTv.watchChannel' => 'Гледай канал',
'liveTv.favorites' => 'Любими', 'liveTv.favorites' => 'Любими',
'liveTv.reorderFavorites' => 'Пренареди любимите', 'liveTv.reorderFavorites' => 'Пренареди любимите',
'liveTv.favoritesLoadFailed' => 'Любимите не можаха да се заредят. Проверете връзката си и опитайте отново.',
'liveTv.joinSession' => 'Присъедини се към текуща сесия', 'liveTv.joinSession' => 'Присъедини се към текуща сесия',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Гледай от началото (преди ${minutes} мин)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Гледай от началото (преди ${minutes} мин)',
'liveTv.watchLive' => 'Гледай на живо', 'liveTv.watchLive' => 'Гледай на живо',
@@ -3121,10 +3126,10 @@ extension on TranslationsBg {
'downloads.episodesQueued' => ({required Object count}) => '${count} епизода са добавени в опашката за изтегляне', 'downloads.episodesQueued' => ({required Object count}) => '${count} епизода са добавени в опашката за изтегляне',
'downloads.downloadDeleted' => 'Изтеглянето е изтрито', 'downloads.downloadDeleted' => 'Изтеглянето е изтрито',
'downloads.deleteConfirm' => ({required Object title}) => 'Да се изтрие ли "${title}" от това устройство?', 'downloads.deleteConfirm' => ({required Object title}) => 'Да се изтрие ли "${title}" от това устройство?',
'downloads.cancelledDownloadTitle' => 'Отменено изтегляне',
'downloads.cancelledDownloadMessage' => 'Това изтегляне беше отменено. Какво искате да направите?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => 'Отменено изтегляне',
'downloads.cancelledDownloadMessage' => 'Това изтегляне беше отменено. Какво искате да направите?',
'downloads.allEpisodesAlreadyDownloaded' => 'Всички епизоди вече са изтеглени', 'downloads.allEpisodesAlreadyDownloaded' => 'Всички епизоди вече са изтеглени',
'downloads.resumeDownload' => 'Възобнови изтеглянето', 'downloads.resumeDownload' => 'Възобнови изтеглянето',
'downloads.cancelledDownload' => 'Отменено изтегляне', 'downloads.cancelledDownload' => 'Отменено изтегляне',
@@ -3148,6 +3153,7 @@ extension on TranslationsBg {
'downloads.customAmount' => 'Персонален брой...', 'downloads.customAmount' => 'Персонален брой...',
'downloads.includeSpecials' => 'Включи специалните', 'downloads.includeSpecials' => 'Включи специалните',
'downloads.howManyEpisodes' => 'Колко епизода?', 'downloads.howManyEpisodes' => 'Колко епизода?',
'downloads.invalidEpisodeCount' => 'Въведете валиден брой епизоди.',
'downloads.keepSynced' => 'Поддържай синхронизирано', 'downloads.keepSynced' => 'Поддържай синхронизирано',
'downloads.downloadOnce' => 'Изтегли еднократно', 'downloads.downloadOnce' => 'Изтегли еднократно',
'downloads.keepNUnwatched' => ({required Object count}) => 'Пази ${count} негледани', 'downloads.keepNUnwatched' => ({required Object count}) => 'Пази ${count} негледани',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesDa extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Fjernet fra Fortsæt med at se'; @override String get removedFromContinueWatching => 'Fjernet fra Fortsæt med at se';
@override String errorLoading({required Object error}) => 'Fejl: ${error}'; @override String errorLoading({required Object error}) => 'Fejl: ${error}';
@override String get streamInterrupted => 'Streamen blev afbrudt. Tryk på afspil, eller spol for at prøve igen.'; @override String get streamInterrupted => 'Streamen blev afbrudt. Tryk på afspil, eller spol for at prøve igen.';
@override String get liveStreamInterrupted => 'Livestreamen blev afbrudt. Tryk på afspil for at prøve igen.';
@override String get fileInfoNotAvailable => 'Filinfo ikke tilgængelig'; @override String get fileInfoNotAvailable => 'Filinfo ikke tilgængelig';
@override String errorLoadingFileInfo({required Object error}) => 'Fejl ved indlæsning af filinfo: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Fejl ved indlæsning af filinfo: ${error}';
@override String get errorLoadingSeries => 'Fejl ved indlæsning af serie'; @override String get errorLoadingSeries => 'Fejl ved indlæsning af serie';
@@ -1124,6 +1125,7 @@ class _TranslationsLiveTvDa extends TranslationsLiveTvEn {
@override String get watchChannel => 'Se kanal'; @override String get watchChannel => 'Se kanal';
@override String get favorites => 'Favoritter'; @override String get favorites => 'Favoritter';
@override String get reorderFavorites => 'Omarranger favoritter'; @override String get reorderFavorites => 'Omarranger favoritter';
@override String get favoritesLoadFailed => 'Favoritter kunne ikke indlæses. Kontrollér forbindelsen, og prøv igen.';
@override String get joinSession => 'Deltag i igangværende session'; @override String get joinSession => 'Deltag i igangværende session';
@override String watchFromStart({required Object minutes}) => 'Se fra start (${minutes} min siden)'; @override String watchFromStart({required Object minutes}) => 'Se fra start (${minutes} min siden)';
@override String get watchLive => 'Se live'; @override String get watchLive => 'Se live';
@@ -1383,6 +1385,7 @@ class _TranslationsDownloadsDa extends TranslationsDownloadsEn {
@override String get customAmount => 'Angiv antal...'; @override String get customAmount => 'Angiv antal...';
@override String get includeSpecials => 'Inkludér specials'; @override String get includeSpecials => 'Inkludér specials';
@override String get howManyEpisodes => 'Hvor mange episoder?'; @override String get howManyEpisodes => 'Hvor mange episoder?';
@override String get invalidEpisodeCount => 'Indtast et gyldigt antal episoder.';
@override String get keepSynced => 'Hold synkroniseret'; @override String get keepSynced => 'Hold synkroniseret';
@override String get downloadOnce => 'Download én gang'; @override String get downloadOnce => 'Download én gang';
@override String keepNUnwatched({required Object count}) => 'Behold ${count} usete'; @override String keepNUnwatched({required Object count}) => 'Behold ${count} usete';
@@ -2601,6 +2604,7 @@ extension on TranslationsDa {
'messages.removedFromContinueWatching' => 'Fjernet fra Fortsæt med at se', 'messages.removedFromContinueWatching' => 'Fjernet fra Fortsæt med at se',
'messages.errorLoading' => ({required Object error}) => 'Fejl: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Fejl: ${error}',
'messages.streamInterrupted' => 'Streamen blev afbrudt. Tryk på afspil, eller spol for at prøve igen.', 'messages.streamInterrupted' => 'Streamen blev afbrudt. Tryk på afspil, eller spol for at prøve igen.',
'messages.liveStreamInterrupted' => 'Livestreamen blev afbrudt. Tryk på afspil for at prøve igen.',
'messages.fileInfoNotAvailable' => 'Filinfo ikke tilgængelig', 'messages.fileInfoNotAvailable' => 'Filinfo ikke tilgængelig',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fejl ved indlæsning af filinfo: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fejl ved indlæsning af filinfo: ${error}',
'messages.errorLoadingSeries' => 'Fejl ved indlæsning af serie', 'messages.errorLoadingSeries' => 'Fejl ved indlæsning af serie',
@@ -2608,9 +2612,9 @@ extension on TranslationsDa {
'messages.noDescriptionAvailable' => 'Ingen beskrivelse tilgængelig', 'messages.noDescriptionAvailable' => 'Ingen beskrivelse tilgængelig',
'messages.noProfilesAvailable' => 'Ingen profiler tilgængelige', 'messages.noProfilesAvailable' => 'Ingen profiler tilgængelige',
'messages.contactAdminForProfiles' => 'Kontakt din serveradministrator for at tilføje profiler', 'messages.contactAdminForProfiles' => 'Kontakt din serveradministrator for at tilføje profiler',
'messages.unableToDetermineLibrarySection' => 'Kan ikke bestemme biblioteksafdeling for dette element',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Kan ikke bestemme biblioteksafdeling for dette element',
'messages.logsCleared' => 'Logs ryddet', 'messages.logsCleared' => 'Logs ryddet',
'messages.logsCopied' => 'Logs kopieret til udklipsholder', 'messages.logsCopied' => 'Logs kopieret til udklipsholder',
'messages.noLogsAvailable' => 'Ingen logs tilgængelige', 'messages.noLogsAvailable' => 'Ingen logs tilgængelige',
@@ -2935,6 +2939,7 @@ extension on TranslationsDa {
'liveTv.watchChannel' => 'Se kanal', 'liveTv.watchChannel' => 'Se kanal',
'liveTv.favorites' => 'Favoritter', 'liveTv.favorites' => 'Favoritter',
'liveTv.reorderFavorites' => 'Omarranger favoritter', 'liveTv.reorderFavorites' => 'Omarranger favoritter',
'liveTv.favoritesLoadFailed' => 'Favoritter kunne ikke indlæses. Kontrollér forbindelsen, og prøv igen.',
'liveTv.joinSession' => 'Deltag i igangværende session', 'liveTv.joinSession' => 'Deltag i igangværende session',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Se fra start (${minutes} min siden)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Se fra start (${minutes} min siden)',
'liveTv.watchLive' => 'Se live', 'liveTv.watchLive' => 'Se live',
@@ -3121,10 +3126,10 @@ extension on TranslationsDa {
'downloads.episodesQueued' => ({required Object count}) => '${count} episoder i downloadkø', 'downloads.episodesQueued' => ({required Object count}) => '${count} episoder i downloadkø',
'downloads.downloadDeleted' => 'Download slettet', 'downloads.downloadDeleted' => 'Download slettet',
'downloads.deleteConfirm' => ({required Object title}) => 'Slet "${title}" fra denne enhed?', 'downloads.deleteConfirm' => ({required Object title}) => 'Slet "${title}" fra denne enhed?',
'downloads.cancelledDownloadTitle' => 'Annulleret download',
'downloads.cancelledDownloadMessage' => 'Denne download blev annulleret. Hvad vil du gøre?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => 'Annulleret download',
'downloads.cancelledDownloadMessage' => 'Denne download blev annulleret. Hvad vil du gøre?',
'downloads.allEpisodesAlreadyDownloaded' => 'Alle episoder er allerede downloadet', 'downloads.allEpisodesAlreadyDownloaded' => 'Alle episoder er allerede downloadet',
'downloads.resumeDownload' => 'Genoptag download', 'downloads.resumeDownload' => 'Genoptag download',
'downloads.cancelledDownload' => 'Annulleret download', 'downloads.cancelledDownload' => 'Annulleret download',
@@ -3148,6 +3153,7 @@ extension on TranslationsDa {
'downloads.customAmount' => 'Angiv antal...', 'downloads.customAmount' => 'Angiv antal...',
'downloads.includeSpecials' => 'Inkludér specials', 'downloads.includeSpecials' => 'Inkludér specials',
'downloads.howManyEpisodes' => 'Hvor mange episoder?', 'downloads.howManyEpisodes' => 'Hvor mange episoder?',
'downloads.invalidEpisodeCount' => 'Indtast et gyldigt antal episoder.',
'downloads.keepSynced' => 'Hold synkroniseret', 'downloads.keepSynced' => 'Hold synkroniseret',
'downloads.downloadOnce' => 'Download én gang', 'downloads.downloadOnce' => 'Download én gang',
'downloads.keepNUnwatched' => ({required Object count}) => 'Behold ${count} usete', 'downloads.keepNUnwatched' => ({required Object count}) => 'Behold ${count} usete',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesDe extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Aus Weiterschauen\' entfernt'; @override String get removedFromContinueWatching => 'Aus Weiterschauen\' entfernt';
@override String errorLoading({required Object error}) => 'Fehler: ${error}'; @override String errorLoading({required Object error}) => 'Fehler: ${error}';
@override String get streamInterrupted => 'Der Stream wurde unterbrochen. Drücke auf Wiedergabe oder spule, um es erneut zu versuchen.'; @override String get streamInterrupted => 'Der Stream wurde unterbrochen. Drücke auf Wiedergabe oder spule, um es erneut zu versuchen.';
@override String get liveStreamInterrupted => 'Der Livestream wurde unterbrochen. Drücke auf Wiedergabe, um es erneut zu versuchen.';
@override String get fileInfoNotAvailable => 'Dateiinfo nicht verfügbar'; @override String get fileInfoNotAvailable => 'Dateiinfo nicht verfügbar';
@override String errorLoadingFileInfo({required Object error}) => 'Fehler beim Laden der Dateiinfo: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Fehler beim Laden der Dateiinfo: ${error}';
@override String get errorLoadingSeries => 'Fehler beim Laden der Serie'; @override String get errorLoadingSeries => 'Fehler beim Laden der Serie';
@@ -1124,6 +1125,7 @@ class _TranslationsLiveTvDe extends TranslationsLiveTvEn {
@override String get watchChannel => 'Kanal ansehen'; @override String get watchChannel => 'Kanal ansehen';
@override String get favorites => 'Favoriten'; @override String get favorites => 'Favoriten';
@override String get reorderFavorites => 'Favoriten sortieren'; @override String get reorderFavorites => 'Favoriten sortieren';
@override String get favoritesLoadFailed => 'Favoriten konnten nicht geladen werden. Überprüfe deine Verbindung und versuche es erneut.';
@override String get joinSession => 'Laufender Sitzung beitreten'; @override String get joinSession => 'Laufender Sitzung beitreten';
@override String watchFromStart({required Object minutes}) => 'Von Anfang an ansehen (vor ${minutes} Min.)'; @override String watchFromStart({required Object minutes}) => 'Von Anfang an ansehen (vor ${minutes} Min.)';
@override String get watchLive => 'Live ansehen'; @override String get watchLive => 'Live ansehen';
@@ -1383,6 +1385,7 @@ class _TranslationsDownloadsDe extends TranslationsDownloadsEn {
@override String get customAmount => 'Eigene Anzahl...'; @override String get customAmount => 'Eigene Anzahl...';
@override String get includeSpecials => 'Specials einschließen'; @override String get includeSpecials => 'Specials einschließen';
@override String get howManyEpisodes => 'Wie viele Episoden?'; @override String get howManyEpisodes => 'Wie viele Episoden?';
@override String get invalidEpisodeCount => 'Gib eine gültige Episodenanzahl ein.';
@override String get keepSynced => 'Synchronisiert halten'; @override String get keepSynced => 'Synchronisiert halten';
@override String get downloadOnce => 'Einmal herunterladen'; @override String get downloadOnce => 'Einmal herunterladen';
@override String keepNUnwatched({required Object count}) => '${count} ungesehene behalten'; @override String keepNUnwatched({required Object count}) => '${count} ungesehene behalten';
@@ -2601,6 +2604,7 @@ extension on TranslationsDe {
'messages.removedFromContinueWatching' => 'Aus Weiterschauen\' entfernt', 'messages.removedFromContinueWatching' => 'Aus Weiterschauen\' entfernt',
'messages.errorLoading' => ({required Object error}) => 'Fehler: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Fehler: ${error}',
'messages.streamInterrupted' => 'Der Stream wurde unterbrochen. Drücke auf Wiedergabe oder spule, um es erneut zu versuchen.', 'messages.streamInterrupted' => 'Der Stream wurde unterbrochen. Drücke auf Wiedergabe oder spule, um es erneut zu versuchen.',
'messages.liveStreamInterrupted' => 'Der Livestream wurde unterbrochen. Drücke auf Wiedergabe, um es erneut zu versuchen.',
'messages.fileInfoNotAvailable' => 'Dateiinfo nicht verfügbar', 'messages.fileInfoNotAvailable' => 'Dateiinfo nicht verfügbar',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fehler beim Laden der Dateiinfo: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fehler beim Laden der Dateiinfo: ${error}',
'messages.errorLoadingSeries' => 'Fehler beim Laden der Serie', 'messages.errorLoadingSeries' => 'Fehler beim Laden der Serie',
@@ -2608,9 +2612,9 @@ extension on TranslationsDe {
'messages.noDescriptionAvailable' => 'Keine Beschreibung verfügbar', 'messages.noDescriptionAvailable' => 'Keine Beschreibung verfügbar',
'messages.noProfilesAvailable' => 'Keine Profile verfügbar', 'messages.noProfilesAvailable' => 'Keine Profile verfügbar',
'messages.contactAdminForProfiles' => 'Kontaktiere deinen Serveradministrator, um Profile hinzuzufügen', 'messages.contactAdminForProfiles' => 'Kontaktiere deinen Serveradministrator, um Profile hinzuzufügen',
'messages.unableToDetermineLibrarySection' => 'Bibliotheksbereich für dieses Element kann nicht ermittelt werden',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Bibliotheksbereich für dieses Element kann nicht ermittelt werden',
'messages.logsCleared' => 'Protokolle gelöscht', 'messages.logsCleared' => 'Protokolle gelöscht',
'messages.logsCopied' => 'Protokolle in Zwischenablage kopiert', 'messages.logsCopied' => 'Protokolle in Zwischenablage kopiert',
'messages.noLogsAvailable' => 'Keine Protokolle verfügbar', 'messages.noLogsAvailable' => 'Keine Protokolle verfügbar',
@@ -2935,6 +2939,7 @@ extension on TranslationsDe {
'liveTv.watchChannel' => 'Kanal ansehen', 'liveTv.watchChannel' => 'Kanal ansehen',
'liveTv.favorites' => 'Favoriten', 'liveTv.favorites' => 'Favoriten',
'liveTv.reorderFavorites' => 'Favoriten sortieren', 'liveTv.reorderFavorites' => 'Favoriten sortieren',
'liveTv.favoritesLoadFailed' => 'Favoriten konnten nicht geladen werden. Überprüfe deine Verbindung und versuche es erneut.',
'liveTv.joinSession' => 'Laufender Sitzung beitreten', 'liveTv.joinSession' => 'Laufender Sitzung beitreten',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Von Anfang an ansehen (vor ${minutes} Min.)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Von Anfang an ansehen (vor ${minutes} Min.)',
'liveTv.watchLive' => 'Live ansehen', 'liveTv.watchLive' => 'Live ansehen',
@@ -3121,10 +3126,10 @@ extension on TranslationsDe {
'downloads.episodesQueued' => ({required Object count}) => '${count} Episoden zum Download hinzugefügt', 'downloads.episodesQueued' => ({required Object count}) => '${count} Episoden zum Download hinzugefügt',
'downloads.downloadDeleted' => 'Download gelöscht', 'downloads.downloadDeleted' => 'Download gelöscht',
'downloads.deleteConfirm' => ({required Object title}) => '"${title}" von diesem Gerät löschen?', 'downloads.deleteConfirm' => ({required Object title}) => '"${title}" von diesem Gerät löschen?',
'downloads.cancelledDownloadTitle' => 'Abgebrochener Download',
'downloads.cancelledDownloadMessage' => 'Dieser Download wurde abgebrochen. Was möchtest du tun?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => 'Abgebrochener Download',
'downloads.cancelledDownloadMessage' => 'Dieser Download wurde abgebrochen. Was möchtest du tun?',
'downloads.allEpisodesAlreadyDownloaded' => 'Alle Episoden sind bereits heruntergeladen', 'downloads.allEpisodesAlreadyDownloaded' => 'Alle Episoden sind bereits heruntergeladen',
'downloads.resumeDownload' => 'Download fortsetzen', 'downloads.resumeDownload' => 'Download fortsetzen',
'downloads.cancelledDownload' => 'Abgebrochener Download', 'downloads.cancelledDownload' => 'Abgebrochener Download',
@@ -3148,6 +3153,7 @@ extension on TranslationsDe {
'downloads.customAmount' => 'Eigene Anzahl...', 'downloads.customAmount' => 'Eigene Anzahl...',
'downloads.includeSpecials' => 'Specials einschließen', 'downloads.includeSpecials' => 'Specials einschließen',
'downloads.howManyEpisodes' => 'Wie viele Episoden?', 'downloads.howManyEpisodes' => 'Wie viele Episoden?',
'downloads.invalidEpisodeCount' => 'Gib eine gültige Episodenanzahl ein.',
'downloads.keepSynced' => 'Synchronisiert halten', 'downloads.keepSynced' => 'Synchronisiert halten',
'downloads.downloadOnce' => 'Einmal herunterladen', 'downloads.downloadOnce' => 'Einmal herunterladen',
'downloads.keepNUnwatched' => ({required Object count}) => '${count} ungesehene behalten', 'downloads.keepNUnwatched' => ({required Object count}) => '${count} ungesehene behalten',
+15 -3
View File
@@ -1644,6 +1644,9 @@ class TranslationsMessagesEn {
/// en: 'The stream was interrupted. Press play or seek to retry.' /// en: 'The stream was interrupted. Press play or seek to retry.'
String get streamInterrupted => 'The stream was interrupted. Press play or seek to retry.'; String get streamInterrupted => 'The stream was interrupted. Press play or seek to retry.';
/// en: 'The live stream was interrupted. Press play to retry.'
String get liveStreamInterrupted => 'The live stream was interrupted. Press play to retry.';
/// en: 'File information not available' /// en: 'File information not available'
String get fileInfoNotAvailable => 'File information not available'; String get fileInfoNotAvailable => 'File information not available';
@@ -2624,6 +2627,9 @@ class TranslationsLiveTvEn {
/// en: 'Reorder Favorites' /// en: 'Reorder Favorites'
String get reorderFavorites => 'Reorder Favorites'; String get reorderFavorites => 'Reorder Favorites';
/// en: 'Could not load favorites. Check your connection and try again.'
String get favoritesLoadFailed => 'Could not load favorites. Check your connection and try again.';
/// en: 'Join Session in Progress' /// en: 'Join Session in Progress'
String get joinSession => 'Join Session in Progress'; String get joinSession => 'Join Session in Progress';
@@ -3305,6 +3311,9 @@ class TranslationsDownloadsEn {
/// en: 'How many episodes?' /// en: 'How many episodes?'
String get howManyEpisodes => 'How many episodes?'; String get howManyEpisodes => 'How many episodes?';
/// en: 'Enter a valid episode count.'
String get invalidEpisodeCount => 'Enter a valid episode count.';
/// en: 'Keep synced' /// en: 'Keep synced'
String get keepSynced => 'Keep synced'; String get keepSynced => 'Keep synced';
@@ -5412,6 +5421,7 @@ extension on Translations {
'messages.removedFromContinueWatching' => 'Removed from Continue Watching', 'messages.removedFromContinueWatching' => 'Removed from Continue Watching',
'messages.errorLoading' => ({required Object error}) => 'Error: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Error: ${error}',
'messages.streamInterrupted' => 'The stream was interrupted. Press play or seek to retry.', 'messages.streamInterrupted' => 'The stream was interrupted. Press play or seek to retry.',
'messages.liveStreamInterrupted' => 'The live stream was interrupted. Press play to retry.',
'messages.fileInfoNotAvailable' => 'File information not available', 'messages.fileInfoNotAvailable' => 'File information not available',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Error loading file info: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Error loading file info: ${error}',
'messages.errorLoadingSeries' => 'Error loading series', 'messages.errorLoadingSeries' => 'Error loading series',
@@ -5419,9 +5429,9 @@ extension on Translations {
'messages.noDescriptionAvailable' => 'No description available', 'messages.noDescriptionAvailable' => 'No description available',
'messages.noProfilesAvailable' => 'No profiles available', 'messages.noProfilesAvailable' => 'No profiles available',
'messages.contactAdminForProfiles' => 'Contact your server administrator to add profiles', 'messages.contactAdminForProfiles' => 'Contact your server administrator to add profiles',
'messages.unableToDetermineLibrarySection' => 'Unable to determine library section for this item',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Unable to determine library section for this item',
'messages.logsCleared' => 'Logs cleared', 'messages.logsCleared' => 'Logs cleared',
'messages.logsCopied' => 'Logs copied to clipboard', 'messages.logsCopied' => 'Logs copied to clipboard',
'messages.noLogsAvailable' => 'No logs available', 'messages.noLogsAvailable' => 'No logs available',
@@ -5746,6 +5756,7 @@ extension on Translations {
'liveTv.watchChannel' => 'Watch Channel', 'liveTv.watchChannel' => 'Watch Channel',
'liveTv.favorites' => 'Favorites', 'liveTv.favorites' => 'Favorites',
'liveTv.reorderFavorites' => 'Reorder Favorites', 'liveTv.reorderFavorites' => 'Reorder Favorites',
'liveTv.favoritesLoadFailed' => 'Could not load favorites. Check your connection and try again.',
'liveTv.joinSession' => 'Join Session in Progress', 'liveTv.joinSession' => 'Join Session in Progress',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Watch from start (${minutes} min ago)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Watch from start (${minutes} min ago)',
'liveTv.watchLive' => 'Watch Live', 'liveTv.watchLive' => 'Watch Live',
@@ -5932,10 +5943,10 @@ extension on Translations {
'downloads.episodesQueued' => ({required Object count}) => '${count} episodes queued for download', 'downloads.episodesQueued' => ({required Object count}) => '${count} episodes queued for download',
'downloads.downloadDeleted' => 'Download deleted', 'downloads.downloadDeleted' => 'Download deleted',
'downloads.deleteConfirm' => ({required Object title}) => 'Delete "${title}" from this device?', 'downloads.deleteConfirm' => ({required Object title}) => 'Delete "${title}" from this device?',
'downloads.cancelledDownloadTitle' => 'Cancelled Download',
'downloads.cancelledDownloadMessage' => 'This download was cancelled. What would you like to do?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => 'Cancelled Download',
'downloads.cancelledDownloadMessage' => 'This download was cancelled. What would you like to do?',
'downloads.allEpisodesAlreadyDownloaded' => 'All episodes already downloaded', 'downloads.allEpisodesAlreadyDownloaded' => 'All episodes already downloaded',
'downloads.resumeDownload' => 'Resume download', 'downloads.resumeDownload' => 'Resume download',
'downloads.cancelledDownload' => 'Cancelled download', 'downloads.cancelledDownload' => 'Cancelled download',
@@ -5959,6 +5970,7 @@ extension on Translations {
'downloads.customAmount' => 'Custom amount...', 'downloads.customAmount' => 'Custom amount...',
'downloads.includeSpecials' => 'Include Specials', 'downloads.includeSpecials' => 'Include Specials',
'downloads.howManyEpisodes' => 'How many episodes?', 'downloads.howManyEpisodes' => 'How many episodes?',
'downloads.invalidEpisodeCount' => 'Enter a valid episode count.',
'downloads.keepSynced' => 'Keep synced', 'downloads.keepSynced' => 'Keep synced',
'downloads.downloadOnce' => 'Download once', 'downloads.downloadOnce' => 'Download once',
'downloads.keepNUnwatched' => ({required Object count}) => 'Keep ${count} unwatched', 'downloads.keepNUnwatched' => ({required Object count}) => 'Keep ${count} unwatched',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesEs extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Eliminado de Seguir Viendo'; @override String get removedFromContinueWatching => 'Eliminado de Seguir Viendo';
@override String errorLoading({required Object error}) => 'Error: ${error}'; @override String errorLoading({required Object error}) => 'Error: ${error}';
@override String get streamInterrupted => 'La reproducción se interrumpió. Pulsa reproducir o avanza para volver a intentarlo.'; @override String get streamInterrupted => 'La reproducción se interrumpió. Pulsa reproducir o avanza para volver a intentarlo.';
@override String get liveStreamInterrupted => 'La transmisión en vivo se interrumpió. Pulsa reproducir para volver a intentarlo.';
@override String get fileInfoNotAvailable => 'Información de archivo no disponible'; @override String get fileInfoNotAvailable => 'Información de archivo no disponible';
@override String errorLoadingFileInfo({required Object error}) => 'Error al cargar info de archivo: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Error al cargar info de archivo: ${error}';
@override String get errorLoadingSeries => 'Error al cargar la serie'; @override String get errorLoadingSeries => 'Error al cargar la serie';
@@ -1124,6 +1125,7 @@ class _TranslationsLiveTvEs extends TranslationsLiveTvEn {
@override String get watchChannel => 'Ver canal'; @override String get watchChannel => 'Ver canal';
@override String get favorites => 'Favoritos'; @override String get favorites => 'Favoritos';
@override String get reorderFavorites => 'Reordenar favoritos'; @override String get reorderFavorites => 'Reordenar favoritos';
@override String get favoritesLoadFailed => 'No se pudieron cargar los favoritos. Comprueba tu conexión e inténtalo de nuevo.';
@override String get joinSession => 'Unirse a sesión en curso'; @override String get joinSession => 'Unirse a sesión en curso';
@override String watchFromStart({required Object minutes}) => 'Ver desde el inicio (hace ${minutes} min)'; @override String watchFromStart({required Object minutes}) => 'Ver desde el inicio (hace ${minutes} min)';
@override String get watchLive => 'Ver en vivo'; @override String get watchLive => 'Ver en vivo';
@@ -1383,6 +1385,7 @@ class _TranslationsDownloadsEs extends TranslationsDownloadsEn {
@override String get customAmount => 'Cantidad personalizada...'; @override String get customAmount => 'Cantidad personalizada...';
@override String get includeSpecials => 'Incluir especiales'; @override String get includeSpecials => 'Incluir especiales';
@override String get howManyEpisodes => '¿Cuántos episodios?'; @override String get howManyEpisodes => '¿Cuántos episodios?';
@override String get invalidEpisodeCount => 'Introduce un número de episodios válido.';
@override String get keepSynced => 'Mantener sincronizado'; @override String get keepSynced => 'Mantener sincronizado';
@override String get downloadOnce => 'Descargar una vez'; @override String get downloadOnce => 'Descargar una vez';
@override String keepNUnwatched({required Object count}) => 'Mantener ${count} sin ver'; @override String keepNUnwatched({required Object count}) => 'Mantener ${count} sin ver';
@@ -2601,6 +2604,7 @@ extension on TranslationsEs {
'messages.removedFromContinueWatching' => 'Eliminado de Seguir Viendo', 'messages.removedFromContinueWatching' => 'Eliminado de Seguir Viendo',
'messages.errorLoading' => ({required Object error}) => 'Error: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Error: ${error}',
'messages.streamInterrupted' => 'La reproducción se interrumpió. Pulsa reproducir o avanza para volver a intentarlo.', 'messages.streamInterrupted' => 'La reproducción se interrumpió. Pulsa reproducir o avanza para volver a intentarlo.',
'messages.liveStreamInterrupted' => 'La transmisión en vivo se interrumpió. Pulsa reproducir para volver a intentarlo.',
'messages.fileInfoNotAvailable' => 'Información de archivo no disponible', 'messages.fileInfoNotAvailable' => 'Información de archivo no disponible',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Error al cargar info de archivo: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Error al cargar info de archivo: ${error}',
'messages.errorLoadingSeries' => 'Error al cargar la serie', 'messages.errorLoadingSeries' => 'Error al cargar la serie',
@@ -2608,9 +2612,9 @@ extension on TranslationsEs {
'messages.noDescriptionAvailable' => 'No hay descripción disponible', 'messages.noDescriptionAvailable' => 'No hay descripción disponible',
'messages.noProfilesAvailable' => 'No hay perfiles disponibles', 'messages.noProfilesAvailable' => 'No hay perfiles disponibles',
'messages.contactAdminForProfiles' => 'Contacta a tu administrador del servidor para añadir perfiles', 'messages.contactAdminForProfiles' => 'Contacta a tu administrador del servidor para añadir perfiles',
'messages.unableToDetermineLibrarySection' => 'No se puede determinar la sección de biblioteca para este elemento',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'No se puede determinar la sección de biblioteca para este elemento',
'messages.logsCleared' => 'Logs borrados', 'messages.logsCleared' => 'Logs borrados',
'messages.logsCopied' => 'Logs copiados al portapapeles', 'messages.logsCopied' => 'Logs copiados al portapapeles',
'messages.noLogsAvailable' => 'No hay logs disponibles', 'messages.noLogsAvailable' => 'No hay logs disponibles',
@@ -2935,6 +2939,7 @@ extension on TranslationsEs {
'liveTv.watchChannel' => 'Ver canal', 'liveTv.watchChannel' => 'Ver canal',
'liveTv.favorites' => 'Favoritos', 'liveTv.favorites' => 'Favoritos',
'liveTv.reorderFavorites' => 'Reordenar favoritos', 'liveTv.reorderFavorites' => 'Reordenar favoritos',
'liveTv.favoritesLoadFailed' => 'No se pudieron cargar los favoritos. Comprueba tu conexión e inténtalo de nuevo.',
'liveTv.joinSession' => 'Unirse a sesión en curso', 'liveTv.joinSession' => 'Unirse a sesión en curso',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Ver desde el inicio (hace ${minutes} min)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Ver desde el inicio (hace ${minutes} min)',
'liveTv.watchLive' => 'Ver en vivo', 'liveTv.watchLive' => 'Ver en vivo',
@@ -3121,10 +3126,10 @@ extension on TranslationsEs {
'downloads.episodesQueued' => ({required Object count}) => '${count} episodios en cola para descargar', 'downloads.episodesQueued' => ({required Object count}) => '${count} episodios en cola para descargar',
'downloads.downloadDeleted' => 'Descarga eliminada', 'downloads.downloadDeleted' => 'Descarga eliminada',
'downloads.deleteConfirm' => ({required Object title}) => '¿Eliminar "${title}" de este dispositivo?', 'downloads.deleteConfirm' => ({required Object title}) => '¿Eliminar "${title}" de este dispositivo?',
'downloads.cancelledDownloadTitle' => 'Descarga cancelada',
'downloads.cancelledDownloadMessage' => 'Esta descarga se canceló. ¿Qué quieres hacer?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => 'Descarga cancelada',
'downloads.cancelledDownloadMessage' => 'Esta descarga se canceló. ¿Qué quieres hacer?',
'downloads.allEpisodesAlreadyDownloaded' => 'Todos los episodios ya están descargados', 'downloads.allEpisodesAlreadyDownloaded' => 'Todos los episodios ya están descargados',
'downloads.resumeDownload' => 'Reanudar descarga', 'downloads.resumeDownload' => 'Reanudar descarga',
'downloads.cancelledDownload' => 'Descarga cancelada', 'downloads.cancelledDownload' => 'Descarga cancelada',
@@ -3148,6 +3153,7 @@ extension on TranslationsEs {
'downloads.customAmount' => 'Cantidad personalizada...', 'downloads.customAmount' => 'Cantidad personalizada...',
'downloads.includeSpecials' => 'Incluir especiales', 'downloads.includeSpecials' => 'Incluir especiales',
'downloads.howManyEpisodes' => '¿Cuántos episodios?', 'downloads.howManyEpisodes' => '¿Cuántos episodios?',
'downloads.invalidEpisodeCount' => 'Introduce un número de episodios válido.',
'downloads.keepSynced' => 'Mantener sincronizado', 'downloads.keepSynced' => 'Mantener sincronizado',
'downloads.downloadOnce' => 'Descargar una vez', 'downloads.downloadOnce' => 'Descargar una vez',
'downloads.keepNUnwatched' => ({required Object count}) => 'Mantener ${count} sin ver', 'downloads.keepNUnwatched' => ({required Object count}) => 'Mantener ${count} sin ver',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesFr extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Supprimer de "Continuer à regarder"'; @override String get removedFromContinueWatching => 'Supprimer de "Continuer à regarder"';
@override String errorLoading({required Object error}) => 'Erreur: ${error}'; @override String errorLoading({required Object error}) => 'Erreur: ${error}';
@override String get streamInterrupted => 'La lecture a été interrompue. Appuyez sur Lecture ou avancez pour réessayer.'; @override String get streamInterrupted => 'La lecture a été interrompue. Appuyez sur Lecture ou avancez pour réessayer.';
@override String get liveStreamInterrupted => 'Le direct a été interrompu. Appuyez sur Lecture pour réessayer.';
@override String get fileInfoNotAvailable => 'Informations sur le fichier non disponibles'; @override String get fileInfoNotAvailable => 'Informations sur le fichier non disponibles';
@override String errorLoadingFileInfo({required Object error}) => 'Erreur lors du chargement des informations sur le fichier: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Erreur lors du chargement des informations sur le fichier: ${error}';
@override String get errorLoadingSeries => 'Erreur lors du chargement de la série'; @override String get errorLoadingSeries => 'Erreur lors du chargement de la série';
@@ -1124,6 +1125,7 @@ class _TranslationsLiveTvFr extends TranslationsLiveTvEn {
@override String get watchChannel => 'Regarder la chaîne'; @override String get watchChannel => 'Regarder la chaîne';
@override String get favorites => 'Favoris'; @override String get favorites => 'Favoris';
@override String get reorderFavorites => 'Réorganiser les favoris'; @override String get reorderFavorites => 'Réorganiser les favoris';
@override String get favoritesLoadFailed => 'Impossible de charger les favoris. Vérifiez votre connexion et réessayez.';
@override String get joinSession => 'Rejoindre la session en cours'; @override String get joinSession => 'Rejoindre la session en cours';
@override String watchFromStart({required Object minutes}) => 'Regarder depuis le début (il y a ${minutes} min)'; @override String watchFromStart({required Object minutes}) => 'Regarder depuis le début (il y a ${minutes} min)';
@override String get watchLive => 'Regarder en direct'; @override String get watchLive => 'Regarder en direct';
@@ -1383,6 +1385,7 @@ class _TranslationsDownloadsFr extends TranslationsDownloadsEn {
@override String get customAmount => 'Quantité personnalisée...'; @override String get customAmount => 'Quantité personnalisée...';
@override String get includeSpecials => 'Inclure les spéciaux'; @override String get includeSpecials => 'Inclure les spéciaux';
@override String get howManyEpisodes => 'Combien d\'épisodes ?'; @override String get howManyEpisodes => 'Combien d\'épisodes ?';
@override String get invalidEpisodeCount => 'Saisissez un nombre d\'épisodes valide.';
@override String get keepSynced => 'Garder synchronisé'; @override String get keepSynced => 'Garder synchronisé';
@override String get downloadOnce => 'Télécharger une fois'; @override String get downloadOnce => 'Télécharger une fois';
@override String keepNUnwatched({required Object count}) => 'Garder ${count} non vus'; @override String keepNUnwatched({required Object count}) => 'Garder ${count} non vus';
@@ -2601,6 +2604,7 @@ extension on TranslationsFr {
'messages.removedFromContinueWatching' => 'Supprimer de "Continuer à regarder"', 'messages.removedFromContinueWatching' => 'Supprimer de "Continuer à regarder"',
'messages.errorLoading' => ({required Object error}) => 'Erreur: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Erreur: ${error}',
'messages.streamInterrupted' => 'La lecture a été interrompue. Appuyez sur Lecture ou avancez pour réessayer.', 'messages.streamInterrupted' => 'La lecture a été interrompue. Appuyez sur Lecture ou avancez pour réessayer.',
'messages.liveStreamInterrupted' => 'Le direct a été interrompu. Appuyez sur Lecture pour réessayer.',
'messages.fileInfoNotAvailable' => 'Informations sur le fichier non disponibles', 'messages.fileInfoNotAvailable' => 'Informations sur le fichier non disponibles',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Erreur lors du chargement des informations sur le fichier: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Erreur lors du chargement des informations sur le fichier: ${error}',
'messages.errorLoadingSeries' => 'Erreur lors du chargement de la série', 'messages.errorLoadingSeries' => 'Erreur lors du chargement de la série',
@@ -2608,9 +2612,9 @@ extension on TranslationsFr {
'messages.noDescriptionAvailable' => 'Aucune description disponible', 'messages.noDescriptionAvailable' => 'Aucune description disponible',
'messages.noProfilesAvailable' => 'Aucun profil disponible', 'messages.noProfilesAvailable' => 'Aucun profil disponible',
'messages.contactAdminForProfiles' => 'Contactez votre administrateur serveur pour ajouter des profils', 'messages.contactAdminForProfiles' => 'Contactez votre administrateur serveur pour ajouter des profils',
'messages.unableToDetermineLibrarySection' => 'Impossible de déterminer la section de la bibliothèque pour cet élément',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Impossible de déterminer la section de la bibliothèque pour cet élément',
'messages.logsCleared' => 'Logs effacés', 'messages.logsCleared' => 'Logs effacés',
'messages.logsCopied' => 'Logs copiés dans le presse-papier', 'messages.logsCopied' => 'Logs copiés dans le presse-papier',
'messages.noLogsAvailable' => 'Aucun log disponible', 'messages.noLogsAvailable' => 'Aucun log disponible',
@@ -2935,6 +2939,7 @@ extension on TranslationsFr {
'liveTv.watchChannel' => 'Regarder la chaîne', 'liveTv.watchChannel' => 'Regarder la chaîne',
'liveTv.favorites' => 'Favoris', 'liveTv.favorites' => 'Favoris',
'liveTv.reorderFavorites' => 'Réorganiser les favoris', 'liveTv.reorderFavorites' => 'Réorganiser les favoris',
'liveTv.favoritesLoadFailed' => 'Impossible de charger les favoris. Vérifiez votre connexion et réessayez.',
'liveTv.joinSession' => 'Rejoindre la session en cours', 'liveTv.joinSession' => 'Rejoindre la session en cours',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Regarder depuis le début (il y a ${minutes} min)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Regarder depuis le début (il y a ${minutes} min)',
'liveTv.watchLive' => 'Regarder en direct', 'liveTv.watchLive' => 'Regarder en direct',
@@ -3121,10 +3126,10 @@ extension on TranslationsFr {
'downloads.episodesQueued' => ({required Object count}) => '${count} épisodes en attente de téléchargement', 'downloads.episodesQueued' => ({required Object count}) => '${count} épisodes en attente de téléchargement',
'downloads.downloadDeleted' => 'Télécharger supprimé', 'downloads.downloadDeleted' => 'Télécharger supprimé',
'downloads.deleteConfirm' => ({required Object title}) => 'Supprimer "${title}" de cet appareil ?', 'downloads.deleteConfirm' => ({required Object title}) => 'Supprimer "${title}" de cet appareil ?',
'downloads.cancelledDownloadTitle' => 'Téléchargement annulé',
'downloads.cancelledDownloadMessage' => 'Ce téléchargement a été annulé. Que voulez-vous faire ?',
_ => null, _ => null,
} ?? switch (path) { } ?? 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', 'downloads.allEpisodesAlreadyDownloaded' => 'Tous les épisodes sont déjà téléchargés',
'downloads.resumeDownload' => 'Reprendre le téléchargement', 'downloads.resumeDownload' => 'Reprendre le téléchargement',
'downloads.cancelledDownload' => 'Téléchargement annulé', 'downloads.cancelledDownload' => 'Téléchargement annulé',
@@ -3148,6 +3153,7 @@ extension on TranslationsFr {
'downloads.customAmount' => 'Quantité personnalisée...', 'downloads.customAmount' => 'Quantité personnalisée...',
'downloads.includeSpecials' => 'Inclure les spéciaux', 'downloads.includeSpecials' => 'Inclure les spéciaux',
'downloads.howManyEpisodes' => 'Combien d\'épisodes ?', 'downloads.howManyEpisodes' => 'Combien d\'épisodes ?',
'downloads.invalidEpisodeCount' => 'Saisissez un nombre d\'épisodes valide.',
'downloads.keepSynced' => 'Garder synchronisé', 'downloads.keepSynced' => 'Garder synchronisé',
'downloads.downloadOnce' => 'Télécharger une fois', 'downloads.downloadOnce' => 'Télécharger une fois',
'downloads.keepNUnwatched' => ({required Object count}) => 'Garder ${count} non vus', 'downloads.keepNUnwatched' => ({required Object count}) => 'Garder ${count} non vus',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesIt extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Rimosso da Continua a guardare'; @override String get removedFromContinueWatching => 'Rimosso da Continua a guardare';
@override String errorLoading({required Object error}) => 'Errore: ${error}'; @override String errorLoading({required Object error}) => 'Errore: ${error}';
@override String get streamInterrupted => 'La riproduzione si è interrotta. Premi Riproduci o scorri per riprovare.'; @override String get streamInterrupted => 'La riproduzione si è interrotta. Premi Riproduci o scorri per riprovare.';
@override String get liveStreamInterrupted => 'La diretta si è interrotta. Premi Riproduci per riprovare.';
@override String get fileInfoNotAvailable => 'Informazioni sul file non disponibili'; @override String get fileInfoNotAvailable => 'Informazioni sul file non disponibili';
@override String errorLoadingFileInfo({required Object error}) => 'Errore caricamento informazioni sul file: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Errore caricamento informazioni sul file: ${error}';
@override String get errorLoadingSeries => 'Errore caricamento serie'; @override String get errorLoadingSeries => 'Errore caricamento serie';
@@ -1124,6 +1125,7 @@ class _TranslationsLiveTvIt extends TranslationsLiveTvEn {
@override String get watchChannel => 'Guarda canale'; @override String get watchChannel => 'Guarda canale';
@override String get favorites => 'Preferiti'; @override String get favorites => 'Preferiti';
@override String get reorderFavorites => 'Riordina preferiti'; @override String get reorderFavorites => 'Riordina preferiti';
@override String get favoritesLoadFailed => 'Impossibile caricare i preferiti. Controlla la connessione e riprova.';
@override String get joinSession => 'Partecipa alla sessione in corso'; @override String get joinSession => 'Partecipa alla sessione in corso';
@override String watchFromStart({required Object minutes}) => 'Guarda dall\'inizio (${minutes} min fa)'; @override String watchFromStart({required Object minutes}) => 'Guarda dall\'inizio (${minutes} min fa)';
@override String get watchLive => 'Guarda in diretta'; @override String get watchLive => 'Guarda in diretta';
@@ -1383,6 +1385,7 @@ class _TranslationsDownloadsIt extends TranslationsDownloadsEn {
@override String get customAmount => 'Quantità personalizzata...'; @override String get customAmount => 'Quantità personalizzata...';
@override String get includeSpecials => 'Includi gli speciali'; @override String get includeSpecials => 'Includi gli speciali';
@override String get howManyEpisodes => 'Quanti episodi?'; @override String get howManyEpisodes => 'Quanti episodi?';
@override String get invalidEpisodeCount => 'Inserisci un numero di episodi valido.';
@override String get keepSynced => 'Mantieni sincronizzato'; @override String get keepSynced => 'Mantieni sincronizzato';
@override String get downloadOnce => 'Scarica una volta'; @override String get downloadOnce => 'Scarica una volta';
@override String keepNUnwatched({required Object count}) => 'Mantieni ${count} non visti'; @override String keepNUnwatched({required Object count}) => 'Mantieni ${count} non visti';
@@ -2601,6 +2604,7 @@ extension on TranslationsIt {
'messages.removedFromContinueWatching' => 'Rimosso da Continua a guardare', 'messages.removedFromContinueWatching' => 'Rimosso da Continua a guardare',
'messages.errorLoading' => ({required Object error}) => 'Errore: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Errore: ${error}',
'messages.streamInterrupted' => 'La riproduzione si è interrotta. Premi Riproduci o scorri per riprovare.', 'messages.streamInterrupted' => 'La riproduzione si è interrotta. Premi Riproduci o scorri per riprovare.',
'messages.liveStreamInterrupted' => 'La diretta si è interrotta. Premi Riproduci per riprovare.',
'messages.fileInfoNotAvailable' => 'Informazioni sul file non disponibili', 'messages.fileInfoNotAvailable' => 'Informazioni sul file non disponibili',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Errore caricamento informazioni sul file: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Errore caricamento informazioni sul file: ${error}',
'messages.errorLoadingSeries' => 'Errore caricamento serie', 'messages.errorLoadingSeries' => 'Errore caricamento serie',
@@ -2608,9 +2612,9 @@ extension on TranslationsIt {
'messages.noDescriptionAvailable' => 'Nessuna descrizione disponibile', 'messages.noDescriptionAvailable' => 'Nessuna descrizione disponibile',
'messages.noProfilesAvailable' => 'Nessun profilo disponibile', 'messages.noProfilesAvailable' => 'Nessun profilo disponibile',
'messages.contactAdminForProfiles' => 'Contatta l\'amministratore del server per aggiungere profili', 'messages.contactAdminForProfiles' => 'Contatta l\'amministratore del server per aggiungere profili',
'messages.unableToDetermineLibrarySection' => 'Impossibile determinare la sezione della libreria per questo elemento',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Impossibile determinare la sezione della libreria per questo elemento',
'messages.logsCleared' => 'Log eliminati', 'messages.logsCleared' => 'Log eliminati',
'messages.logsCopied' => 'Log copiati negli appunti', 'messages.logsCopied' => 'Log copiati negli appunti',
'messages.noLogsAvailable' => 'Nessun log disponibile', 'messages.noLogsAvailable' => 'Nessun log disponibile',
@@ -2935,6 +2939,7 @@ extension on TranslationsIt {
'liveTv.watchChannel' => 'Guarda canale', 'liveTv.watchChannel' => 'Guarda canale',
'liveTv.favorites' => 'Preferiti', 'liveTv.favorites' => 'Preferiti',
'liveTv.reorderFavorites' => 'Riordina preferiti', 'liveTv.reorderFavorites' => 'Riordina preferiti',
'liveTv.favoritesLoadFailed' => 'Impossibile caricare i preferiti. Controlla la connessione e riprova.',
'liveTv.joinSession' => 'Partecipa alla sessione in corso', 'liveTv.joinSession' => 'Partecipa alla sessione in corso',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Guarda dall\'inizio (${minutes} min fa)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Guarda dall\'inizio (${minutes} min fa)',
'liveTv.watchLive' => 'Guarda in diretta', 'liveTv.watchLive' => 'Guarda in diretta',
@@ -3121,10 +3126,10 @@ extension on TranslationsIt {
'downloads.episodesQueued' => ({required Object count}) => '${count} episodi in coda per il download', 'downloads.episodesQueued' => ({required Object count}) => '${count} episodi in coda per il download',
'downloads.downloadDeleted' => 'Download eliminato', 'downloads.downloadDeleted' => 'Download eliminato',
'downloads.deleteConfirm' => ({required Object title}) => 'Eliminare "${title}" da questo dispositivo?', 'downloads.deleteConfirm' => ({required Object title}) => 'Eliminare "${title}" da questo dispositivo?',
'downloads.cancelledDownloadTitle' => 'Download annullato',
'downloads.cancelledDownloadMessage' => 'Questo download è stato annullato. Cosa vuoi fare?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => 'Download annullato',
'downloads.cancelledDownloadMessage' => 'Questo download è stato annullato. Cosa vuoi fare?',
'downloads.allEpisodesAlreadyDownloaded' => 'Tutti gli episodi sono già stati scaricati', 'downloads.allEpisodesAlreadyDownloaded' => 'Tutti gli episodi sono già stati scaricati',
'downloads.resumeDownload' => 'Riprendi download', 'downloads.resumeDownload' => 'Riprendi download',
'downloads.cancelledDownload' => 'Download annullato', 'downloads.cancelledDownload' => 'Download annullato',
@@ -3148,6 +3153,7 @@ extension on TranslationsIt {
'downloads.customAmount' => 'Quantità personalizzata...', 'downloads.customAmount' => 'Quantità personalizzata...',
'downloads.includeSpecials' => 'Includi gli speciali', 'downloads.includeSpecials' => 'Includi gli speciali',
'downloads.howManyEpisodes' => 'Quanti episodi?', 'downloads.howManyEpisodes' => 'Quanti episodi?',
'downloads.invalidEpisodeCount' => 'Inserisci un numero di episodi valido.',
'downloads.keepSynced' => 'Mantieni sincronizzato', 'downloads.keepSynced' => 'Mantieni sincronizzato',
'downloads.downloadOnce' => 'Scarica una volta', 'downloads.downloadOnce' => 'Scarica una volta',
'downloads.keepNUnwatched' => ({required Object count}) => 'Mantieni ${count} non visti', 'downloads.keepNUnwatched' => ({required Object count}) => 'Mantieni ${count} non visti',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesJa extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => '視聴中から削除しました'; @override String get removedFromContinueWatching => '視聴中から削除しました';
@override String errorLoading({required Object error}) => 'エラー: ${error}'; @override String errorLoading({required Object error}) => 'エラー: ${error}';
@override String get streamInterrupted => 'ストリームが中断されました。再生を押すかシークして再試行してください。'; @override String get streamInterrupted => 'ストリームが中断されました。再生を押すかシークして再試行してください。';
@override String get liveStreamInterrupted => 'ライブストリームが中断されました。再生を押して再試行してください。';
@override String get fileInfoNotAvailable => 'ファイル情報が利用できません'; @override String get fileInfoNotAvailable => 'ファイル情報が利用できません';
@override String errorLoadingFileInfo({required Object error}) => 'ファイル情報の読み込みエラー: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'ファイル情報の読み込みエラー: ${error}';
@override String get errorLoadingSeries => 'シリーズの読み込みエラー'; @override String get errorLoadingSeries => 'シリーズの読み込みエラー';
@@ -1123,6 +1124,7 @@ class _TranslationsLiveTvJa extends TranslationsLiveTvEn {
@override String get watchChannel => 'チャンネルを視聴'; @override String get watchChannel => 'チャンネルを視聴';
@override String get favorites => 'お気に入り'; @override String get favorites => 'お気に入り';
@override String get reorderFavorites => 'お気に入りを並べ替え'; @override String get reorderFavorites => 'お気に入りを並べ替え';
@override String get favoritesLoadFailed => 'お気に入りを読み込めませんでした。接続を確認してもう一度お試しください。';
@override String get joinSession => '進行中のセッションに参加'; @override String get joinSession => '進行中のセッションに参加';
@override String watchFromStart({required Object minutes}) => '最初から視聴(${minutes}分前に開始)'; @override String watchFromStart({required Object minutes}) => '最初から視聴(${minutes}分前に開始)';
@override String get watchLive => 'ライブで視聴'; @override String get watchLive => 'ライブで視聴';
@@ -1381,6 +1383,7 @@ class _TranslationsDownloadsJa extends TranslationsDownloadsEn {
@override String get customAmount => '数を指定...'; @override String get customAmount => '数を指定...';
@override String get includeSpecials => 'スペシャルを含める'; @override String get includeSpecials => 'スペシャルを含める';
@override String get howManyEpisodes => '何エピソード?'; @override String get howManyEpisodes => '何エピソード?';
@override String get invalidEpisodeCount => '有効なエピソード数を入力してください。';
@override String get keepSynced => '同期を維持'; @override String get keepSynced => '同期を維持';
@override String get downloadOnce => '一度だけダウンロード'; @override String get downloadOnce => '一度だけダウンロード';
@override String keepNUnwatched({required Object count}) => '未視聴を${count}件保持'; @override String keepNUnwatched({required Object count}) => '未視聴を${count}件保持';
@@ -2599,6 +2602,7 @@ extension on TranslationsJa {
'messages.removedFromContinueWatching' => '視聴中から削除しました', 'messages.removedFromContinueWatching' => '視聴中から削除しました',
'messages.errorLoading' => ({required Object error}) => 'エラー: ${error}', 'messages.errorLoading' => ({required Object error}) => 'エラー: ${error}',
'messages.streamInterrupted' => 'ストリームが中断されました。再生を押すかシークして再試行してください。', 'messages.streamInterrupted' => 'ストリームが中断されました。再生を押すかシークして再試行してください。',
'messages.liveStreamInterrupted' => 'ライブストリームが中断されました。再生を押して再試行してください。',
'messages.fileInfoNotAvailable' => 'ファイル情報が利用できません', 'messages.fileInfoNotAvailable' => 'ファイル情報が利用できません',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'ファイル情報の読み込みエラー: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'ファイル情報の読み込みエラー: ${error}',
'messages.errorLoadingSeries' => 'シリーズの読み込みエラー', 'messages.errorLoadingSeries' => 'シリーズの読み込みエラー',
@@ -2606,9 +2610,9 @@ extension on TranslationsJa {
'messages.noDescriptionAvailable' => '説明はありません', 'messages.noDescriptionAvailable' => '説明はありません',
'messages.noProfilesAvailable' => '利用可能なプロフィールがありません', 'messages.noProfilesAvailable' => '利用可能なプロフィールがありません',
'messages.contactAdminForProfiles' => 'プロファイルを追加するにはサーバー管理者に連絡してください', 'messages.contactAdminForProfiles' => 'プロファイルを追加するにはサーバー管理者に連絡してください',
'messages.unableToDetermineLibrarySection' => 'このアイテムのライブラリセクションを判別できません',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'このアイテムのライブラリセクションを判別できません',
'messages.logsCleared' => 'ログをクリアしました', 'messages.logsCleared' => 'ログをクリアしました',
'messages.logsCopied' => 'ログをクリップボードにコピーしました', 'messages.logsCopied' => 'ログをクリップボードにコピーしました',
'messages.noLogsAvailable' => 'ログがありません', 'messages.noLogsAvailable' => 'ログがありません',
@@ -2933,6 +2937,7 @@ extension on TranslationsJa {
'liveTv.watchChannel' => 'チャンネルを視聴', 'liveTv.watchChannel' => 'チャンネルを視聴',
'liveTv.favorites' => 'お気に入り', 'liveTv.favorites' => 'お気に入り',
'liveTv.reorderFavorites' => 'お気に入りを並べ替え', 'liveTv.reorderFavorites' => 'お気に入りを並べ替え',
'liveTv.favoritesLoadFailed' => 'お気に入りを読み込めませんでした。接続を確認してもう一度お試しください。',
'liveTv.joinSession' => '進行中のセッションに参加', 'liveTv.joinSession' => '進行中のセッションに参加',
'liveTv.watchFromStart' => ({required Object minutes}) => '最初から視聴(${minutes}分前に開始)', 'liveTv.watchFromStart' => ({required Object minutes}) => '最初から視聴(${minutes}分前に開始)',
'liveTv.watchLive' => 'ライブで視聴', 'liveTv.watchLive' => 'ライブで視聴',
@@ -3119,10 +3124,10 @@ extension on TranslationsJa {
'downloads.episodesQueued' => ({required Object count}) => '${count}エピソードをダウンロードキューに追加しました', 'downloads.episodesQueued' => ({required Object count}) => '${count}エピソードをダウンロードキューに追加しました',
'downloads.downloadDeleted' => 'ダウンロードを削除しました', 'downloads.downloadDeleted' => 'ダウンロードを削除しました',
'downloads.deleteConfirm' => ({required Object title}) => 'このデバイスから「${title}」を削除しますか?', 'downloads.deleteConfirm' => ({required Object title}) => 'このデバイスから「${title}」を削除しますか?',
'downloads.cancelledDownloadTitle' => 'キャンセルされたダウンロード',
'downloads.cancelledDownloadMessage' => 'このダウンロードはキャンセルされました。どうしますか?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => 'キャンセルされたダウンロード',
'downloads.cancelledDownloadMessage' => 'このダウンロードはキャンセルされました。どうしますか?',
'downloads.allEpisodesAlreadyDownloaded' => 'すべてのエピソードはすでにダウンロード済みです', 'downloads.allEpisodesAlreadyDownloaded' => 'すべてのエピソードはすでにダウンロード済みです',
'downloads.resumeDownload' => 'ダウンロードを再開', 'downloads.resumeDownload' => 'ダウンロードを再開',
'downloads.cancelledDownload' => 'キャンセルされたダウンロード', 'downloads.cancelledDownload' => 'キャンセルされたダウンロード',
@@ -3146,6 +3151,7 @@ extension on TranslationsJa {
'downloads.customAmount' => '数を指定...', 'downloads.customAmount' => '数を指定...',
'downloads.includeSpecials' => 'スペシャルを含める', 'downloads.includeSpecials' => 'スペシャルを含める',
'downloads.howManyEpisodes' => '何エピソード?', 'downloads.howManyEpisodes' => '何エピソード?',
'downloads.invalidEpisodeCount' => '有効なエピソード数を入力してください。',
'downloads.keepSynced' => '同期を維持', 'downloads.keepSynced' => '同期を維持',
'downloads.downloadOnce' => '一度だけダウンロード', 'downloads.downloadOnce' => '一度だけダウンロード',
'downloads.keepNUnwatched' => ({required Object count}) => '未視聴を${count}件保持', 'downloads.keepNUnwatched' => ({required Object count}) => '未視聴を${count}件保持',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesKo extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => '계속 시청 목록에서 제거됨'; @override String get removedFromContinueWatching => '계속 시청 목록에서 제거됨';
@override String errorLoading({required Object error}) => '오류: ${error}'; @override String errorLoading({required Object error}) => '오류: ${error}';
@override String get streamInterrupted => '스트림이 중단되었습니다. 재생을 누르거나 탐색하여 다시 시도하세요.'; @override String get streamInterrupted => '스트림이 중단되었습니다. 재생을 누르거나 탐색하여 다시 시도하세요.';
@override String get liveStreamInterrupted => '라이브 스트림이 중단되었습니다. 재생을 눌러 다시 시도하세요.';
@override String get fileInfoNotAvailable => '파일 정보가 없습니다'; @override String get fileInfoNotAvailable => '파일 정보가 없습니다';
@override String errorLoadingFileInfo({required Object error}) => '파일 정보 로딩 중 오류: ${error}'; @override String errorLoadingFileInfo({required Object error}) => '파일 정보 로딩 중 오류: ${error}';
@override String get errorLoadingSeries => '시리즈 로딩 중 오류'; @override String get errorLoadingSeries => '시리즈 로딩 중 오류';
@@ -1123,6 +1124,7 @@ class _TranslationsLiveTvKo extends TranslationsLiveTvEn {
@override String get watchChannel => '채널 시청'; @override String get watchChannel => '채널 시청';
@override String get favorites => '즐겨찾기'; @override String get favorites => '즐겨찾기';
@override String get reorderFavorites => '즐겨찾기 순서 변경'; @override String get reorderFavorites => '즐겨찾기 순서 변경';
@override String get favoritesLoadFailed => '즐겨찾기를 불러올 수 없습니다. 연결을 확인하고 다시 시도하세요.';
@override String get joinSession => '진행 중인 세션 참여'; @override String get joinSession => '진행 중인 세션 참여';
@override String watchFromStart({required Object minutes}) => '처음부터 시청 (${minutes}분 전 시작)'; @override String watchFromStart({required Object minutes}) => '처음부터 시청 (${minutes}분 전 시작)';
@override String get watchLive => '실시간 시청'; @override String get watchLive => '실시간 시청';
@@ -1381,6 +1383,7 @@ class _TranslationsDownloadsKo extends TranslationsDownloadsEn {
@override String get customAmount => '직접 입력...'; @override String get customAmount => '직접 입력...';
@override String get includeSpecials => '스페셜 포함'; @override String get includeSpecials => '스페셜 포함';
@override String get howManyEpisodes => '몇 개의 에피소드?'; @override String get howManyEpisodes => '몇 개의 에피소드?';
@override String get invalidEpisodeCount => '올바른 에피소드 수를 입력하세요.';
@override String get keepSynced => '동기화 유지'; @override String get keepSynced => '동기화 유지';
@override String get downloadOnce => '한 번만 다운로드'; @override String get downloadOnce => '한 번만 다운로드';
@override String keepNUnwatched({required Object count}) => '미시청 ${count}개 유지'; @override String keepNUnwatched({required Object count}) => '미시청 ${count}개 유지';
@@ -2599,6 +2602,7 @@ extension on TranslationsKo {
'messages.removedFromContinueWatching' => '계속 시청 목록에서 제거됨', 'messages.removedFromContinueWatching' => '계속 시청 목록에서 제거됨',
'messages.errorLoading' => ({required Object error}) => '오류: ${error}', 'messages.errorLoading' => ({required Object error}) => '오류: ${error}',
'messages.streamInterrupted' => '스트림이 중단되었습니다. 재생을 누르거나 탐색하여 다시 시도하세요.', 'messages.streamInterrupted' => '스트림이 중단되었습니다. 재생을 누르거나 탐색하여 다시 시도하세요.',
'messages.liveStreamInterrupted' => '라이브 스트림이 중단되었습니다. 재생을 눌러 다시 시도하세요.',
'messages.fileInfoNotAvailable' => '파일 정보가 없습니다', 'messages.fileInfoNotAvailable' => '파일 정보가 없습니다',
'messages.errorLoadingFileInfo' => ({required Object error}) => '파일 정보 로딩 중 오류: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => '파일 정보 로딩 중 오류: ${error}',
'messages.errorLoadingSeries' => '시리즈 로딩 중 오류', 'messages.errorLoadingSeries' => '시리즈 로딩 중 오류',
@@ -2606,9 +2610,9 @@ extension on TranslationsKo {
'messages.noDescriptionAvailable' => '설명이 없습니다', 'messages.noDescriptionAvailable' => '설명이 없습니다',
'messages.noProfilesAvailable' => '사용 가능한 프로필이 없습니다', 'messages.noProfilesAvailable' => '사용 가능한 프로필이 없습니다',
'messages.contactAdminForProfiles' => '프로필을 추가하려면 서버 관리자에게 문의하세요', 'messages.contactAdminForProfiles' => '프로필을 추가하려면 서버 관리자에게 문의하세요',
'messages.unableToDetermineLibrarySection' => '이 항목의 라이브러리 섹션을 확인할 수 없습니다',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => '이 항목의 라이브러리 섹션을 확인할 수 없습니다',
'messages.logsCleared' => '로그가 삭제 되었습니다', 'messages.logsCleared' => '로그가 삭제 되었습니다',
'messages.logsCopied' => '로그가 클립보드에 복사 되었습니다', 'messages.logsCopied' => '로그가 클립보드에 복사 되었습니다',
'messages.noLogsAvailable' => '사용 가능한 로그가 없습니다', 'messages.noLogsAvailable' => '사용 가능한 로그가 없습니다',
@@ -2933,6 +2937,7 @@ extension on TranslationsKo {
'liveTv.watchChannel' => '채널 시청', 'liveTv.watchChannel' => '채널 시청',
'liveTv.favorites' => '즐겨찾기', 'liveTv.favorites' => '즐겨찾기',
'liveTv.reorderFavorites' => '즐겨찾기 순서 변경', 'liveTv.reorderFavorites' => '즐겨찾기 순서 변경',
'liveTv.favoritesLoadFailed' => '즐겨찾기를 불러올 수 없습니다. 연결을 확인하고 다시 시도하세요.',
'liveTv.joinSession' => '진행 중인 세션 참여', 'liveTv.joinSession' => '진행 중인 세션 참여',
'liveTv.watchFromStart' => ({required Object minutes}) => '처음부터 시청 (${minutes}분 전 시작)', 'liveTv.watchFromStart' => ({required Object minutes}) => '처음부터 시청 (${minutes}분 전 시작)',
'liveTv.watchLive' => '실시간 시청', 'liveTv.watchLive' => '실시간 시청',
@@ -3119,10 +3124,10 @@ extension on TranslationsKo {
'downloads.episodesQueued' => ({required Object count}) => '${count} 에피소드가 다운로드 대기열에 추가 되었습니다', 'downloads.episodesQueued' => ({required Object count}) => '${count} 에피소드가 다운로드 대기열에 추가 되었습니다',
'downloads.downloadDeleted' => '다운로드 삭제됨', 'downloads.downloadDeleted' => '다운로드 삭제됨',
'downloads.deleteConfirm' => ({required Object title}) => '이 기기에서 "${title}"을(를) 삭제할까요?', 'downloads.deleteConfirm' => ({required Object title}) => '이 기기에서 "${title}"을(를) 삭제할까요?',
'downloads.cancelledDownloadTitle' => '취소된 다운로드',
'downloads.cancelledDownloadMessage' => '이 다운로드가 취소되었습니다. 어떻게 하시겠습니까?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => '취소된 다운로드',
'downloads.cancelledDownloadMessage' => '이 다운로드가 취소되었습니다. 어떻게 하시겠습니까?',
'downloads.allEpisodesAlreadyDownloaded' => '모든 에피소드가 이미 다운로드되었습니다', 'downloads.allEpisodesAlreadyDownloaded' => '모든 에피소드가 이미 다운로드되었습니다',
'downloads.resumeDownload' => '다운로드 재개', 'downloads.resumeDownload' => '다운로드 재개',
'downloads.cancelledDownload' => '취소된 다운로드', 'downloads.cancelledDownload' => '취소된 다운로드',
@@ -3146,6 +3151,7 @@ extension on TranslationsKo {
'downloads.customAmount' => '직접 입력...', 'downloads.customAmount' => '직접 입력...',
'downloads.includeSpecials' => '스페셜 포함', 'downloads.includeSpecials' => '스페셜 포함',
'downloads.howManyEpisodes' => '몇 개의 에피소드?', 'downloads.howManyEpisodes' => '몇 개의 에피소드?',
'downloads.invalidEpisodeCount' => '올바른 에피소드 수를 입력하세요.',
'downloads.keepSynced' => '동기화 유지', 'downloads.keepSynced' => '동기화 유지',
'downloads.downloadOnce' => '한 번만 다운로드', 'downloads.downloadOnce' => '한 번만 다운로드',
'downloads.keepNUnwatched' => ({required Object count}) => '미시청 ${count}개 유지', 'downloads.keepNUnwatched' => ({required Object count}) => '미시청 ${count}개 유지',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesNb extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Fjernet fra Fortsett å se'; @override String get removedFromContinueWatching => 'Fjernet fra Fortsett å se';
@override String errorLoading({required Object error}) => 'Feil: ${error}'; @override String errorLoading({required Object error}) => 'Feil: ${error}';
@override String get streamInterrupted => 'Avspillingen ble avbrutt. Trykk på Spill av eller spol for å prøve på nytt.'; @override String get streamInterrupted => 'Avspillingen ble avbrutt. Trykk på Spill av eller spol for å prøve på nytt.';
@override String get liveStreamInterrupted => 'Direktesendingen ble avbrutt. Trykk på Spill av for å prøve på nytt.';
@override String get fileInfoNotAvailable => 'Filinformasjon ikke tilgjengelig'; @override String get fileInfoNotAvailable => 'Filinformasjon ikke tilgjengelig';
@override String errorLoadingFileInfo({required Object error}) => 'Feil ved lasting av filinformasjon: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Feil ved lasting av filinformasjon: ${error}';
@override String get errorLoadingSeries => 'Feil ved lasting av serie'; @override String get errorLoadingSeries => 'Feil ved lasting av serie';
@@ -1124,6 +1125,7 @@ class _TranslationsLiveTvNb extends TranslationsLiveTvEn {
@override String get watchChannel => 'Se kanal'; @override String get watchChannel => 'Se kanal';
@override String get favorites => 'Favoritter'; @override String get favorites => 'Favoritter';
@override String get reorderFavorites => 'Endre rekkefølge på favoritter'; @override String get reorderFavorites => 'Endre rekkefølge på favoritter';
@override String get favoritesLoadFailed => 'Kunne ikke laste inn favoritter. Kontroller tilkoblingen og prøv på nytt.';
@override String get joinSession => 'Bli med i pågående økt'; @override String get joinSession => 'Bli med i pågående økt';
@override String watchFromStart({required Object minutes}) => 'Se fra starten (${minutes} min siden)'; @override String watchFromStart({required Object minutes}) => 'Se fra starten (${minutes} min siden)';
@override String get watchLive => 'Se direkte'; @override String get watchLive => 'Se direkte';
@@ -1383,6 +1385,7 @@ class _TranslationsDownloadsNb extends TranslationsDownloadsEn {
@override String get customAmount => 'Egendefinert antall...'; @override String get customAmount => 'Egendefinert antall...';
@override String get includeSpecials => 'Inkluder spesialepisoder'; @override String get includeSpecials => 'Inkluder spesialepisoder';
@override String get howManyEpisodes => 'Hvor mange episoder?'; @override String get howManyEpisodes => 'Hvor mange episoder?';
@override String get invalidEpisodeCount => 'Angi et gyldig antall episoder.';
@override String get keepSynced => 'Hold synkronisert'; @override String get keepSynced => 'Hold synkronisert';
@override String get downloadOnce => 'Last ned én gang'; @override String get downloadOnce => 'Last ned én gang';
@override String keepNUnwatched({required Object count}) => 'Behold ${count} usette'; @override String keepNUnwatched({required Object count}) => 'Behold ${count} usette';
@@ -2601,6 +2604,7 @@ extension on TranslationsNb {
'messages.removedFromContinueWatching' => 'Fjernet fra Fortsett å se', 'messages.removedFromContinueWatching' => 'Fjernet fra Fortsett å se',
'messages.errorLoading' => ({required Object error}) => 'Feil: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Feil: ${error}',
'messages.streamInterrupted' => 'Avspillingen ble avbrutt. Trykk på Spill av eller spol for å prøve på nytt.', 'messages.streamInterrupted' => 'Avspillingen ble avbrutt. Trykk på Spill av eller spol for å prøve på nytt.',
'messages.liveStreamInterrupted' => 'Direktesendingen ble avbrutt. Trykk på Spill av for å prøve på nytt.',
'messages.fileInfoNotAvailable' => 'Filinformasjon ikke tilgjengelig', 'messages.fileInfoNotAvailable' => 'Filinformasjon ikke tilgjengelig',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Feil ved lasting av filinformasjon: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Feil ved lasting av filinformasjon: ${error}',
'messages.errorLoadingSeries' => 'Feil ved lasting av serie', 'messages.errorLoadingSeries' => 'Feil ved lasting av serie',
@@ -2608,9 +2612,9 @@ extension on TranslationsNb {
'messages.noDescriptionAvailable' => 'Ingen beskrivelse tilgjengelig', 'messages.noDescriptionAvailable' => 'Ingen beskrivelse tilgjengelig',
'messages.noProfilesAvailable' => 'Ingen profiler tilgjengelige', 'messages.noProfilesAvailable' => 'Ingen profiler tilgjengelige',
'messages.contactAdminForProfiles' => 'Kontakt serveradministratoren din for å legge til profiler', 'messages.contactAdminForProfiles' => 'Kontakt serveradministratoren din for å legge til profiler',
'messages.unableToDetermineLibrarySection' => 'Kan ikke fastslå bibliotekseksjonen for dette elementet',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Kan ikke fastslå bibliotekseksjonen for dette elementet',
'messages.logsCleared' => 'Logger tømt', 'messages.logsCleared' => 'Logger tømt',
'messages.logsCopied' => 'Logger kopiert til utklippstavle', 'messages.logsCopied' => 'Logger kopiert til utklippstavle',
'messages.noLogsAvailable' => 'Ingen logger tilgjengelig', 'messages.noLogsAvailable' => 'Ingen logger tilgjengelig',
@@ -2935,6 +2939,7 @@ extension on TranslationsNb {
'liveTv.watchChannel' => 'Se kanal', 'liveTv.watchChannel' => 'Se kanal',
'liveTv.favorites' => 'Favoritter', 'liveTv.favorites' => 'Favoritter',
'liveTv.reorderFavorites' => 'Endre rekkefølge på favoritter', 'liveTv.reorderFavorites' => 'Endre rekkefølge på favoritter',
'liveTv.favoritesLoadFailed' => 'Kunne ikke laste inn favoritter. Kontroller tilkoblingen og prøv på nytt.',
'liveTv.joinSession' => 'Bli med i pågående økt', 'liveTv.joinSession' => 'Bli med i pågående økt',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Se fra starten (${minutes} min siden)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Se fra starten (${minutes} min siden)',
'liveTv.watchLive' => 'Se direkte', 'liveTv.watchLive' => 'Se direkte',
@@ -3121,10 +3126,10 @@ extension on TranslationsNb {
'downloads.episodesQueued' => ({required Object count}) => '${count} episoder i nedlastingskø', 'downloads.episodesQueued' => ({required Object count}) => '${count} episoder i nedlastingskø',
'downloads.downloadDeleted' => 'Nedlasting slettet', 'downloads.downloadDeleted' => 'Nedlasting slettet',
'downloads.deleteConfirm' => ({required Object title}) => 'Slette "${title}" fra denne enheten?', 'downloads.deleteConfirm' => ({required Object title}) => 'Slette "${title}" fra denne enheten?',
'downloads.cancelledDownloadTitle' => 'Avbrutt nedlasting',
'downloads.cancelledDownloadMessage' => 'Denne nedlastingen ble avbrutt. Hva vil du gjøre?',
_ => null, _ => null,
} ?? switch (path) { } ?? 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', 'downloads.allEpisodesAlreadyDownloaded' => 'Alle episoder er allerede lastet ned',
'downloads.resumeDownload' => 'Gjenoppta nedlasting', 'downloads.resumeDownload' => 'Gjenoppta nedlasting',
'downloads.cancelledDownload' => 'Avbrutt nedlasting', 'downloads.cancelledDownload' => 'Avbrutt nedlasting',
@@ -3148,6 +3153,7 @@ extension on TranslationsNb {
'downloads.customAmount' => 'Egendefinert antall...', 'downloads.customAmount' => 'Egendefinert antall...',
'downloads.includeSpecials' => 'Inkluder spesialepisoder', 'downloads.includeSpecials' => 'Inkluder spesialepisoder',
'downloads.howManyEpisodes' => 'Hvor mange episoder?', 'downloads.howManyEpisodes' => 'Hvor mange episoder?',
'downloads.invalidEpisodeCount' => 'Angi et gyldig antall episoder.',
'downloads.keepSynced' => 'Hold synkronisert', 'downloads.keepSynced' => 'Hold synkronisert',
'downloads.downloadOnce' => 'Last ned én gang', 'downloads.downloadOnce' => 'Last ned én gang',
'downloads.keepNUnwatched' => ({required Object count}) => 'Behold ${count} usette', 'downloads.keepNUnwatched' => ({required Object count}) => 'Behold ${count} usette',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesNl extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Verwijderd uit Doorgaan met kijken'; @override String get removedFromContinueWatching => 'Verwijderd uit Doorgaan met kijken';
@override String errorLoading({required Object error}) => 'Fout: ${error}'; @override String errorLoading({required Object error}) => 'Fout: ${error}';
@override String get streamInterrupted => 'De stream is onderbroken. Druk op afspelen of spoel om het opnieuw te proberen.'; @override String get streamInterrupted => 'De stream is onderbroken. Druk op afspelen of spoel om het opnieuw te proberen.';
@override String get liveStreamInterrupted => 'De livestream is onderbroken. Druk op afspelen om het opnieuw te proberen.';
@override String get fileInfoNotAvailable => 'Bestand informatie niet beschikbaar'; @override String get fileInfoNotAvailable => 'Bestand informatie niet beschikbaar';
@override String errorLoadingFileInfo({required Object error}) => 'Fout bij laden bestand info: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Fout bij laden bestand info: ${error}';
@override String get errorLoadingSeries => 'Fout bij laden serie'; @override String get errorLoadingSeries => 'Fout bij laden serie';
@@ -1124,6 +1125,7 @@ class _TranslationsLiveTvNl extends TranslationsLiveTvEn {
@override String get watchChannel => 'Kanaal bekijken'; @override String get watchChannel => 'Kanaal bekijken';
@override String get favorites => 'Favorieten'; @override String get favorites => 'Favorieten';
@override String get reorderFavorites => 'Favorieten herordenen'; @override String get reorderFavorites => 'Favorieten herordenen';
@override String get favoritesLoadFailed => 'Favorieten konden niet worden geladen. Controleer je verbinding en probeer het opnieuw.';
@override String get joinSession => 'Deelnemen aan lopende sessie'; @override String get joinSession => 'Deelnemen aan lopende sessie';
@override String watchFromStart({required Object minutes}) => 'Kijk vanaf het begin (${minutes} min geleden)'; @override String watchFromStart({required Object minutes}) => 'Kijk vanaf het begin (${minutes} min geleden)';
@override String get watchLive => 'Live kijken'; @override String get watchLive => 'Live kijken';
@@ -1383,6 +1385,7 @@ class _TranslationsDownloadsNl extends TranslationsDownloadsEn {
@override String get customAmount => 'Aangepast aantal...'; @override String get customAmount => 'Aangepast aantal...';
@override String get includeSpecials => 'Specials opnemen'; @override String get includeSpecials => 'Specials opnemen';
@override String get howManyEpisodes => 'Hoeveel afleveringen?'; @override String get howManyEpisodes => 'Hoeveel afleveringen?';
@override String get invalidEpisodeCount => 'Voer een geldig aantal afleveringen in.';
@override String get keepSynced => 'Gesynchroniseerd houden'; @override String get keepSynced => 'Gesynchroniseerd houden';
@override String get downloadOnce => 'Eenmalig downloaden'; @override String get downloadOnce => 'Eenmalig downloaden';
@override String keepNUnwatched({required Object count}) => '${count} onbekeken behouden'; @override String keepNUnwatched({required Object count}) => '${count} onbekeken behouden';
@@ -2601,6 +2604,7 @@ extension on TranslationsNl {
'messages.removedFromContinueWatching' => 'Verwijderd uit Doorgaan met kijken', 'messages.removedFromContinueWatching' => 'Verwijderd uit Doorgaan met kijken',
'messages.errorLoading' => ({required Object error}) => 'Fout: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Fout: ${error}',
'messages.streamInterrupted' => 'De stream is onderbroken. Druk op afspelen of spoel om het opnieuw te proberen.', 'messages.streamInterrupted' => 'De stream is onderbroken. Druk op afspelen of spoel om het opnieuw te proberen.',
'messages.liveStreamInterrupted' => 'De livestream is onderbroken. Druk op afspelen om het opnieuw te proberen.',
'messages.fileInfoNotAvailable' => 'Bestand informatie niet beschikbaar', 'messages.fileInfoNotAvailable' => 'Bestand informatie niet beschikbaar',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fout bij laden bestand info: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fout bij laden bestand info: ${error}',
'messages.errorLoadingSeries' => 'Fout bij laden serie', 'messages.errorLoadingSeries' => 'Fout bij laden serie',
@@ -2608,9 +2612,9 @@ extension on TranslationsNl {
'messages.noDescriptionAvailable' => 'Geen beschrijving beschikbaar', 'messages.noDescriptionAvailable' => 'Geen beschrijving beschikbaar',
'messages.noProfilesAvailable' => 'Geen profielen beschikbaar', 'messages.noProfilesAvailable' => 'Geen profielen beschikbaar',
'messages.contactAdminForProfiles' => 'Neem contact op met je serverbeheerder om profielen toe te voegen', 'messages.contactAdminForProfiles' => 'Neem contact op met je serverbeheerder om profielen toe te voegen',
'messages.unableToDetermineLibrarySection' => 'Kan bibliotheeksectie voor dit item niet bepalen',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Kan bibliotheeksectie voor dit item niet bepalen',
'messages.logsCleared' => 'Logs gewist', 'messages.logsCleared' => 'Logs gewist',
'messages.logsCopied' => 'Logs gekopieerd naar klembord', 'messages.logsCopied' => 'Logs gekopieerd naar klembord',
'messages.noLogsAvailable' => 'Geen logs beschikbaar', 'messages.noLogsAvailable' => 'Geen logs beschikbaar',
@@ -2935,6 +2939,7 @@ extension on TranslationsNl {
'liveTv.watchChannel' => 'Kanaal bekijken', 'liveTv.watchChannel' => 'Kanaal bekijken',
'liveTv.favorites' => 'Favorieten', 'liveTv.favorites' => 'Favorieten',
'liveTv.reorderFavorites' => 'Favorieten herordenen', 'liveTv.reorderFavorites' => 'Favorieten herordenen',
'liveTv.favoritesLoadFailed' => 'Favorieten konden niet worden geladen. Controleer je verbinding en probeer het opnieuw.',
'liveTv.joinSession' => 'Deelnemen aan lopende sessie', 'liveTv.joinSession' => 'Deelnemen aan lopende sessie',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Kijk vanaf het begin (${minutes} min geleden)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Kijk vanaf het begin (${minutes} min geleden)',
'liveTv.watchLive' => 'Live kijken', 'liveTv.watchLive' => 'Live kijken',
@@ -3121,10 +3126,10 @@ extension on TranslationsNl {
'downloads.episodesQueued' => ({required Object count}) => '${count} afleveringen in wachtrij voor download', 'downloads.episodesQueued' => ({required Object count}) => '${count} afleveringen in wachtrij voor download',
'downloads.downloadDeleted' => 'Download verwijderd', 'downloads.downloadDeleted' => 'Download verwijderd',
'downloads.deleteConfirm' => ({required Object title}) => '"${title}" van dit apparaat verwijderen?', 'downloads.deleteConfirm' => ({required Object title}) => '"${title}" van dit apparaat verwijderen?',
'downloads.cancelledDownloadTitle' => 'Geannuleerde download',
'downloads.cancelledDownloadMessage' => 'Deze download is geannuleerd. Wat wil je doen?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => 'Geannuleerde download',
'downloads.cancelledDownloadMessage' => 'Deze download is geannuleerd. Wat wil je doen?',
'downloads.allEpisodesAlreadyDownloaded' => 'Alle afleveringen zijn al gedownload', 'downloads.allEpisodesAlreadyDownloaded' => 'Alle afleveringen zijn al gedownload',
'downloads.resumeDownload' => 'Download hervatten', 'downloads.resumeDownload' => 'Download hervatten',
'downloads.cancelledDownload' => 'Geannuleerde download', 'downloads.cancelledDownload' => 'Geannuleerde download',
@@ -3148,6 +3153,7 @@ extension on TranslationsNl {
'downloads.customAmount' => 'Aangepast aantal...', 'downloads.customAmount' => 'Aangepast aantal...',
'downloads.includeSpecials' => 'Specials opnemen', 'downloads.includeSpecials' => 'Specials opnemen',
'downloads.howManyEpisodes' => 'Hoeveel afleveringen?', 'downloads.howManyEpisodes' => 'Hoeveel afleveringen?',
'downloads.invalidEpisodeCount' => 'Voer een geldig aantal afleveringen in.',
'downloads.keepSynced' => 'Gesynchroniseerd houden', 'downloads.keepSynced' => 'Gesynchroniseerd houden',
'downloads.downloadOnce' => 'Eenmalig downloaden', 'downloads.downloadOnce' => 'Eenmalig downloaden',
'downloads.keepNUnwatched' => ({required Object count}) => '${count} onbekeken behouden', 'downloads.keepNUnwatched' => ({required Object count}) => '${count} onbekeken behouden',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesPl extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Usunięto z kontynuowania oglądania'; @override String get removedFromContinueWatching => 'Usunięto z kontynuowania oglądania';
@override String errorLoading({required Object error}) => 'Błąd: ${error}'; @override String errorLoading({required Object error}) => 'Błąd: ${error}';
@override String get streamInterrupted => 'Strumień został przerwany. Naciśnij odtwarzanie lub przewiń, aby spróbować ponownie.'; @override String get streamInterrupted => 'Strumień został przerwany. Naciśnij odtwarzanie lub przewiń, aby spróbować ponownie.';
@override String get liveStreamInterrupted => 'Transmisja na żywo została przerwana. Naciśnij odtwarzanie, aby spróbować ponownie.';
@override String get fileInfoNotAvailable => 'Informacje o pliku niedostępne'; @override String get fileInfoNotAvailable => 'Informacje o pliku niedostępne';
@override String errorLoadingFileInfo({required Object error}) => 'Błąd ładowania informacji o pliku: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Błąd ładowania informacji o pliku: ${error}';
@override String get errorLoadingSeries => 'Błąd ładowania serialu'; @override String get errorLoadingSeries => 'Błąd ładowania serialu';
@@ -1126,6 +1127,7 @@ class _TranslationsLiveTvPl extends TranslationsLiveTvEn {
@override String get watchChannel => 'Oglądaj kanał'; @override String get watchChannel => 'Oglądaj kanał';
@override String get favorites => 'Ulubione'; @override String get favorites => 'Ulubione';
@override String get reorderFavorites => 'Zmień kolejność ulubionych'; @override String get reorderFavorites => 'Zmień kolejność ulubionych';
@override String get favoritesLoadFailed => 'Nie udało się wczytać ulubionych. Sprawdź połączenie i spróbuj ponownie.';
@override String get joinSession => 'Dołącz do trwającej sesji'; @override String get joinSession => 'Dołącz do trwającej sesji';
@override String watchFromStart({required Object minutes}) => 'Oglądaj od początku (${minutes} min temu)'; @override String watchFromStart({required Object minutes}) => 'Oglądaj od początku (${minutes} min temu)';
@override String get watchLive => 'Oglądaj na żywo'; @override String get watchLive => 'Oglądaj na żywo';
@@ -1387,6 +1389,7 @@ class _TranslationsDownloadsPl extends TranslationsDownloadsEn {
@override String get customAmount => 'Własna ilość...'; @override String get customAmount => 'Własna ilość...';
@override String get includeSpecials => 'Uwzględnij odcinki specjalne'; @override String get includeSpecials => 'Uwzględnij odcinki specjalne';
@override String get howManyEpisodes => 'Ile odcinków?'; @override String get howManyEpisodes => 'Ile odcinków?';
@override String get invalidEpisodeCount => 'Wprowadź prawidłową liczbę odcinków.';
@override String get keepSynced => 'Synchronizuj na bieżąco'; @override String get keepSynced => 'Synchronizuj na bieżąco';
@override String get downloadOnce => 'Pobierz raz'; @override String get downloadOnce => 'Pobierz raz';
@override String keepNUnwatched({required Object count}) => 'Zachowaj ${count} nieobejrzanych'; @override String keepNUnwatched({required Object count}) => 'Zachowaj ${count} nieobejrzanych';
@@ -2605,6 +2608,7 @@ extension on TranslationsPl {
'messages.removedFromContinueWatching' => 'Usunięto z kontynuowania oglądania', 'messages.removedFromContinueWatching' => 'Usunięto z kontynuowania oglądania',
'messages.errorLoading' => ({required Object error}) => 'Błąd: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Błąd: ${error}',
'messages.streamInterrupted' => 'Strumień został przerwany. Naciśnij odtwarzanie lub przewiń, aby spróbować ponownie.', 'messages.streamInterrupted' => 'Strumień został przerwany. Naciśnij odtwarzanie lub przewiń, aby spróbować ponownie.',
'messages.liveStreamInterrupted' => 'Transmisja na żywo została przerwana. Naciśnij odtwarzanie, aby spróbować ponownie.',
'messages.fileInfoNotAvailable' => 'Informacje o pliku niedostępne', 'messages.fileInfoNotAvailable' => 'Informacje o pliku niedostępne',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Błąd ładowania informacji o pliku: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Błąd ładowania informacji o pliku: ${error}',
'messages.errorLoadingSeries' => 'Błąd ładowania serialu', 'messages.errorLoadingSeries' => 'Błąd ładowania serialu',
@@ -2612,9 +2616,9 @@ extension on TranslationsPl {
'messages.noDescriptionAvailable' => 'Brak dostępnego opisu', 'messages.noDescriptionAvailable' => 'Brak dostępnego opisu',
'messages.noProfilesAvailable' => 'Brak dostępnych profili', 'messages.noProfilesAvailable' => 'Brak dostępnych profili',
'messages.contactAdminForProfiles' => 'Skontaktuj się z administratorem serwera, aby dodać profile', 'messages.contactAdminForProfiles' => 'Skontaktuj się z administratorem serwera, aby dodać profile',
'messages.unableToDetermineLibrarySection' => 'Nie można określić sekcji biblioteki dla tego elementu',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Nie można określić sekcji biblioteki dla tego elementu',
'messages.logsCleared' => 'Logi wyczyszczone', 'messages.logsCleared' => 'Logi wyczyszczone',
'messages.logsCopied' => 'Logi skopiowane do schowka', 'messages.logsCopied' => 'Logi skopiowane do schowka',
'messages.noLogsAvailable' => 'Brak dostępnych logów', 'messages.noLogsAvailable' => 'Brak dostępnych logów',
@@ -2939,6 +2943,7 @@ extension on TranslationsPl {
'liveTv.watchChannel' => 'Oglądaj kanał', 'liveTv.watchChannel' => 'Oglądaj kanał',
'liveTv.favorites' => 'Ulubione', 'liveTv.favorites' => 'Ulubione',
'liveTv.reorderFavorites' => 'Zmień kolejność ulubionych', 'liveTv.reorderFavorites' => 'Zmień kolejność ulubionych',
'liveTv.favoritesLoadFailed' => 'Nie udało się wczytać ulubionych. Sprawdź połączenie i spróbuj ponownie.',
'liveTv.joinSession' => 'Dołącz do trwającej sesji', 'liveTv.joinSession' => 'Dołącz do trwającej sesji',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Oglądaj od początku (${minutes} min temu)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Oglądaj od początku (${minutes} min temu)',
'liveTv.watchLive' => 'Oglądaj na żywo', 'liveTv.watchLive' => 'Oglądaj na żywo',
@@ -3125,10 +3130,10 @@ extension on TranslationsPl {
'downloads.episodesQueued' => ({required Object count}) => '${count} odcinków w kolejce pobierania', 'downloads.episodesQueued' => ({required Object count}) => '${count} odcinków w kolejce pobierania',
'downloads.downloadDeleted' => 'Pobranie usunięte', 'downloads.downloadDeleted' => 'Pobranie usunięte',
'downloads.deleteConfirm' => ({required Object title}) => 'Usunąć "${title}" z tego urządzenia?', 'downloads.deleteConfirm' => ({required Object title}) => 'Usunąć "${title}" z tego urządzenia?',
'downloads.cancelledDownloadTitle' => 'Anulowane pobieranie',
'downloads.cancelledDownloadMessage' => 'To pobieranie zostało anulowane. Co chcesz zrobić?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => 'Anulowane pobieranie',
'downloads.cancelledDownloadMessage' => 'To pobieranie zostało anulowane. Co chcesz zrobić?',
'downloads.allEpisodesAlreadyDownloaded' => 'Wszystkie odcinki są już pobrane', 'downloads.allEpisodesAlreadyDownloaded' => 'Wszystkie odcinki są już pobrane',
'downloads.resumeDownload' => 'Wznów pobieranie', 'downloads.resumeDownload' => 'Wznów pobieranie',
'downloads.cancelledDownload' => 'Anulowane pobieranie', 'downloads.cancelledDownload' => 'Anulowane pobieranie',
@@ -3152,6 +3157,7 @@ extension on TranslationsPl {
'downloads.customAmount' => 'Własna ilość...', 'downloads.customAmount' => 'Własna ilość...',
'downloads.includeSpecials' => 'Uwzględnij odcinki specjalne', 'downloads.includeSpecials' => 'Uwzględnij odcinki specjalne',
'downloads.howManyEpisodes' => 'Ile odcinków?', 'downloads.howManyEpisodes' => 'Ile odcinków?',
'downloads.invalidEpisodeCount' => 'Wprowadź prawidłową liczbę odcinków.',
'downloads.keepSynced' => 'Synchronizuj na bieżąco', 'downloads.keepSynced' => 'Synchronizuj na bieżąco',
'downloads.downloadOnce' => 'Pobierz raz', 'downloads.downloadOnce' => 'Pobierz raz',
'downloads.keepNUnwatched' => ({required Object count}) => 'Zachowaj ${count} nieobejrzanych', 'downloads.keepNUnwatched' => ({required Object count}) => 'Zachowaj ${count} nieobejrzanych',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesPt extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Removido de Continuar Assistindo'; @override String get removedFromContinueWatching => 'Removido de Continuar Assistindo';
@override String errorLoading({required Object error}) => 'Erro: ${error}'; @override String errorLoading({required Object error}) => 'Erro: ${error}';
@override String get streamInterrupted => 'A transmissão foi interrompida. Toque em reproduzir ou avance para tentar novamente.'; @override String get streamInterrupted => 'A transmissão foi interrompida. Toque em reproduzir ou avance para tentar novamente.';
@override String get liveStreamInterrupted => 'A transmissão ao vivo foi interrompida. Toque em reproduzir para tentar novamente.';
@override String get fileInfoNotAvailable => 'Informações do arquivo não disponíveis'; @override String get fileInfoNotAvailable => 'Informações do arquivo não disponíveis';
@override String errorLoadingFileInfo({required Object error}) => 'Erro ao carregar info do arquivo: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Erro ao carregar info do arquivo: ${error}';
@override String get errorLoadingSeries => 'Erro ao carregar série'; @override String get errorLoadingSeries => 'Erro ao carregar série';
@@ -1124,6 +1125,7 @@ class _TranslationsLiveTvPt extends TranslationsLiveTvEn {
@override String get watchChannel => 'Assistir Canal'; @override String get watchChannel => 'Assistir Canal';
@override String get favorites => 'Favoritos'; @override String get favorites => 'Favoritos';
@override String get reorderFavorites => 'Reordenar favoritos'; @override String get reorderFavorites => 'Reordenar favoritos';
@override String get favoritesLoadFailed => 'Não foi possível carregar os favoritos. Verifique sua conexão e tente novamente.';
@override String get joinSession => 'Entrar na sessão em andamento'; @override String get joinSession => 'Entrar na sessão em andamento';
@override String watchFromStart({required Object minutes}) => 'Assistir do início (${minutes} min atrás)'; @override String watchFromStart({required Object minutes}) => 'Assistir do início (${minutes} min atrás)';
@override String get watchLive => 'Assistir ao vivo'; @override String get watchLive => 'Assistir ao vivo';
@@ -1383,6 +1385,7 @@ class _TranslationsDownloadsPt extends TranslationsDownloadsEn {
@override String get customAmount => 'Quantidade personalizada...'; @override String get customAmount => 'Quantidade personalizada...';
@override String get includeSpecials => 'Incluir especiais'; @override String get includeSpecials => 'Incluir especiais';
@override String get howManyEpisodes => 'Quantos episódios?'; @override String get howManyEpisodes => 'Quantos episódios?';
@override String get invalidEpisodeCount => 'Insira uma quantidade válida de episódios.';
@override String get keepSynced => 'Manter sincronizado'; @override String get keepSynced => 'Manter sincronizado';
@override String get downloadOnce => 'Baixar uma vez'; @override String get downloadOnce => 'Baixar uma vez';
@override String keepNUnwatched({required Object count}) => 'Manter ${count} não assistidos'; @override String keepNUnwatched({required Object count}) => 'Manter ${count} não assistidos';
@@ -2601,6 +2604,7 @@ extension on TranslationsPt {
'messages.removedFromContinueWatching' => 'Removido de Continuar Assistindo', 'messages.removedFromContinueWatching' => 'Removido de Continuar Assistindo',
'messages.errorLoading' => ({required Object error}) => 'Erro: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Erro: ${error}',
'messages.streamInterrupted' => 'A transmissão foi interrompida. Toque em reproduzir ou avance para tentar novamente.', 'messages.streamInterrupted' => 'A transmissão foi interrompida. Toque em reproduzir ou avance para tentar novamente.',
'messages.liveStreamInterrupted' => 'A transmissão ao vivo foi interrompida. Toque em reproduzir para tentar novamente.',
'messages.fileInfoNotAvailable' => 'Informações do arquivo não disponíveis', 'messages.fileInfoNotAvailable' => 'Informações do arquivo não disponíveis',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Erro ao carregar info do arquivo: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Erro ao carregar info do arquivo: ${error}',
'messages.errorLoadingSeries' => 'Erro ao carregar série', 'messages.errorLoadingSeries' => 'Erro ao carregar série',
@@ -2608,9 +2612,9 @@ extension on TranslationsPt {
'messages.noDescriptionAvailable' => 'Nenhuma descrição disponível', 'messages.noDescriptionAvailable' => 'Nenhuma descrição disponível',
'messages.noProfilesAvailable' => 'Nenhum perfil disponível', 'messages.noProfilesAvailable' => 'Nenhum perfil disponível',
'messages.contactAdminForProfiles' => 'Contate o administrador do servidor para adicionar perfis', 'messages.contactAdminForProfiles' => 'Contate o administrador do servidor para adicionar perfis',
'messages.unableToDetermineLibrarySection' => 'Não é possível determinar a secção da biblioteca para este item',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Não é possível determinar a secção da biblioteca para este item',
'messages.logsCleared' => 'Logs limpos', 'messages.logsCleared' => 'Logs limpos',
'messages.logsCopied' => 'Logs copiados para a área de transferência', 'messages.logsCopied' => 'Logs copiados para a área de transferência',
'messages.noLogsAvailable' => 'Nenhum log disponível', 'messages.noLogsAvailable' => 'Nenhum log disponível',
@@ -2935,6 +2939,7 @@ extension on TranslationsPt {
'liveTv.watchChannel' => 'Assistir Canal', 'liveTv.watchChannel' => 'Assistir Canal',
'liveTv.favorites' => 'Favoritos', 'liveTv.favorites' => 'Favoritos',
'liveTv.reorderFavorites' => 'Reordenar favoritos', 'liveTv.reorderFavorites' => 'Reordenar favoritos',
'liveTv.favoritesLoadFailed' => 'Não foi possível carregar os favoritos. Verifique sua conexão e tente novamente.',
'liveTv.joinSession' => 'Entrar na sessão em andamento', 'liveTv.joinSession' => 'Entrar na sessão em andamento',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Assistir do início (${minutes} min atrás)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Assistir do início (${minutes} min atrás)',
'liveTv.watchLive' => 'Assistir ao vivo', 'liveTv.watchLive' => 'Assistir ao vivo',
@@ -3121,10 +3126,10 @@ extension on TranslationsPt {
'downloads.episodesQueued' => ({required Object count}) => '${count} episódios na fila de download', 'downloads.episodesQueued' => ({required Object count}) => '${count} episódios na fila de download',
'downloads.downloadDeleted' => 'Download excluído', 'downloads.downloadDeleted' => 'Download excluído',
'downloads.deleteConfirm' => ({required Object title}) => 'Excluir "${title}" deste dispositivo?', 'downloads.deleteConfirm' => ({required Object title}) => 'Excluir "${title}" deste dispositivo?',
'downloads.cancelledDownloadTitle' => 'Download cancelado',
'downloads.cancelledDownloadMessage' => 'Este download foi cancelado. O que você deseja fazer?',
_ => null, _ => null,
} ?? switch (path) { } ?? 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', 'downloads.allEpisodesAlreadyDownloaded' => 'Todos os episódios já foram baixados',
'downloads.resumeDownload' => 'Retomar download', 'downloads.resumeDownload' => 'Retomar download',
'downloads.cancelledDownload' => 'Download cancelado', 'downloads.cancelledDownload' => 'Download cancelado',
@@ -3148,6 +3153,7 @@ extension on TranslationsPt {
'downloads.customAmount' => 'Quantidade personalizada...', 'downloads.customAmount' => 'Quantidade personalizada...',
'downloads.includeSpecials' => 'Incluir especiais', 'downloads.includeSpecials' => 'Incluir especiais',
'downloads.howManyEpisodes' => 'Quantos episódios?', 'downloads.howManyEpisodes' => 'Quantos episódios?',
'downloads.invalidEpisodeCount' => 'Insira uma quantidade válida de episódios.',
'downloads.keepSynced' => 'Manter sincronizado', 'downloads.keepSynced' => 'Manter sincronizado',
'downloads.downloadOnce' => 'Baixar uma vez', 'downloads.downloadOnce' => 'Baixar uma vez',
'downloads.keepNUnwatched' => ({required Object count}) => 'Manter ${count} não assistidos', 'downloads.keepNUnwatched' => ({required Object count}) => 'Manter ${count} não assistidos',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesRu extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Удалено из «Продолжить просмотр»'; @override String get removedFromContinueWatching => 'Удалено из «Продолжить просмотр»';
@override String errorLoading({required Object error}) => 'Ошибка: ${error}'; @override String errorLoading({required Object error}) => 'Ошибка: ${error}';
@override String get streamInterrupted => 'Поток прервался. Нажмите «Воспроизвести» или перемотайте, чтобы повторить попытку.'; @override String get streamInterrupted => 'Поток прервался. Нажмите «Воспроизвести» или перемотайте, чтобы повторить попытку.';
@override String get liveStreamInterrupted => 'Прямая трансляция прервалась. Нажмите «Воспроизвести», чтобы повторить попытку.';
@override String get fileInfoNotAvailable => 'Информация о файле недоступна'; @override String get fileInfoNotAvailable => 'Информация о файле недоступна';
@override String errorLoadingFileInfo({required Object error}) => 'Ошибка загрузки информации о файле: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Ошибка загрузки информации о файле: ${error}';
@override String get errorLoadingSeries => 'Ошибка загрузки сериала'; @override String get errorLoadingSeries => 'Ошибка загрузки сериала';
@@ -1126,6 +1127,7 @@ class _TranslationsLiveTvRu extends TranslationsLiveTvEn {
@override String get watchChannel => 'Смотреть канал'; @override String get watchChannel => 'Смотреть канал';
@override String get favorites => 'Избранное'; @override String get favorites => 'Избранное';
@override String get reorderFavorites => 'Изменить порядок избранного'; @override String get reorderFavorites => 'Изменить порядок избранного';
@override String get favoritesLoadFailed => 'Не удалось загрузить избранное. Проверьте подключение и повторите попытку.';
@override String get joinSession => 'Присоединиться к текущему сеансу'; @override String get joinSession => 'Присоединиться к текущему сеансу';
@override String watchFromStart({required Object minutes}) => 'Смотреть сначала (${minutes} мин. назад)'; @override String watchFromStart({required Object minutes}) => 'Смотреть сначала (${minutes} мин. назад)';
@override String get watchLive => 'Смотреть в прямом эфире'; @override String get watchLive => 'Смотреть в прямом эфире';
@@ -1387,6 +1389,7 @@ class _TranslationsDownloadsRu extends TranslationsDownloadsEn {
@override String get customAmount => 'Указать количество...'; @override String get customAmount => 'Указать количество...';
@override String get includeSpecials => 'Включить спецвыпуски'; @override String get includeSpecials => 'Включить спецвыпуски';
@override String get howManyEpisodes => 'Сколько эпизодов?'; @override String get howManyEpisodes => 'Сколько эпизодов?';
@override String get invalidEpisodeCount => 'Введите допустимое количество эпизодов.';
@override String get keepSynced => 'Синхронизировать'; @override String get keepSynced => 'Синхронизировать';
@override String get downloadOnce => 'Скачать один раз'; @override String get downloadOnce => 'Скачать один раз';
@override String keepNUnwatched({required Object count}) => 'Хранить ${count} непросмотренных'; @override String keepNUnwatched({required Object count}) => 'Хранить ${count} непросмотренных';
@@ -2605,6 +2608,7 @@ extension on TranslationsRu {
'messages.removedFromContinueWatching' => 'Удалено из «Продолжить просмотр»', 'messages.removedFromContinueWatching' => 'Удалено из «Продолжить просмотр»',
'messages.errorLoading' => ({required Object error}) => 'Ошибка: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Ошибка: ${error}',
'messages.streamInterrupted' => 'Поток прервался. Нажмите «Воспроизвести» или перемотайте, чтобы повторить попытку.', 'messages.streamInterrupted' => 'Поток прервался. Нажмите «Воспроизвести» или перемотайте, чтобы повторить попытку.',
'messages.liveStreamInterrupted' => 'Прямая трансляция прервалась. Нажмите «Воспроизвести», чтобы повторить попытку.',
'messages.fileInfoNotAvailable' => 'Информация о файле недоступна', 'messages.fileInfoNotAvailable' => 'Информация о файле недоступна',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Ошибка загрузки информации о файле: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Ошибка загрузки информации о файле: ${error}',
'messages.errorLoadingSeries' => 'Ошибка загрузки сериала', 'messages.errorLoadingSeries' => 'Ошибка загрузки сериала',
@@ -2612,9 +2616,9 @@ extension on TranslationsRu {
'messages.noDescriptionAvailable' => 'Описание недоступно', 'messages.noDescriptionAvailable' => 'Описание недоступно',
'messages.noProfilesAvailable' => 'Профили недоступны', 'messages.noProfilesAvailable' => 'Профили недоступны',
'messages.contactAdminForProfiles' => 'Обратитесь к администратору сервера для добавления профилей', 'messages.contactAdminForProfiles' => 'Обратитесь к администратору сервера для добавления профилей',
'messages.unableToDetermineLibrarySection' => 'Не удаётся определить раздел библиотеки для этого элемента',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Не удаётся определить раздел библиотеки для этого элемента',
'messages.logsCleared' => 'Логи очищены', 'messages.logsCleared' => 'Логи очищены',
'messages.logsCopied' => 'Логи скопированы в буфер обмена', 'messages.logsCopied' => 'Логи скопированы в буфер обмена',
'messages.noLogsAvailable' => 'Логи отсутствуют', 'messages.noLogsAvailable' => 'Логи отсутствуют',
@@ -2939,6 +2943,7 @@ extension on TranslationsRu {
'liveTv.watchChannel' => 'Смотреть канал', 'liveTv.watchChannel' => 'Смотреть канал',
'liveTv.favorites' => 'Избранное', 'liveTv.favorites' => 'Избранное',
'liveTv.reorderFavorites' => 'Изменить порядок избранного', 'liveTv.reorderFavorites' => 'Изменить порядок избранного',
'liveTv.favoritesLoadFailed' => 'Не удалось загрузить избранное. Проверьте подключение и повторите попытку.',
'liveTv.joinSession' => 'Присоединиться к текущему сеансу', 'liveTv.joinSession' => 'Присоединиться к текущему сеансу',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Смотреть сначала (${minutes} мин. назад)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Смотреть сначала (${minutes} мин. назад)',
'liveTv.watchLive' => 'Смотреть в прямом эфире', 'liveTv.watchLive' => 'Смотреть в прямом эфире',
@@ -3125,10 +3130,10 @@ extension on TranslationsRu {
'downloads.episodesQueued' => ({required Object count}) => '${count} эпизодов поставлено в очередь загрузки', 'downloads.episodesQueued' => ({required Object count}) => '${count} эпизодов поставлено в очередь загрузки',
'downloads.downloadDeleted' => 'Загрузка удалена', 'downloads.downloadDeleted' => 'Загрузка удалена',
'downloads.deleteConfirm' => ({required Object title}) => 'Удалить "${title}" с этого устройства?', 'downloads.deleteConfirm' => ({required Object title}) => 'Удалить "${title}" с этого устройства?',
'downloads.cancelledDownloadTitle' => 'Загрузка отменена',
'downloads.cancelledDownloadMessage' => 'Эта загрузка была отменена. Что вы хотите сделать?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => 'Загрузка отменена',
'downloads.cancelledDownloadMessage' => 'Эта загрузка была отменена. Что вы хотите сделать?',
'downloads.allEpisodesAlreadyDownloaded' => 'Все эпизоды уже загружены', 'downloads.allEpisodesAlreadyDownloaded' => 'Все эпизоды уже загружены',
'downloads.resumeDownload' => 'Возобновить загрузку', 'downloads.resumeDownload' => 'Возобновить загрузку',
'downloads.cancelledDownload' => 'Загрузка отменена', 'downloads.cancelledDownload' => 'Загрузка отменена',
@@ -3152,6 +3157,7 @@ extension on TranslationsRu {
'downloads.customAmount' => 'Указать количество...', 'downloads.customAmount' => 'Указать количество...',
'downloads.includeSpecials' => 'Включить спецвыпуски', 'downloads.includeSpecials' => 'Включить спецвыпуски',
'downloads.howManyEpisodes' => 'Сколько эпизодов?', 'downloads.howManyEpisodes' => 'Сколько эпизодов?',
'downloads.invalidEpisodeCount' => 'Введите допустимое количество эпизодов.',
'downloads.keepSynced' => 'Синхронизировать', 'downloads.keepSynced' => 'Синхронизировать',
'downloads.downloadOnce' => 'Скачать один раз', 'downloads.downloadOnce' => 'Скачать один раз',
'downloads.keepNUnwatched' => ({required Object count}) => 'Хранить ${count} непросмотренных', 'downloads.keepNUnwatched' => ({required Object count}) => 'Хранить ${count} непросмотренных',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesSv extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => 'Borttagen från Fortsätt titta'; @override String get removedFromContinueWatching => 'Borttagen från Fortsätt titta';
@override String errorLoading({required Object error}) => 'Fel: ${error}'; @override String errorLoading({required Object error}) => 'Fel: ${error}';
@override String get streamInterrupted => 'Uppspelningen avbröts. Tryck på play eller spola för att försöka igen.'; @override String get streamInterrupted => 'Uppspelningen avbröts. Tryck på play eller spola för att försöka igen.';
@override String get liveStreamInterrupted => 'Livestreamen avbröts. Tryck på play för att försöka igen.';
@override String get fileInfoNotAvailable => 'Filinformation inte tillgänglig'; @override String get fileInfoNotAvailable => 'Filinformation inte tillgänglig';
@override String errorLoadingFileInfo({required Object error}) => 'Fel vid laddning av filinformation: ${error}'; @override String errorLoadingFileInfo({required Object error}) => 'Fel vid laddning av filinformation: ${error}';
@override String get errorLoadingSeries => 'Fel vid laddning av serie'; @override String get errorLoadingSeries => 'Fel vid laddning av serie';
@@ -1124,6 +1125,7 @@ class _TranslationsLiveTvSv extends TranslationsLiveTvEn {
@override String get watchChannel => 'Titta på kanal'; @override String get watchChannel => 'Titta på kanal';
@override String get favorites => 'Favoriter'; @override String get favorites => 'Favoriter';
@override String get reorderFavorites => 'Ordna om favoriter'; @override String get reorderFavorites => 'Ordna om favoriter';
@override String get favoritesLoadFailed => 'Det gick inte att läsa in favoriter. Kontrollera anslutningen och försök igen.';
@override String get joinSession => 'Gå med i pågående session'; @override String get joinSession => 'Gå med i pågående session';
@override String watchFromStart({required Object minutes}) => 'Titta från början (${minutes} min sedan)'; @override String watchFromStart({required Object minutes}) => 'Titta från början (${minutes} min sedan)';
@override String get watchLive => 'Titta live'; @override String get watchLive => 'Titta live';
@@ -1383,6 +1385,7 @@ class _TranslationsDownloadsSv extends TranslationsDownloadsEn {
@override String get customAmount => 'Ange antal...'; @override String get customAmount => 'Ange antal...';
@override String get includeSpecials => 'Inkludera specialavsnitt'; @override String get includeSpecials => 'Inkludera specialavsnitt';
@override String get howManyEpisodes => 'Hur många avsnitt?'; @override String get howManyEpisodes => 'Hur många avsnitt?';
@override String get invalidEpisodeCount => 'Ange ett giltigt antal avsnitt.';
@override String get keepSynced => 'Håll synkroniserad'; @override String get keepSynced => 'Håll synkroniserad';
@override String get downloadOnce => 'Ladda ner en gång'; @override String get downloadOnce => 'Ladda ner en gång';
@override String keepNUnwatched({required Object count}) => 'Behåll ${count} osedda'; @override String keepNUnwatched({required Object count}) => 'Behåll ${count} osedda';
@@ -2601,6 +2604,7 @@ extension on TranslationsSv {
'messages.removedFromContinueWatching' => 'Borttagen från Fortsätt titta', 'messages.removedFromContinueWatching' => 'Borttagen från Fortsätt titta',
'messages.errorLoading' => ({required Object error}) => 'Fel: ${error}', 'messages.errorLoading' => ({required Object error}) => 'Fel: ${error}',
'messages.streamInterrupted' => 'Uppspelningen avbröts. Tryck på play eller spola för att försöka igen.', 'messages.streamInterrupted' => 'Uppspelningen avbröts. Tryck på play eller spola för att försöka igen.',
'messages.liveStreamInterrupted' => 'Livestreamen avbröts. Tryck på play för att försöka igen.',
'messages.fileInfoNotAvailable' => 'Filinformation inte tillgänglig', 'messages.fileInfoNotAvailable' => 'Filinformation inte tillgänglig',
'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fel vid laddning av filinformation: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => 'Fel vid laddning av filinformation: ${error}',
'messages.errorLoadingSeries' => 'Fel vid laddning av serie', 'messages.errorLoadingSeries' => 'Fel vid laddning av serie',
@@ -2608,9 +2612,9 @@ extension on TranslationsSv {
'messages.noDescriptionAvailable' => 'Ingen beskrivning tillgänglig', 'messages.noDescriptionAvailable' => 'Ingen beskrivning tillgänglig',
'messages.noProfilesAvailable' => 'Inga profiler tillgängliga', 'messages.noProfilesAvailable' => 'Inga profiler tillgängliga',
'messages.contactAdminForProfiles' => 'Kontakta din serveradministratör för att lägga till profiler', 'messages.contactAdminForProfiles' => 'Kontakta din serveradministratör för att lägga till profiler',
'messages.unableToDetermineLibrarySection' => 'Kan inte avgöra biblioteksavdelningen för detta objekt',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => 'Kan inte avgöra biblioteksavdelningen för detta objekt',
'messages.logsCleared' => 'Loggar rensade', 'messages.logsCleared' => 'Loggar rensade',
'messages.logsCopied' => 'Loggar kopierade till urklipp', 'messages.logsCopied' => 'Loggar kopierade till urklipp',
'messages.noLogsAvailable' => 'Inga loggar tillgängliga', 'messages.noLogsAvailable' => 'Inga loggar tillgängliga',
@@ -2935,6 +2939,7 @@ extension on TranslationsSv {
'liveTv.watchChannel' => 'Titta på kanal', 'liveTv.watchChannel' => 'Titta på kanal',
'liveTv.favorites' => 'Favoriter', 'liveTv.favorites' => 'Favoriter',
'liveTv.reorderFavorites' => 'Ordna om favoriter', 'liveTv.reorderFavorites' => 'Ordna om favoriter',
'liveTv.favoritesLoadFailed' => 'Det gick inte att läsa in favoriter. Kontrollera anslutningen och försök igen.',
'liveTv.joinSession' => 'Gå med i pågående session', 'liveTv.joinSession' => 'Gå med i pågående session',
'liveTv.watchFromStart' => ({required Object minutes}) => 'Titta från början (${minutes} min sedan)', 'liveTv.watchFromStart' => ({required Object minutes}) => 'Titta från början (${minutes} min sedan)',
'liveTv.watchLive' => 'Titta live', 'liveTv.watchLive' => 'Titta live',
@@ -3121,10 +3126,10 @@ extension on TranslationsSv {
'downloads.episodesQueued' => ({required Object count}) => '${count} avsnitt köade för nedladdning', 'downloads.episodesQueued' => ({required Object count}) => '${count} avsnitt köade för nedladdning',
'downloads.downloadDeleted' => 'Nedladdning borttagen', 'downloads.downloadDeleted' => 'Nedladdning borttagen',
'downloads.deleteConfirm' => ({required Object title}) => 'Ta bort "${title}" från den här enheten?', 'downloads.deleteConfirm' => ({required Object title}) => 'Ta bort "${title}" från den här enheten?',
'downloads.cancelledDownloadTitle' => 'Avbruten nedladdning',
'downloads.cancelledDownloadMessage' => 'Den här nedladdningen avbröts. Vad vill du göra?',
_ => null, _ => null,
} ?? switch (path) { } ?? 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', 'downloads.allEpisodesAlreadyDownloaded' => 'Alla avsnitt är redan nedladdade',
'downloads.resumeDownload' => 'Återuppta nedladdning', 'downloads.resumeDownload' => 'Återuppta nedladdning',
'downloads.cancelledDownload' => 'Avbruten nedladdning', 'downloads.cancelledDownload' => 'Avbruten nedladdning',
@@ -3148,6 +3153,7 @@ extension on TranslationsSv {
'downloads.customAmount' => 'Ange antal...', 'downloads.customAmount' => 'Ange antal...',
'downloads.includeSpecials' => 'Inkludera specialavsnitt', 'downloads.includeSpecials' => 'Inkludera specialavsnitt',
'downloads.howManyEpisodes' => 'Hur många avsnitt?', 'downloads.howManyEpisodes' => 'Hur många avsnitt?',
'downloads.invalidEpisodeCount' => 'Ange ett giltigt antal avsnitt.',
'downloads.keepSynced' => 'Håll synkroniserad', 'downloads.keepSynced' => 'Håll synkroniserad',
'downloads.downloadOnce' => 'Ladda ner en gång', 'downloads.downloadOnce' => 'Ladda ner en gång',
'downloads.keepNUnwatched' => ({required Object count}) => 'Behåll ${count} osedda', 'downloads.keepNUnwatched' => ({required Object count}) => 'Behåll ${count} osedda',
+9 -3
View File
@@ -696,6 +696,7 @@ class _TranslationsMessagesZh extends TranslationsMessagesEn {
@override String get removedFromContinueWatching => '已从继续观看中移除'; @override String get removedFromContinueWatching => '已从继续观看中移除';
@override String errorLoading({required Object error}) => '错误: ${error}'; @override String errorLoading({required Object error}) => '错误: ${error}';
@override String get streamInterrupted => '视频流已中断。按播放键或拖动进度条重试。'; @override String get streamInterrupted => '视频流已中断。按播放键或拖动进度条重试。';
@override String get liveStreamInterrupted => '直播流已中断。按播放键重试。';
@override String get fileInfoNotAvailable => '文件信息不可用'; @override String get fileInfoNotAvailable => '文件信息不可用';
@override String errorLoadingFileInfo({required Object error}) => '加载文件信息时出错: ${error}'; @override String errorLoadingFileInfo({required Object error}) => '加载文件信息时出错: ${error}';
@override String get errorLoadingSeries => '加载系列时出错'; @override String get errorLoadingSeries => '加载系列时出错';
@@ -1123,6 +1124,7 @@ class _TranslationsLiveTvZh extends TranslationsLiveTvEn {
@override String get watchChannel => '观看频道'; @override String get watchChannel => '观看频道';
@override String get favorites => '收藏'; @override String get favorites => '收藏';
@override String get reorderFavorites => '重新排序收藏'; @override String get reorderFavorites => '重新排序收藏';
@override String get favoritesLoadFailed => '无法加载收藏。请检查网络连接后重试。';
@override String get joinSession => '加入正在进行的会话'; @override String get joinSession => '加入正在进行的会话';
@override String watchFromStart({required Object minutes}) => '从头观看(${minutes}分钟前开始)'; @override String watchFromStart({required Object minutes}) => '从头观看(${minutes}分钟前开始)';
@override String get watchLive => '观看直播'; @override String get watchLive => '观看直播';
@@ -1381,6 +1383,7 @@ class _TranslationsDownloadsZh extends TranslationsDownloadsEn {
@override String get customAmount => '自定义数量...'; @override String get customAmount => '自定义数量...';
@override String get includeSpecials => '包含特别篇'; @override String get includeSpecials => '包含特别篇';
@override String get howManyEpisodes => '下载几集?'; @override String get howManyEpisodes => '下载几集?';
@override String get invalidEpisodeCount => '请输入有效的集数。';
@override String get keepSynced => '保持同步'; @override String get keepSynced => '保持同步';
@override String get downloadOnce => '下载一次'; @override String get downloadOnce => '下载一次';
@override String keepNUnwatched({required Object count}) => '保留${count}个未观看'; @override String keepNUnwatched({required Object count}) => '保留${count}个未观看';
@@ -2599,6 +2602,7 @@ extension on TranslationsZh {
'messages.removedFromContinueWatching' => '已从继续观看中移除', 'messages.removedFromContinueWatching' => '已从继续观看中移除',
'messages.errorLoading' => ({required Object error}) => '错误: ${error}', 'messages.errorLoading' => ({required Object error}) => '错误: ${error}',
'messages.streamInterrupted' => '视频流已中断。按播放键或拖动进度条重试。', 'messages.streamInterrupted' => '视频流已中断。按播放键或拖动进度条重试。',
'messages.liveStreamInterrupted' => '直播流已中断。按播放键重试。',
'messages.fileInfoNotAvailable' => '文件信息不可用', 'messages.fileInfoNotAvailable' => '文件信息不可用',
'messages.errorLoadingFileInfo' => ({required Object error}) => '加载文件信息时出错: ${error}', 'messages.errorLoadingFileInfo' => ({required Object error}) => '加载文件信息时出错: ${error}',
'messages.errorLoadingSeries' => '加载系列时出错', 'messages.errorLoadingSeries' => '加载系列时出错',
@@ -2606,9 +2610,9 @@ extension on TranslationsZh {
'messages.noDescriptionAvailable' => '暂无描述', 'messages.noDescriptionAvailable' => '暂无描述',
'messages.noProfilesAvailable' => '没有可用的用户', 'messages.noProfilesAvailable' => '没有可用的用户',
'messages.contactAdminForProfiles' => '请联系服务器管理员添加用户配置', 'messages.contactAdminForProfiles' => '请联系服务器管理员添加用户配置',
'messages.unableToDetermineLibrarySection' => '无法确定此项目的库分区',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'messages.unableToDetermineLibrarySection' => '无法确定此项目的库分区',
'messages.logsCleared' => '日志已清除', 'messages.logsCleared' => '日志已清除',
'messages.logsCopied' => '日志已复制到剪贴板', 'messages.logsCopied' => '日志已复制到剪贴板',
'messages.noLogsAvailable' => '没有可用日志', 'messages.noLogsAvailable' => '没有可用日志',
@@ -2933,6 +2937,7 @@ extension on TranslationsZh {
'liveTv.watchChannel' => '观看频道', 'liveTv.watchChannel' => '观看频道',
'liveTv.favorites' => '收藏', 'liveTv.favorites' => '收藏',
'liveTv.reorderFavorites' => '重新排序收藏', 'liveTv.reorderFavorites' => '重新排序收藏',
'liveTv.favoritesLoadFailed' => '无法加载收藏。请检查网络连接后重试。',
'liveTv.joinSession' => '加入正在进行的会话', 'liveTv.joinSession' => '加入正在进行的会话',
'liveTv.watchFromStart' => ({required Object minutes}) => '从头观看(${minutes}分钟前开始)', 'liveTv.watchFromStart' => ({required Object minutes}) => '从头观看(${minutes}分钟前开始)',
'liveTv.watchLive' => '观看直播', 'liveTv.watchLive' => '观看直播',
@@ -3119,10 +3124,10 @@ extension on TranslationsZh {
'downloads.episodesQueued' => ({required Object count}) => '${count} 集已加入下载队列', 'downloads.episodesQueued' => ({required Object count}) => '${count} 集已加入下载队列',
'downloads.downloadDeleted' => '下载已删除', 'downloads.downloadDeleted' => '下载已删除',
'downloads.deleteConfirm' => ({required Object title}) => '要从此设备删除“${title}”吗?', 'downloads.deleteConfirm' => ({required Object title}) => '要从此设备删除“${title}”吗?',
'downloads.cancelledDownloadTitle' => '已取消的下载',
'downloads.cancelledDownloadMessage' => '此下载已取消。你想怎么做?',
_ => null, _ => null,
} ?? switch (path) { } ?? switch (path) {
'downloads.cancelledDownloadTitle' => '已取消的下载',
'downloads.cancelledDownloadMessage' => '此下载已取消。你想怎么做?',
'downloads.allEpisodesAlreadyDownloaded' => '所有剧集均已下载', 'downloads.allEpisodesAlreadyDownloaded' => '所有剧集均已下载',
'downloads.resumeDownload' => '继续下载', 'downloads.resumeDownload' => '继续下载',
'downloads.cancelledDownload' => '已取消的下载', 'downloads.cancelledDownload' => '已取消的下载',
@@ -3146,6 +3151,7 @@ extension on TranslationsZh {
'downloads.customAmount' => '自定义数量...', 'downloads.customAmount' => '自定义数量...',
'downloads.includeSpecials' => '包含特别篇', 'downloads.includeSpecials' => '包含特别篇',
'downloads.howManyEpisodes' => '下载几集?', 'downloads.howManyEpisodes' => '下载几集?',
'downloads.invalidEpisodeCount' => '请输入有效的集数。',
'downloads.keepSynced' => '保持同步', 'downloads.keepSynced' => '保持同步',
'downloads.downloadOnce' => '下载一次', 'downloads.downloadOnce' => '下载一次',
'downloads.keepNUnwatched' => ({required Object count}) => '保留${count}个未观看', 'downloads.keepNUnwatched' => ({required Object count}) => '保留${count}个未观看',
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "Borttagen från Fortsätt titta", "removedFromContinueWatching": "Borttagen från Fortsätt titta",
"errorLoading": "Fel: ${error}", "errorLoading": "Fel: ${error}",
"streamInterrupted": "Uppspelningen avbröts. Tryck på play eller spola för att försöka igen.", "streamInterrupted": "Uppspelningen avbröts. Tryck på play eller spola för att försöka igen.",
"liveStreamInterrupted": "Livestreamen avbröts. Tryck på play för att försöka igen.",
"fileInfoNotAvailable": "Filinformation inte tillgänglig", "fileInfoNotAvailable": "Filinformation inte tillgänglig",
"errorLoadingFileInfo": "Fel vid laddning av filinformation: ${error}", "errorLoadingFileInfo": "Fel vid laddning av filinformation: ${error}",
"errorLoadingSeries": "Fel vid laddning av serie", "errorLoadingSeries": "Fel vid laddning av serie",
@@ -916,6 +917,7 @@
"watchChannel": "Titta på kanal", "watchChannel": "Titta på kanal",
"favorites": "Favoriter", "favorites": "Favoriter",
"reorderFavorites": "Ordna om favoriter", "reorderFavorites": "Ordna om favoriter",
"favoritesLoadFailed": "Det gick inte att läsa in favoriter. Kontrollera anslutningen och försök igen.",
"joinSession": "Gå med i pågående session", "joinSession": "Gå med i pågående session",
"watchFromStart": "Titta från början (${minutes} min sedan)", "watchFromStart": "Titta från början (${minutes} min sedan)",
"watchLive": "Titta live", "watchLive": "Titta live",
@@ -1140,6 +1142,7 @@
"customAmount": "Ange antal...", "customAmount": "Ange antal...",
"includeSpecials": "Inkludera specialavsnitt", "includeSpecials": "Inkludera specialavsnitt",
"howManyEpisodes": "Hur många avsnitt?", "howManyEpisodes": "Hur många avsnitt?",
"invalidEpisodeCount": "Ange ett giltigt antal avsnitt.",
"keepSynced": "Håll synkroniserad", "keepSynced": "Håll synkroniserad",
"downloadOnce": "Ladda ner en gång", "downloadOnce": "Ladda ner en gång",
"keepNUnwatched": "Behåll ${count} osedda", "keepNUnwatched": "Behåll ${count} osedda",
+3
View File
@@ -537,6 +537,7 @@
"removedFromContinueWatching": "已从继续观看中移除", "removedFromContinueWatching": "已从继续观看中移除",
"errorLoading": "错误: ${error}", "errorLoading": "错误: ${error}",
"streamInterrupted": "视频流已中断。按播放键或拖动进度条重试。", "streamInterrupted": "视频流已中断。按播放键或拖动进度条重试。",
"liveStreamInterrupted": "直播流已中断。按播放键重试。",
"fileInfoNotAvailable": "文件信息不可用", "fileInfoNotAvailable": "文件信息不可用",
"errorLoadingFileInfo": "加载文件信息时出错: ${error}", "errorLoadingFileInfo": "加载文件信息时出错: ${error}",
"errorLoadingSeries": "加载系列时出错", "errorLoadingSeries": "加载系列时出错",
@@ -915,6 +916,7 @@
"watchChannel": "观看频道", "watchChannel": "观看频道",
"favorites": "收藏", "favorites": "收藏",
"reorderFavorites": "重新排序收藏", "reorderFavorites": "重新排序收藏",
"favoritesLoadFailed": "无法加载收藏。请检查网络连接后重试。",
"joinSession": "加入正在进行的会话", "joinSession": "加入正在进行的会话",
"watchFromStart": "从头观看(${minutes}分钟前开始)", "watchFromStart": "从头观看(${minutes}分钟前开始)",
"watchLive": "观看直播", "watchLive": "观看直播",
@@ -1138,6 +1140,7 @@
"customAmount": "自定义数量...", "customAmount": "自定义数量...",
"includeSpecials": "包含特别篇", "includeSpecials": "包含特别篇",
"howManyEpisodes": "下载几集?", "howManyEpisodes": "下载几集?",
"invalidEpisodeCount": "请输入有效的集数。",
"keepSynced": "保持同步", "keepSynced": "保持同步",
"downloadOnce": "下载一次", "downloadOnce": "下载一次",
"keepNUnwatched": "保留${count}个未观看", "keepNUnwatched": "保留${count}个未观看",
+9 -1
View File
@@ -459,10 +459,18 @@ class PlayerNative extends PlayerBase {
_armedNextUri = null; _armedNextUri = null;
_armedNextFd = null; _armedNextFd = null;
appLogger.d('MPV-audio: armed entry advanced → playlist-remove 0, ${_uriTail(uri ?? '')}'); appLogger.d('MPV-audio: armed entry advanced → playlist-remove 0, ${_uriTail(uri ?? '')}');
unawaited(command(['playlist-remove', '0'])); unawaited(_removeSpentPlaylistEntry());
if (uri != null) trackTransitionController.add(uri); if (uri != null) trackTransitionController.add(uri);
} }
Future<void> _removeSpentPlaylistEntry() async {
try {
await command(['playlist-remove', '0']);
} catch (error, stackTrace) {
appLogger.w('MPV-audio: failed to remove spent playlist entry', error: error, stackTrace: stackTrace);
}
}
@override @override
void handlePropertyChange(String name, dynamic value) { void handlePropertyChange(String name, dynamic value) {
if (audioOnly && name == 'playlist-pos') { if (audioOnly && name == 'playlist-pos') {
+8 -1
View File
@@ -172,7 +172,14 @@ class ActiveProfileBinder {
// notifications. They don't mean the active profile changed, and a // notifications. They don't mean the active profile changed, and a
// failed bind intentionally leaves `_lastBoundProfileId` unset so the // failed bind intentionally leaves `_lastBoundProfileId` unset so the
// same profile can be retried later. // same profile can be retried later.
if (id == _bindingProfileId) return; if (id == _bindingProfileId) {
// The active id can briefly move away and back while this pass is
// awaiting multiple connection binds. Any component that observed the
// intermediate id may already have returned an empty stale result, so
// the current pass cannot be committed as the final same-id bind.
if (_pendingRebind) _pendingSameIdRebind = true;
return;
}
// A rebind is already in flight — flag a follow-up so the loop in // A rebind is already in flight — flag a follow-up so the loop in
// [_rebind] picks up the new active id once the current pass settles. // [_rebind] picks up the new active id once the current pass settles.
// Otherwise the switch is silently dropped (the early-return on // Otherwise the switch is silently dropped (the early-return on
+14 -1
View File
@@ -42,7 +42,13 @@ class DiscoverProvider extends ChangeNotifier with DisposableChangeNotifierMixin
static const int continueWatchingPreviewLimit = 20; static const int continueWatchingPreviewLimit = 20;
static const int _continueWatchingProbeLimit = continueWatchingPreviewLimit + 1; static const int _continueWatchingProbeLimit = continueWatchingPreviewLimit + 1;
DiscoverProvider(this._multiServer, this._hiddenLibraries, this._libraries, {required this.isProfileBinding}) { DiscoverProvider(
this._multiServer,
this._hiddenLibraries,
this._libraries, {
required this.isProfileBinding,
Future<void> Function(List<MediaItem>)? syncSystemShelf,
}) : _syncSystemShelfOverride = syncSystemShelf {
_loadCoordinator = CoalescedLoadCoordinator<String>(onFull: _loadOnce, onDelta: _loadDeltaOnce); _loadCoordinator = CoalescedLoadCoordinator<String>(onFull: _loadOnce, onDelta: _loadDeltaOnce);
// Late server connects (reconnect after outage, slow wave) refresh // Late server connects (reconnect after outage, slow wave) refresh
// discover the same way they refresh libraries. Removed in [dispose] so a // discover the same way they refresh libraries. Removed in [dispose] so a
@@ -79,6 +85,7 @@ class DiscoverProvider extends ChangeNotifier with DisposableChangeNotifierMixin
/// instead of flashing the empty placeholder (main_screen primes another /// instead of flashing the empty placeholder (main_screen primes another
/// load once binding settles). /// load once binding settles).
final bool Function() isProfileBinding; final bool Function() isProfileBinding;
final Future<void> Function(List<MediaItem>)? _syncSystemShelfOverride;
StreamSubscription<WatchStateEvent>? _watchStateSubscription; StreamSubscription<WatchStateEvent>? _watchStateSubscription;
StreamSubscription<DeletionEvent>? _deletionSubscription; StreamSubscription<DeletionEvent>? _deletionSubscription;
@@ -499,6 +506,7 @@ class DiscoverProvider extends ChangeNotifier with DisposableChangeNotifierMixin
if (viewOffset != null && index != -1 && _onDeck[index].viewOffsetMs != viewOffset) { if (viewOffset != null && index != -1 && _onDeck[index].viewOffsetMs != viewOffset) {
_onDeck = List.of(_onDeck)..[index] = _onDeck[index].copyWith(viewOffsetMs: viewOffset); _onDeck = List.of(_onDeck)..[index] = _onDeck[index].copyWith(viewOffsetMs: viewOffset);
safeNotifyListeners(); safeNotifyListeners();
unawaited(_syncSystemShelf(_onDeck));
} }
return; return;
} }
@@ -629,6 +637,11 @@ class DiscoverProvider extends ChangeNotifier with DisposableChangeNotifierMixin
if (isDisposed) return; if (isDisposed) return;
try { try {
final syncOverride = _syncSystemShelfOverride;
if (syncOverride != null) {
await syncOverride(onDeck);
continue;
}
final settings = await SettingsService.getInstance(); final settings = await SettingsService.getInstance();
if (isDisposed) return; if (isDisposed) return;
final syncableOnDeck = onDeck final syncableOnDeck = onDeck
+1 -1
View File
@@ -165,7 +165,7 @@ class _DownloadMetadataStore extends ChangeNotifier {
}) async { }) async {
final activeScope = _downloadManager.activeClientScopeIdForServer(ServerId(serverId)); final activeScope = _downloadManager.activeClientScopeIdForServer(ServerId(serverId));
if (activeScope != null && activeScope.isNotEmpty) return activeScope; if (activeScope != null && activeScope.isNotEmpty) return activeScope;
for (final globalKey in downloads.keys) { for (final globalKey in downloads.keys.toList(growable: false)) {
if (!ownsDownloadKey(globalKey)) continue; if (!ownsDownloadKey(globalKey)) continue;
final parsed = parseGlobalKey(globalKey); final parsed = parseGlobalKey(globalKey);
if (parsed?.serverId != serverId) continue; if (parsed?.serverId != serverId) continue;
+13 -10
View File
@@ -292,6 +292,9 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin
} }
} }
@visibleForTesting
Future<void> debugHydrateOfflineWatchOverlay() => _applyOfflineWatchOverlay();
/// Load all persisted downloads and metadata from the database/cache /// Load all persisted downloads and metadata from the database/cache
Future<void> _loadPersistedDownloads() async { Future<void> _loadPersistedDownloads() async {
try { try {
@@ -901,24 +904,23 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin
final globalKey = metadata.globalKey; final globalKey = metadata.globalKey;
final config = versionConfig ?? DownloadVersionConfig(); final config = versionConfig ?? DownloadVersionConfig();
if (!_queueing.add(globalKey)) return 0;
// Check if downloads are blocked on cellular safeNotifyListeners();
if (await DownloadManagerService.shouldBlockDownloadOnCellular()) {
throw CellularDownloadBlockedException();
}
try { try {
// Mark as queueing to show loading state in UI // Claim the operation before the first await so a second tap cannot
_queueing.add(globalKey); // launch a duplicate container expansion.
safeNotifyListeners(); if (await DownloadManagerService.shouldBlockDownloadOnCellular()) {
throw CellularDownloadBlockedException();
}
if (metadata.isMovie || metadata.isEpisode || metadata.kind == MediaKind.track) { if (metadata.isMovie || metadata.isEpisode || metadata.kind == MediaKind.track) {
final queued = await _queueSingleDownload(metadata, client, mediaIndex: config.mediaIndex); final queued = await _queueSingleDownload(metadata, client, mediaIndex: config.mediaIndex);
return queued ? 1 : 0; return queued ? 1 : 0;
} else if (metadata.kind == MediaKind.album || metadata.kind == MediaKind.artist) { } else if (metadata.kind == MediaKind.album || metadata.kind == MediaKind.artist) {
return _withStashedMetadata(metadata, () => _queueMusicContainerDownload(metadata, client)); return await _withStashedMetadata(metadata, () => _queueMusicContainerDownload(metadata, client));
} else if (metadata.isShow || metadata.isSeason) { } else if (metadata.isShow || metadata.isSeason) {
return _withStashedMetadata( return await _withStashedMetadata(
metadata, metadata,
() => _expandAndQueue( () => _expandAndQueue(
container: metadata, container: metadata,
@@ -1360,6 +1362,7 @@ class DownloadProvider extends ChangeNotifier with DisposableChangeNotifierMixin
try { try {
for (final entry in descendants) { for (final entry in descendants) {
await _deleteDownload(entry.key, notify: false); await _deleteDownload(entry.key, notify: false);
DeletionNotifier().notifyDeletedItem(item: entry.value, isDownloadOnly: true);
} }
} finally { } finally {
_batchDeletionDepth--; _batchDeletionDepth--;
@@ -0,0 +1,11 @@
import 'package:flutter/widgets.dart';
enum LiveTvRefreshLifecycleTransition { pause, resume, ignore }
LiveTvRefreshLifecycleTransition liveTvRefreshTransition(AppLifecycleState state) {
return switch (state) {
AppLifecycleState.paused || AppLifecycleState.hidden => LiveTvRefreshLifecycleTransition.pause,
AppLifecycleState.resumed => LiveTvRefreshLifecycleTransition.resume,
AppLifecycleState.inactive || AppLifecycleState.detached => LiveTvRefreshLifecycleTransition.ignore,
};
}
+68 -39
View File
@@ -21,6 +21,7 @@ import '../../widgets/settings_builder.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
import '../../utils/desktop_window_padding.dart'; import '../../utils/desktop_window_padding.dart';
import '../../utils/platform_detector.dart'; import '../../utils/platform_detector.dart';
import '../../utils/serial_future_queue.dart';
import '../../utils/snackbar_helper.dart'; import '../../utils/snackbar_helper.dart';
import '../../widgets/app_icon.dart'; import '../../widgets/app_icon.dart';
import '../../widgets/focusable_tab_chip.dart'; import '../../widgets/focusable_tab_chip.dart';
@@ -74,7 +75,9 @@ class _LiveTvScreenState extends State<LiveTvScreen>
final Map<String, FavoriteChannelPersistenceMode> _favoriteModeByStore = {}; final Map<String, FavoriteChannelPersistenceMode> _favoriteModeByStore = {};
Future<void>? _channelsLoadFuture; Future<void>? _channelsLoadFuture;
int _favoritesLoadGeneration = 0; int _favoritesLoadGeneration = 0;
int _favoritesMutationGeneration = 0; Future<void>? _favoritesLoadFuture;
final SerialFutureQueue _favoritesMutationQueue = SerialFutureQueue();
bool _favoritesLoaded = false;
List<LiveTvChannel> get _filteredChannels => filterLiveTvChannelsForFavorites( List<LiveTvChannel> get _filteredChannels => filterLiveTvChannelsForFavorites(
channels: _channels, channels: _channels,
@@ -402,7 +405,15 @@ class _LiveTvScreenState extends State<LiveTvScreen>
_refreshVisibleTabs(multiServer); _refreshVisibleTabs(multiServer);
// Load favorites by backend store: Plex is cloud/account-scoped, Jellyfin per server. // Load favorites by backend store: Plex is cloud/account-scoped, Jellyfin per server.
unawaited(_loadFavorites(multiServer)); final favoritesLoad = _loadFavorites(multiServer);
_favoritesLoadFuture = favoritesLoad;
unawaited(
favoritesLoad.whenComplete(() {
if (identical(_favoritesLoadFuture, favoritesLoad)) {
_favoritesLoadFuture = null;
}
}),
);
if (allChannels.isNotEmpty && PlatformDetector.shouldUseSideNavigation(context)) { if (allChannels.isNotEmpty && PlatformDetector.shouldUseSideNavigation(context)) {
WidgetsBinding.instance.addPostFrameCallback((_) { WidgetsBinding.instance.addPostFrameCallback((_) {
@@ -422,7 +433,7 @@ class _LiveTvScreenState extends State<LiveTvScreen>
Future<void> _loadFavorites(MultiServerProvider multiServer) async { Future<void> _loadFavorites(MultiServerProvider multiServer) async {
final loadGeneration = ++_favoritesLoadGeneration; final loadGeneration = ++_favoritesLoadGeneration;
final mutationGeneration = _favoritesMutationGeneration; _favoritesLoaded = false;
try { try {
final sourceByLiveServer = Map<String, String>.of(_favoriteSourceByLiveServer); final sourceByLiveServer = Map<String, String>.of(_favoriteSourceByLiveServer);
final storeByLiveServer = Map<String, String>.of(_favoriteStoreByLiveServer); final storeByLiveServer = Map<String, String>.of(_favoriteStoreByLiveServer);
@@ -450,11 +461,7 @@ class _LiveTvScreenState extends State<LiveTvScreen>
} }
} }
if (!mounted || if (!mounted || loadGeneration != _favoritesLoadGeneration) return;
loadGeneration != _favoritesLoadGeneration ||
mutationGeneration != _favoritesMutationGeneration) {
return;
}
setState(() { setState(() {
_favoriteSourceByLiveServer _favoriteSourceByLiveServer
..clear() ..clear()
@@ -471,6 +478,7 @@ class _LiveTvScreenState extends State<LiveTvScreen>
_favoriteChannels = merged; _favoriteChannels = merged;
_refreshFavoriteKeys(); _refreshFavoriteKeys();
}); });
_favoritesLoaded = true;
appLogger.d('Live TV: loaded ${merged.length} favorite channels'); appLogger.d('Live TV: loaded ${merged.length} favorite channels');
} catch (e) { } catch (e) {
appLogger.e('Failed to load favorite channels', error: e); appLogger.e('Failed to load favorite channels', error: e);
@@ -484,23 +492,42 @@ class _LiveTvScreenState extends State<LiveTvScreen>
} }
void _toggleFavorite(LiveTvChannel channel) { void _toggleFavorite(LiveTvChannel channel) {
++_favoritesMutationGeneration; _enqueueFavoriteMutation(() {
final source = _sourceForChannel(channel); final source = _sourceForChannel(channel);
final favoriteKey = favoriteChannelKey(source, channel.key); final favoriteKey = favoriteChannelKey(source, channel.key);
final scopeKey = liveTvChannelScopeKey(channel); final scopeKey = liveTvChannelScopeKey(channel);
final storeKey = channel.favoriteStoreKey ?? _favoriteStoreByChannel[scopeKey]; final storeKey = channel.favoriteStoreKey ?? _favoriteStoreByChannel[scopeKey];
if (storeKey != null) _favoriteStoreBySource[source] = storeKey; if (storeKey != null) _favoriteStoreBySource[source] = storeKey;
setState(() { setState(() {
if (_favoriteKeys.contains(favoriteKey)) { if (_favoriteKeys.contains(favoriteKey)) {
_favoriteChannels = _favoriteChannels.where((f) => f.id != channel.key || f.source != source).toList(); _favoriteChannels = _favoriteChannels.where((f) => f.id != channel.key || f.source != source).toList();
} else { } else {
_favoriteChannels = [..._favoriteChannels, FavoriteChannel.fromLiveTvChannel(channel, source)]; _favoriteChannels = [..._favoriteChannels, FavoriteChannel.fromLiveTvChannel(channel, source)];
} }
_refreshFavoriteKeys(); _refreshFavoriteKeys();
});
}); });
}
_persistFavorites(); void _enqueueFavoriteMutation(VoidCallback mutation) {
final pendingLoad = _favoritesLoadFuture;
unawaited(
_favoritesMutationQueue
.run(() async {
if (pendingLoad != null) await pendingLoad;
if (!mounted) return;
if (!_favoritesLoaded) {
showErrorSnackBar(context, t.liveTv.favoritesLoadFailed);
return;
}
mutation();
await _persistFavorites();
})
.catchError((Object error, StackTrace stackTrace) {
appLogger.e('Failed to mutate favorite channels', error: error, stackTrace: stackTrace);
}),
);
} }
void _showReorderFavorites() { void _showReorderFavorites() {
@@ -512,34 +539,35 @@ class _LiveTvScreenState extends State<LiveTvScreen>
favorites: List.from(_favoriteChannels), favorites: List.from(_favoriteChannels),
channelMap: channelMap, channelMap: channelMap,
onReorder: (reordered) { onReorder: (reordered) {
++_favoritesMutationGeneration; _enqueueFavoriteMutation(() {
setState(() { setState(() {
_favoriteChannels = reordered; _favoriteChannels = reordered;
_refreshFavoriteKeys(); _refreshFavoriteKeys();
});
}); });
_persistFavorites();
}, },
onRemove: (removed) { onRemove: (removed) {
++_favoritesMutationGeneration; _enqueueFavoriteMutation(() {
setState(() { setState(() {
_favoriteChannels = _favoriteChannels.where((f) => f.stableKey != removed.stableKey).toList(); _favoriteChannels = _favoriteChannels.where((f) => f.stableKey != removed.stableKey).toList();
_refreshFavoriteKeys(); _refreshFavoriteKeys();
});
}); });
_persistFavorites();
}, },
), ),
); );
} }
void _persistFavorites() { Future<void> _persistFavorites() async {
final multiServer = context.read<MultiServerProvider>(); final multiServer = context.read<MultiServerProvider>();
final byStore = <String, List<FavoriteChannel>>{}; final byStore = <String, List<FavoriteChannel>>{};
for (final f in _favoriteChannels) { for (final favorite in _favoriteChannels) {
final storeKey = _favoriteStoreBySource[f.source]; final storeKey = _favoriteStoreBySource[favorite.source];
if (storeKey == null) continue; if (storeKey == null) continue;
byStore.putIfAbsent(storeKey, () => []).add(f); byStore.putIfAbsent(storeKey, () => []).add(favorite);
} }
final writtenStores = <String>{}; final writtenStores = <String>{};
final writes = <Future<void>>[];
for (final serverInfo in multiServer.liveTvServers) { for (final serverInfo in multiServer.liveTvServers) {
final client = multiServer.getClientForServer(ServerId(serverInfo.serverId)); final client = multiServer.getClientForServer(ServerId(serverInfo.serverId));
if (client == null) continue; if (client == null) continue;
@@ -548,14 +576,15 @@ class _LiveTvScreenState extends State<LiveTvScreen>
if (storeKey == null || !writtenStores.add(storeKey)) continue; if (storeKey == null || !writtenStores.add(storeKey)) continue;
final mode = _favoriteModeByStore[storeKey] ?? client.liveTv.favoritePersistenceMode; final mode = _favoriteModeByStore[storeKey] ?? client.liveTv.favoritePersistenceMode;
final source = _favoriteSourceByLiveServer[liveServerKey]; final source = _favoriteSourceByLiveServer[liveServerKey];
if (source == null) continue; // not yet resolved — skip; next toggle will catch up if (source == null) continue;
final channels = switch (mode) { final channels = switch (mode) {
FavoriteChannelPersistenceMode.sharedFullList => byStore[storeKey] ?? const <FavoriteChannel>[], FavoriteChannelPersistenceMode.sharedFullList => byStore[storeKey] ?? const <FavoriteChannel>[],
FavoriteChannelPersistenceMode.serverSlice => FavoriteChannelPersistenceMode.serverSlice =>
(byStore[storeKey] ?? const <FavoriteChannel>[]).where((f) => f.source == source).toList(), (byStore[storeKey] ?? const <FavoriteChannel>[]).where((favorite) => favorite.source == source).toList(),
}; };
unawaited(client.liveTv.setFavoriteChannels(channels)); writes.add(client.liveTv.setFavoriteChannels(channels));
} }
await Future.wait(writes);
} }
void _focusCurrentTab() { void _focusCurrentTab() {
+5 -6
View File
@@ -20,6 +20,7 @@ import '../../../providers/multi_server_provider.dart';
import '../../../media/media_server_client.dart'; import '../../../media/media_server_client.dart';
import '../../../theme/mono_tokens.dart'; import '../../../theme/mono_tokens.dart';
import '../../../utils/app_logger.dart'; import '../../../utils/app_logger.dart';
import '../live_tv_refresh_lifecycle.dart';
import '../../../utils/formatters.dart'; import '../../../utils/formatters.dart';
import '../../../utils/live_tv_grouping.dart'; import '../../../utils/live_tv_grouping.dart';
import '../../../utils/live_tv_matching.dart'; import '../../../utils/live_tv_matching.dart';
@@ -195,14 +196,12 @@ class GuideTabState extends State<GuideTab> with MountedSetStateMixin, WidgetsBi
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) { switch (liveTvRefreshTransition(state)) {
case AppLifecycleState.paused: case LiveTvRefreshLifecycleTransition.pause:
case AppLifecycleState.hidden:
pauseRefresh(); pauseRefresh();
case AppLifecycleState.resumed: case LiveTvRefreshLifecycleTransition.resume:
if (_isGuideVisible) resumeRefresh(); if (_isGuideVisible) resumeRefresh();
case AppLifecycleState.inactive: case LiveTvRefreshLifecycleTransition.ignore:
case AppLifecycleState.detached:
break; break;
} }
} }
+15 -6
View File
@@ -20,6 +20,7 @@ import '../../../utils/formatters.dart';
import '../../../widgets/app_icon.dart'; import '../../../widgets/app_icon.dart';
import '../../../widgets/overlay_sheet.dart'; import '../../../widgets/overlay_sheet.dart';
import '../../../widgets/settings_section.dart'; import '../../../widgets/settings_section.dart';
import '../live_tv_refresh_lifecycle.dart';
import '../livetv_recording_actions.dart'; import '../livetv_recording_actions.dart';
import '../livetv_styles.dart'; import '../livetv_styles.dart';
@@ -67,7 +68,7 @@ class RecordingsTabState extends State<RecordingsTab> with WidgetsBindingObserve
bool _pendingFocus = false; bool _pendingFocus = false;
bool _refreshRequested = true; bool _refreshRequested = true;
bool _tickerEnabled = false; bool _tickerEnabled = false;
bool _appResumed = true; bool _appRefreshActive = true;
final _firstTileFocusNode = FocusNode(debugLabel: 'recordings_tab_first_tile'); final _firstTileFocusNode = FocusNode(debugLabel: 'recordings_tab_first_tile');
@override @override
@@ -88,10 +89,18 @@ class RecordingsTabState extends State<RecordingsTab> with WidgetsBindingObserve
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
final resumed = state == AppLifecycleState.resumed; switch (liveTvRefreshTransition(state)) {
if (resumed == _appResumed) return; case LiveTvRefreshLifecycleTransition.pause:
_appResumed = resumed; if (!_appRefreshActive) return;
_syncRefreshTimer(); _appRefreshActive = false;
_syncRefreshTimer();
case LiveTvRefreshLifecycleTransition.resume:
if (_appRefreshActive) return;
_appRefreshActive = true;
_syncRefreshTimer();
case LiveTvRefreshLifecycleTransition.ignore:
break;
}
} }
@override @override
@@ -126,7 +135,7 @@ class RecordingsTabState extends State<RecordingsTab> with WidgetsBindingObserve
void _syncRefreshTimer({bool reload = false}) { void _syncRefreshTimer({bool reload = false}) {
_refreshTimer?.cancel(); _refreshTimer?.cancel();
_refreshTimer = null; _refreshTimer = null;
if (!_refreshRequested || !_tickerEnabled || !_appResumed || !mounted) return; if (!_refreshRequested || !_tickerEnabled || !_appRefreshActive || !mounted) return;
_refreshTimer = Timer.periodic(const Duration(seconds: 30), (_) => _load()); _refreshTimer = Timer.periodic(const Duration(seconds: 30), (_) => _load());
if (reload) unawaited(_load()); if (reload) unawaited(_load());
} }
+15 -6
View File
@@ -20,6 +20,7 @@ import '../../../widgets/hub_section.dart';
import '../../../widgets/overlay_sheet.dart'; import '../../../widgets/overlay_sheet.dart';
import '../live_tv_actions_mixin.dart'; import '../live_tv_actions_mixin.dart';
import '../live_tv_show_schedule_screen.dart'; import '../live_tv_show_schedule_screen.dart';
import '../live_tv_refresh_lifecycle.dart';
class WhatsOnTab extends StatefulWidget { class WhatsOnTab extends StatefulWidget {
final List<LiveTvChannel> channels; final List<LiveTvChannel> channels;
@@ -41,7 +42,7 @@ class WhatsOnTabState extends State<WhatsOnTab>
List<GlobalKey<HubSectionState>> _hubKeys = []; List<GlobalKey<HubSectionState>> _hubKeys = [];
bool _refreshRequested = true; bool _refreshRequested = true;
bool _tickerEnabled = false; bool _tickerEnabled = false;
bool _appResumed = true; bool _appRefreshActive = true;
@override @override
List<LiveTvChannel> get liveTvChannels => widget.channels; List<LiveTvChannel> get liveTvChannels => widget.channels;
@@ -64,10 +65,18 @@ class WhatsOnTabState extends State<WhatsOnTab>
@override @override
void didChangeAppLifecycleState(AppLifecycleState state) { void didChangeAppLifecycleState(AppLifecycleState state) {
final resumed = state == AppLifecycleState.resumed; switch (liveTvRefreshTransition(state)) {
if (resumed == _appResumed) return; case LiveTvRefreshLifecycleTransition.pause:
_appResumed = resumed; if (!_appRefreshActive) return;
_syncRefreshTimer(); _appRefreshActive = false;
_syncRefreshTimer();
case LiveTvRefreshLifecycleTransition.resume:
if (_appRefreshActive) return;
_appRefreshActive = true;
_syncRefreshTimer();
case LiveTvRefreshLifecycleTransition.ignore:
break;
}
} }
void pauseRefresh() { void pauseRefresh() {
@@ -83,7 +92,7 @@ class WhatsOnTabState extends State<WhatsOnTab>
void _syncRefreshTimer() { void _syncRefreshTimer() {
_refreshTimer?.cancel(); _refreshTimer?.cancel();
_refreshTimer = null; _refreshTimer = null;
if (!_refreshRequested || !_tickerEnabled || !_appResumed || !mounted) return; if (!_refreshRequested || !_tickerEnabled || !_appRefreshActive || !mounted) return;
_refreshTimer = Timer.periodic(const Duration(seconds: 60), (_) => _loadHubs()); _refreshTimer = Timer.periodic(const Duration(seconds: 60), (_) => _loadHubs());
} }
@@ -16,10 +16,13 @@ Future<LiveStreamRetryResult> runLiveStreamRetry<Session>({
required bool Function() isCurrent, required bool Function() isCurrent,
required void Function(Session session) adoptSession, required void Function(Session session) adoptSession,
required void Function(Object error, StackTrace stackTrace) reportFailure, required void Function(Object error, StackTrace stackTrace) reportFailure,
required void Function(Session session) discardSession,
required void Function() onFinished, required void Function() onFinished,
}) async { }) async {
Session? recovered;
var adopted = false;
try { try {
final recovered = await recover(); recovered = await recover();
if (!isCurrent()) return LiveStreamRetryResult.stale; if (!isCurrent()) return LiveStreamRetryResult.stale;
if (recovered == null) throw StateError('Live stream recovery returned no session'); if (recovered == null) throw StateError('Live stream recovery returned no session');
@@ -34,12 +37,14 @@ Future<LiveStreamRetryResult> runLiveStreamRetry<Session>({
if (!isCurrent()) return LiveStreamRetryResult.stale; if (!isCurrent()) return LiveStreamRetryResult.stale;
adoptSession(recovered); adoptSession(recovered);
adopted = true;
return LiveStreamRetryResult.succeeded; return LiveStreamRetryResult.succeeded;
} catch (error, stackTrace) { } catch (error, stackTrace) {
if (!isCurrent()) return LiveStreamRetryResult.stale; if (!isCurrent()) return LiveStreamRetryResult.stale;
reportFailure(error, stackTrace); reportFailure(error, stackTrace);
return LiveStreamRetryResult.failed; return LiveStreamRetryResult.failed;
} finally { } finally {
if (recovered != null && !adopted) discardSession(recovered);
onFinished(); onFinished();
} }
} }
+2 -2
View File
@@ -29,12 +29,12 @@ extension _VideoPlayerErrorMethods on VideoPlayerScreenState {
if (_live.fallbackLevel < 2) { if (_live.fallbackLevel < 2) {
_live.fallbackLevel++; _live.fallbackLevel++;
_live.retrying = true; _live.retrying = true;
appLogger.w('Live stream failed, retrying with fallback level $_live.fallbackLevel'); appLogger.w('Live stream failed, retrying with fallback level ${_live.fallbackLevel}');
unawaited(_retryLiveStream()); unawaited(_retryLiveStream());
return; return;
} }
if (_live.retryFailed) { if (_live.retryFailed) {
showGlobalErrorSnackBar(t.messages.streamInterrupted); showGlobalErrorSnackBar(t.messages.liveStreamInterrupted);
return; return;
} }
} }
+2 -1
View File
@@ -123,10 +123,11 @@ extension _VideoPlayerLiveTvMethods on VideoPlayerScreenState {
_live.adoptSession(recovered); _live.adoptSession(recovered);
_live.markStreamRestartedAtLiveEdge(); _live.markStreamRestartedAtLiveEdge();
}, },
discardSession: _abandonLiveSession,
reportFailure: (error, stackTrace) { reportFailure: (error, stackTrace) {
appLogger.e('Failed to recover live stream', error: error, stackTrace: stackTrace); appLogger.e('Failed to recover live stream', error: error, stackTrace: stackTrace);
_live.retryFailed = true; _live.retryFailed = true;
showGlobalErrorSnackBar(t.messages.streamInterrupted); showGlobalErrorSnackBar(t.messages.liveStreamInterrupted);
}, },
onFinished: () { onFinished: () {
if (isCurrent()) _live.retrying = false; if (isCurrent()) _live.retrying = false;
@@ -10,6 +10,7 @@ import '../../i18n/strings.g.dart';
import '../../models/companion_remote/remote_command.dart'; import '../../models/companion_remote/remote_command.dart';
import '../../models/companion_remote/remote_session.dart'; import '../../models/companion_remote/remote_session.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
import '../../utils/serial_future_queue.dart';
import '../base_peer_service.dart'; import '../base_peer_service.dart';
import 'remote_auth_context.dart'; import 'remote_auth_context.dart';
import 'remote_auth_service.dart'; import 'remote_auth_service.dart';
@@ -29,6 +30,8 @@ class CompanionRemotePeerService with KeepaliveMixin {
// Client-side (remote) fields // Client-side (remote) fields
IOWebSocketChannel? _channel; IOWebSocketChannel? _channel;
StreamSubscription<dynamic>? _clientSocketSubscription;
StreamSubscription<dynamic>? _channelSubscription;
String? _myPeerId; String? _myPeerId;
String? _hostAddress; // Format: "ip:port" String? _hostAddress; // Format: "ip:port"
@@ -223,7 +226,8 @@ class CompanionRemotePeerService with KeepaliveMixin {
} }
}); });
socket.listen( late final StreamSubscription<dynamic> socketSubscription;
socketSubscription = socket.listen(
(data) async { (data) async {
try { try {
if (!isAuthenticated) { if (!isAuthenticated) {
@@ -313,6 +317,15 @@ class CompanionRemotePeerService with KeepaliveMixin {
unawaited(_clientSocket!.close(4004, 'Replaced by new connection')); unawaited(_clientSocket!.close(4004, 'Replaced by new connection'));
} }
final previousSubscription = _clientSocketSubscription;
if (previousSubscription != null) {
try {
await previousSubscription.cancel();
} catch (e) {
appLogger.d('CompanionRemote: previous client listener cancel ignored', error: e);
}
}
_clientSocketSubscription = socketSubscription;
_clientSocket = socket; _clientSocket = socket;
_sessionEncKey = sessionEncKey; _sessionEncKey = sessionEncKey;
_sendCounter = 0; _sendCounter = 0;
@@ -350,6 +363,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
} }
}, },
onDone: () { onDone: () {
unawaited(socketSubscription.cancel());
authTimeout?.cancel(); authTimeout?.cancel();
appLogger.d('CompanionRemote: WebSocket connection closed'); appLogger.d('CompanionRemote: WebSocket connection closed');
// A replaced client's socket closes AFTER the new client already took // A replaced client's socket closes AFTER the new client already took
@@ -357,6 +371,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
// tear it down, or we'd clobber the live connection. // tear it down, or we'd clobber the live connection.
if (isAuthenticated && identical(_clientSocket, socket)) { if (isAuthenticated && identical(_clientSocket, socket)) {
_clientSocket = null; _clientSocket = null;
_clientSocketSubscription = null;
_sessionEncKey = null; _sessionEncKey = null;
_isAuthenticated = false; _isAuthenticated = false;
_selectedAuthContextId = null; _selectedAuthContextId = null;
@@ -429,7 +444,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
List<int>? clientNonce; List<int>? clientNonce;
String? receivedHostClientId; String? receivedHostClientId;
_channel!.stream.listen( _channelSubscription = _channel!.stream.listen(
(data) async { (data) async {
try { try {
if (_isAuthenticated) { if (_isAuthenticated) {
@@ -596,6 +611,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
_deviceDisconnectedController.add(null); _deviceDisconnectedController.add(null);
_connectionStateController.add(RemoteSessionStatus.disconnected); _connectionStateController.add(RemoteSessionStatus.disconnected);
_isAuthenticated = false; _isAuthenticated = false;
_channelSubscription = null;
_sessionEncKey = null; _sessionEncKey = null;
_selectedAuthContextId = null; _selectedAuthContextId = null;
_selectedHostClientId = null; _selectedHostClientId = null;
@@ -770,12 +786,12 @@ class CompanionRemotePeerService with KeepaliveMixin {
// Serialize cryptographic operations so implicit nonce counters cannot // Serialize cryptographic operations so implicit nonce counters cannot
// interleave when stream callbacks overlap. // interleave when stream callbacks overlap.
Future<void>? _encryptChain; final SerialFutureQueue _encryptQueue = SerialFutureQueue();
Future<void>? _decryptChain; final SerialFutureQueue _decryptQueue = SerialFutureQueue();
Future<void>? _sendChain; final SerialFutureQueue _sendQueue = SerialFutureQueue();
Future<List<int>> _encryptOutgoing(String plaintext) { Future<List<int>> _encryptOutgoing(String plaintext) {
final result = (_encryptChain ?? Future<void>.value()).then((_) async { return _encryptQueue.run(() async {
final encrypted = await RemoteAuthService.instance.encrypt( final encrypted = await RemoteAuthService.instance.encrypt(
_sessionEncKey!, _sessionEncKey!,
utf8.encode(plaintext), utf8.encode(plaintext),
@@ -785,8 +801,6 @@ class CompanionRemotePeerService with KeepaliveMixin {
_sendCounter++; _sendCounter++;
return encrypted; return encrypted;
}); });
_encryptChain = result.then<void>((_) {});
return result;
} }
Future<void> _sendEncryptedToSocket(WebSocket socket, String plaintext) async { Future<void> _sendEncryptedToSocket(WebSocket socket, String plaintext) async {
@@ -797,9 +811,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
Future<String?> _decryptIncoming(dynamic data) { Future<String?> _decryptIncoming(dynamic data) {
if (_sessionEncKey == null) return Future<String?>.value(); if (_sessionEncKey == null) return Future<String?>.value();
final result = (_decryptChain ?? Future<void>.value()).then((_) => _decryptIncomingNow(data)); return _decryptQueue.run(() => _decryptIncomingNow(data));
_decryptChain = result.then<void>((_) {});
return result;
} }
Future<String?> _decryptIncomingNow(dynamic data) async { Future<String?> _decryptIncomingNow(dynamic data) async {
@@ -881,7 +893,7 @@ class CompanionRemotePeerService with KeepaliveMixin {
} }
// Chain sends to prevent counter interleaving from concurrent async encrypts // Chain sends to prevent counter interleaving from concurrent async encrypts
_sendChain = (_sendChain ?? Future.value()).then((_) async { _sendQueue.run(() async {
try { try {
final json = jsonEncode(command.toJson()); final json = jsonEncode(command.toJson());
final encrypted = await _encryptOutgoing(json); final encrypted = await _encryptOutgoing(json);
@@ -905,56 +917,64 @@ class CompanionRemotePeerService with KeepaliveMixin {
}); });
} }
Future<void> _runDisconnectCleanup(Future<dynamic>? operation, String name) async {
if (operation == null) return;
try {
await operation;
} catch (e) {
appLogger.d('CompanionRemote: $name cleanup ignored', error: e);
}
}
Future<void> disconnect() async { Future<void> disconnect() async {
appLogger.d('CompanionRemote: Disconnecting'); appLogger.d('CompanionRemote: Disconnecting');
_isAuthenticated = false;
stopKeepalive(); stopKeepalive();
// Flush commands already decoded by the overlapping stream callbacks final clientSocket = _clientSocket;
// before closing their transport, then reject any later send request. final channel = _channel;
await _decryptChain; final server = _server;
await _sendChain;
await _encryptChain;
_isAuthenticated = false;
if (_clientSocket != null) { _server = null;
try {
await _clientSocket!.close(); try {
} catch (e) { // Stop inbound callbacks before taking queue snapshots. New sends are
appLogger.d('CompanionRemote: client socket close ignored', error: e); // already rejected by `_isAuthenticated = false`.
} await _runDisconnectCleanup(_clientSocketSubscription?.cancel(), 'client listener');
_clientSocketSubscription = null;
await _runDisconnectCleanup(_channelSubscription?.cancel(), 'channel listener');
_channelSubscription = null;
final serverClose = server?.close(force: true);
// Decrypted commands may enqueue acknowledgements, and sends enqueue
// encryption, so drain in dependency order.
await _decryptQueue.settled;
await _sendQueue.settled;
await _encryptQueue.settled;
await _runDisconnectCleanup(clientSocket?.close(), 'client socket');
await _runDisconnectCleanup(channel?.sink.close(), 'channel');
await _runDisconnectCleanup(serverClose, 'server');
} finally {
_clientSocket = null; _clientSocket = null;
}
if (_channel != null) {
try {
await _channel!.sink.close();
} catch (e) {
appLogger.d('CompanionRemote: channel close ignored', error: e);
}
_channel = null; _channel = null;
_myPeerId = null;
_hostAddress = null;
_role = null;
_selectedAuthContextId = null;
_selectedHostClientId = null;
_sessionEncKey = null;
_sendCounter = 0;
_recvCounter = 0;
_sendQueue.reset();
_encryptQueue.reset();
_decryptQueue.reset();
_failedAuthAttempts.clear();
_authLockouts.clear();
_connectionStateController.add(RemoteSessionStatus.disconnected);
} }
if (_server != null) {
await _server!.close();
_server = null;
}
_myPeerId = null;
_hostAddress = null;
_role = null;
_selectedAuthContextId = null;
_selectedHostClientId = null;
_sessionEncKey = null;
_sendCounter = 0;
_recvCounter = 0;
_sendChain = null;
_encryptChain = null;
_decryptChain = null;
_failedAuthAttempts.clear();
_authLockouts.clear();
_connectionStateController.add(RemoteSessionStatus.disconnected);
} }
/// Whether the HTTP server is currently running. /// Whether the HTTP server is currently running.
+19 -4
View File
@@ -6,6 +6,7 @@ import 'package:drift/drift.dart';
import '../database/app_database.dart'; import '../database/app_database.dart';
import '../media/media_backend.dart'; import '../media/media_backend.dart';
import '../media/media_item.dart'; import '../media/media_item.dart';
import '../utils/app_logger.dart';
import '../utils/global_key_utils.dart'; import '../utils/global_key_utils.dart';
import '../utils/isolate_helper.dart'; import '../utils/isolate_helper.dart';
import 'api_cache.dart'; import 'api_cache.dart';
@@ -116,10 +117,11 @@ class JellyfinApiCache extends ApiCache {
} }
} }
/// Persist a watched/unwatched flip into every cached `BaseItemDto` row /// Persist a watched/unwatched flip into cached `BaseItemDto` rows for
/// for [itemId] (one per cached userId). Mirrors what the server returns /// [itemId]. Compound Jellyfin scope ids update only their user. A legacy
/// after the flip so a later cache reload reflects the current watched /// bare machine id is accepted only when its matching rows belong to one
/// state without a network roundtrip. /// user; ambiguous multi-user writes are skipped rather than bleeding watch
/// state across profiles.
/// ///
/// [viewOffsetMs] is converted to Jellyfin's 100-ns ticks for /// [viewOffsetMs] is converted to Jellyfin's 100-ns ticks for
/// `UserData.PlaybackPositionTicks`. [lastViewedAt] is treated as Plex's /// `UserData.PlaybackPositionTicks`. [lastViewedAt] is treated as Plex's
@@ -141,6 +143,19 @@ class JellyfinApiCache extends ApiCache {
..where((t) => _resolver.itemKeyPredicate(t.cacheKey, serverId, itemId)); ..where((t) => _resolver.itemKeyPredicate(t.cacheKey, serverId, itemId));
final rows = await query.get(); final rows = await query.get();
if (rows.isEmpty) return; if (rows.isEmpty) return;
if (!serverId.contains('/')) {
final userIds = <String>{
for (final row in rows)
if (JellyfinCacheResolver.parseItemKey(row.cacheKey) case final key?) key.userId,
};
if (userIds.length > 1) {
appLogger.w(
'Skipping ambiguous bare-scope Jellyfin watch-state cache write',
error: {'serverId': serverId, 'itemId': itemId, 'userCount': userIds.length},
);
return;
}
}
for (final row in rows) { for (final row in rows) {
try { try {
final data = jsonDecode(row.data) as Map<String, dynamic>; final data = jsonDecode(row.data) as Map<String, dynamic>;
+29 -10
View File
@@ -74,22 +74,41 @@ class JellyfinCacheResolver {
..where((t) => t.pinned.equals(true)) ..where((t) => t.pinned.equals(true))
..orderBy([(t) => OrderingTerm.asc(t.cacheKey)])) ..orderBy([(t) => OrderingTerm.asc(t.cacheKey)]))
.get(); .get();
if (rows.isEmpty) return const [];
final connections = await (database.select(database.connections)..where((t) => t.kind.equals('jellyfin'))).get();
final connectionById = {for (final connection in connections) connection.id: connection};
final bindings = await database.select(database.profileConnections).get();
final bindingsByConnection = <String, List<ProfileConnectionRow>>{};
for (final binding in bindings) {
bindingsByConnection.putIfAbsent(binding.connectionId, () => []).add(binding);
}
bool matchesBinding(String connectionId, String userId) {
final connectionBindings = bindingsByConnection[connectionId];
return connectionBindings == null ||
connectionBindings.isEmpty ||
connectionBindings.any((binding) => binding.userIdentifier == userId);
}
final matches = <ResolvedJellyfinCacheItem>[]; final matches = <ResolvedJellyfinCacheItem>[];
for (final row in rows) { for (final row in rows) {
final resolved = await _resolveRow(row); final key = parseItemKey(row.cacheKey);
if (resolved != null) matches.add(resolved); if (key == null) continue;
final compoundId = '${key.machineId}/${key.userId}';
final compound = connectionById[compoundId];
if (compound != null && matchesBinding(compound.id, key.userId)) {
matches.add((cacheRow: row, connection: compound, key: key));
continue;
}
final legacy = connectionById[key.machineId];
if (legacy != null && matchesBinding(legacy.id, key.userId)) {
matches.add((cacheRow: row, connection: legacy, key: key));
}
} }
return matches; return matches;
} }
Future<ResolvedJellyfinCacheItem?> _resolveRow(ApiCacheData row) async {
final key = parseItemKey(row.cacheKey);
if (key == null) return null;
final connection = await findConnection(key.scopeId, userId: key.userId);
if (connection == null) return null;
return (cacheRow: row, connection: connection, key: key);
}
Future<ConnectionRow?> findConnection(String serverOrScopeId, {String? userId}) async { Future<ConnectionRow?> findConnection(String serverOrScopeId, {String? userId}) async {
final scope = _splitScope(serverOrScopeId); final scope = _splitScope(serverOrScopeId);
if (scope.userId != null && userId != null && scope.userId != userId) return null; if (scope.userId != null && userId != null && scope.userId != userId) return null;
+18 -10
View File
@@ -90,7 +90,8 @@ class MultiServerManager {
/// Coalescing guard for reconnectOfflineServers — prevents concurrent reconnect sweeps /// Coalescing guard for reconnectOfflineServers — prevents concurrent reconnect sweeps
Future<void>? _activeReconnect; Future<void>? _activeReconnect;
int _profileRefreshGeneration = 0; int _profileRefreshEpoch = 0;
final Map<String, int> _profileRefreshGenerations = {};
/// Debounce timer for connectivity events — collapses rapid network flapping /// Debounce timer for connectivity events — collapses rapid network flapping
Timer? _connectivityDebounce; Timer? _connectivityDebounce;
@@ -500,8 +501,15 @@ class MultiServerManager {
PlexAccountConnection connection, { PlexAccountConnection connection, {
Duration timeout = MediaServerTimeouts.perServerConnect, Duration timeout = MediaServerTimeouts.perServerConnect,
}) async { }) async {
final generation = ++_profileRefreshGeneration; final accountId = connection.id;
if (connection.servers.isEmpty) return const {}; final epoch = _profileRefreshEpoch;
final generation = (_profileRefreshGenerations[accountId] ?? 0) + 1;
_profileRefreshGenerations[accountId] = generation;
bool isStale() => epoch != _profileRefreshEpoch || _profileRefreshGenerations[accountId] != generation;
if (connection.servers.isEmpty) {
if (!isStale()) _profileRefreshGenerations.remove(accountId);
return const {};
}
final bound = <String>{}; final bound = <String>{};
final futures = connection.servers.map((server) async { final futures = connection.servers.map((server) async {
final serverId = server.clientIdentifier; final serverId = server.clientIdentifier;
@@ -510,9 +518,7 @@ class MultiServerManager {
final existing = _clients[serverId]; final existing = _clients[serverId];
if (existing is PlexClient && ((_serverStatus[serverId] ?? false) || _authErrorServers.contains(serverId))) { if (existing is PlexClient && ((_serverStatus[serverId] ?? false) || _authErrorServers.contains(serverId))) {
await existing.applyTokenUpdate(server.accessToken); await existing.applyTokenUpdate(server.accessToken);
if (generation != _profileRefreshGeneration || if (isStale() || !identical(_plexServers[serverId], server) || !identical(_clients[serverId], existing)) {
!identical(_plexServers[serverId], server) ||
!identical(_clients[serverId], existing)) {
return; return;
} }
_authErrorServers.remove(serverId); _authErrorServers.remove(serverId);
@@ -526,7 +532,7 @@ class MultiServerManager {
server: server, server: server,
clientIdentifier: connection.clientIdentifier, clientIdentifier: connection.clientIdentifier,
).namedTimeout(timeout, operation: 'connect to ${server.name}'); ).namedTimeout(timeout, operation: 'connect to ${server.name}');
if (generation != _profileRefreshGeneration || !identical(_plexServers[serverId], server)) { if (isStale() || !identical(_plexServers[serverId], server)) {
_closeClient(client); _closeClient(client);
return; return;
} }
@@ -538,18 +544,19 @@ class MultiServerManager {
bound.add(serverId); bound.add(serverId);
_connectProgressController.add((serverId: serverId, online: true)); _connectProgressController.add((serverId: serverId, online: true));
} catch (e, stackTrace) { } catch (e, stackTrace) {
if (generation != _profileRefreshGeneration || !identical(_plexServers[serverId], server)) return; if (isStale() || !identical(_plexServers[serverId], server)) return;
appLogger.e('refreshTokensForProfile: failed to connect ${server.name}', error: e, stackTrace: stackTrace); appLogger.e('refreshTokensForProfile: failed to connect ${server.name}', error: e, stackTrace: stackTrace);
_serverStatus[serverId] = false; _serverStatus[serverId] = false;
_connectProgressController.add((serverId: serverId, online: false)); _connectProgressController.add((serverId: serverId, online: false));
} }
}); });
await Future.wait(futures); await Future.wait(futures);
if (generation != _profileRefreshGeneration) return const {}; if (isStale()) return const {};
_statusController.add(Map.from(_serverStatus)); _statusController.add(Map.from(_serverStatus));
if (bound.isNotEmpty && _connectivitySubscription == null) { if (bound.isNotEmpty && _connectivitySubscription == null) {
_startNetworkMonitoring(); _startNetworkMonitoring();
} }
_profileRefreshGenerations.remove(accountId);
return bound; return bound;
} }
@@ -1171,7 +1178,8 @@ class MultiServerManager {
} }
Set<MediaServerClient> _detachAllClients() { Set<MediaServerClient> _detachAllClients() {
++_profileRefreshGeneration; ++_profileRefreshEpoch;
_profileRefreshGenerations.clear();
_stopNetworkMonitoring(); _stopNetworkMonitoring();
for (final timer in _reconnectDebounce.values) { for (final timer in _reconnectDebounce.values) {
timer.cancel(); timer.cancel();
+10 -5
View File
@@ -15,6 +15,15 @@ import 'dialogs.dart';
import 'download_version_utils.dart'; import 'download_version_utils.dart';
import 'snackbar_helper.dart'; import 'snackbar_helper.dart';
@visibleForTesting
String? validateEpisodeCountInput(String text, {required bool allowZero}) {
final count = int.tryParse(text);
if (count == null || count < 0 || (!allowZero && count == 0)) {
return t.downloads.invalidEpisodeCount;
}
return null;
}
/// Dialog option for the download picker. Typed to avoid stringly-typed values. /// Dialog option for the download picker. Typed to avoid stringly-typed values.
enum _DownloadChoice { all, unwatched, next5, next10, custom, delete } enum _DownloadChoice { all, unwatched, next5, next10, custom, delete }
@@ -313,11 +322,7 @@ Future<int?> _showEpisodeCountDialog(
confirmText: t.common.ok, confirmText: t.common.ok,
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly], inputFormatters: [FilteringTextInputFormatter.digitsOnly],
validator: (text) { validator: (text) => validateEpisodeCountInput(text, allowZero: allowZero),
final n = int.tryParse(text);
if (n == null || n < 0 || (!allowZero && n == 0)) return '';
return null;
},
); );
if (result == null) return null; if (result == null) return null;
return int.tryParse(result); return int.tryParse(result);
+26
View File
@@ -0,0 +1,26 @@
/// Runs asynchronous operations one at a time without allowing a failed
/// operation to poison the queue tail.
class SerialFutureQueue {
Future<void> _tail = Future<void>.value();
Future<T> run<T>(Future<T> Function() operation) {
final result = _tail.then((_) => operation());
_tail = _settle(result);
return result;
}
Future<void> get settled => _tail;
void reset() {
_tail = Future<void>.value();
}
static Future<void> _settle<T>(Future<T> operation) async {
try {
await operation;
} catch (_) {
// The returned operation future carries the error to its caller. The
// internal tail only represents when the queue may start its next item.
}
}
}
+30 -28
View File
@@ -1,9 +1,10 @@
import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import '../i18n/strings.g.dart'; import '../i18n/strings.g.dart';
import '../providers/download_provider.dart'; import '../providers/download_provider.dart';
import '../widgets/deletion_progress_dialog.dart'; import '../widgets/deletion_progress_dialog.dart';
import 'dialogs.dart';
class SmartDeletionHandler { class SmartDeletionHandler {
/// Execute deletion with smart progress dialog /// Execute deletion with smart progress dialog
@@ -15,11 +16,11 @@ class SmartDeletionHandler {
int delayMs = 500, int delayMs = 500,
}) async { }) async {
bool deletionComplete = false; bool deletionComplete = false;
final dialogKey = GlobalKey(); ({NavigatorState navigator, DialogRoute<void> route})? progressDialog;
Future.delayed(Duration(milliseconds: delayMs), () { final progressTimer = Timer(Duration(milliseconds: delayMs), () {
if (!deletionComplete && context.mounted) { if (!deletionComplete && context.mounted) {
_showProgressDialog(context, provider, globalKey, dialogKey); progressDialog = _showProgressDialog(context, globalKey);
} }
}); });
@@ -27,39 +28,40 @@ class SmartDeletionHandler {
await provider.deleteDownload(globalKey); await provider.deleteDownload(globalKey);
} finally { } finally {
deletionComplete = true; deletionComplete = true;
final dialogContext = dialogKey.currentContext; progressTimer.cancel();
if (dialogContext != null && dialogContext.mounted) { final dialog = progressDialog;
final route = ModalRoute.of(dialogContext); if (dialog != null && dialog.navigator.mounted && dialog.route.isActive) {
if (route != null && route.isActive) { dialog.navigator.removeRoute(dialog.route);
Navigator.of(dialogContext).removeRoute(route);
}
} }
} }
} }
static void _showProgressDialog(BuildContext context, DownloadProvider _, String globalKey, GlobalKey dialogKey) { static ({NavigatorState navigator, DialogRoute<void> route}) _showProgressDialog(
showScopedDialog<void>( BuildContext context,
String globalKey,
) {
final navigator = Navigator.of(context);
final route = DialogRoute<void>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (dialogContext) => KeyedSubtree( builder: (_) => Consumer<DownloadProvider>(
key: dialogKey, builder: (context, provider, child) {
child: Consumer<DownloadProvider>( final progress = provider.getDeletionProgress(globalKey);
builder: (context, provider, child) {
final progress = provider.getDeletionProgress(globalKey);
if (progress == null) { if (progress == null) {
return AlertDialog( return AlertDialog(
content: Row( content: Row(
mainAxisSize: .min, mainAxisSize: .min,
children: [const CircularProgressIndicator(), const SizedBox(width: 20), Text(t.downloads.deleting)], children: [const CircularProgressIndicator(), const SizedBox(width: 20), Text(t.downloads.deleting)],
), ),
); );
} }
return DeletionProgressDialog(progress: progress); return DeletionProgressDialog(progress: progress);
}, },
),
), ),
); );
navigator.push(route);
return (navigator: navigator, route: route);
} }
} }
@@ -16,10 +16,34 @@ import '../../profiles/active_profile_provider.dart';
import '../../profiles/plex_home_service.dart'; import '../../profiles/plex_home_service.dart';
import '../../profiles/profile_connection_registry.dart'; import '../../profiles/profile_connection_registry.dart';
import '../../providers/companion_remote_provider.dart'; import '../../providers/companion_remote_provider.dart';
import '../../services/base_peer_service.dart';
import '../../services/settings_service.dart'; import '../../services/settings_service.dart';
import '../../utils/app_logger.dart'; import '../../utils/app_logger.dart';
import '../loading_indicator_box.dart'; import '../loading_indicator_box.dart';
@visibleForTesting
String companionRemotePairingErrorMessage(Object error) {
if (error is PeerError) {
return switch (error.type) {
PeerErrorType.timeout => t.companionRemote.pairing.connectionTimedOut,
PeerErrorType.connectionFailed || PeerErrorType.invalidSession => t.companionRemote.pairing.sessionNotFound,
PeerErrorType.authFailed => t.companionRemote.pairing.authFailed,
_ => error.message,
};
}
final message = error.toString().replaceFirst('Exception: ', '');
if (message.contains('timeout') || message.contains('Timed out')) {
return t.companionRemote.pairing.connectionTimedOut;
} else if (message.contains('Failed to connect')) {
return t.companionRemote.pairing.sessionNotFound;
} else if (message.contains('Authentication failed')) {
return t.companionRemote.pairing.authFailed;
}
return t.companionRemote.pairing.failedToConnect(error: message);
}
/// Discovers LAN hosts and provides UI to connect to them. /// Discovers LAN hosts and provides UI to connect to them.
class DiscoveryView extends StatefulWidget { class DiscoveryView extends StatefulWidget {
const DiscoveryView({super.key}); const DiscoveryView({super.key});
@@ -158,7 +182,7 @@ class _DiscoveryViewState extends State<DiscoveryView> with ControllerDisposerMi
} catch (e) { } catch (e) {
appLogger.e('Failed to connect', error: e); appLogger.e('Failed to connect', error: e);
if (!mounted) return; if (!mounted) return;
setState(() => _errorMessage = _parseErrorMessage(e.toString())); setState(() => _errorMessage = companionRemotePairingErrorMessage(e));
} finally { } finally {
setStateIfMounted(() => _isConnecting = false); setStateIfMounted(() => _isConnecting = false);
} }
@@ -184,17 +208,6 @@ class _DiscoveryViewState extends State<DiscoveryView> with ControllerDisposerMi
); );
} }
String _parseErrorMessage(String error) {
if (error.contains('timeout') || error.contains('Timed out')) {
return t.companionRemote.pairing.connectionTimedOut;
} else if (error.contains('Failed to connect')) {
return t.companionRemote.pairing.sessionNotFound;
} else if (error.contains('Authentication failed')) {
return t.companionRemote.pairing.authFailed;
}
return t.companionRemote.pairing.failedToConnect(error: error.replaceAll('Exception: ', ''));
}
IconData _platformIcon(String platform) { IconData _platformIcon(String platform) {
switch (platform.toLowerCase()) { switch (platform.toLowerCase()) {
case 'macos': case 'macos':
+19 -9
View File
@@ -202,12 +202,15 @@ class OptimizedMediaImage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final path = localFilePath; final path = localFilePath;
if (path == null) return _buildResolved(context, null); if (path == null) {
return _buildResolved(context, _LocalFileResolution.missing, null);
}
return _ResolvedLocalFile(path: path, builder: _buildResolved); return _ResolvedLocalFile(path: path, builder: _buildResolved);
} }
Widget _buildResolved(BuildContext context, File? localFile) { Widget _buildResolved(BuildContext context, _LocalFileResolution resolution, File? localFile) {
final hasLocal = localFile != null; if (resolution == _LocalFileResolution.pending) return _surfacePlaceholder(context);
final hasLocal = resolution == _LocalFileResolution.present;
if (!hasLocal && (imagePath == null || imagePath!.isEmpty)) { if (!hasLocal && (imagePath == null || imagePath!.isEmpty)) {
return _buildFallback(context); return _buildFallback(context);
@@ -216,7 +219,7 @@ class OptimizedMediaImage extends StatelessWidget {
if (_hasKnownDimensions) { if (_hasKnownDimensions) {
return blurArtwork( return blurArtwork(
hasLocal hasLocal
? _buildLocalFileImage(context, localFile, width!, height!) ? _buildLocalFileImage(context, localFile!, width!, height!)
: _buildCachedImage(context, width!, height!), : _buildCachedImage(context, width!, height!),
); );
} }
@@ -227,7 +230,7 @@ class OptimizedMediaImage extends StatelessWidget {
final effectiveWidth = _resolvedDimension(width, constraints.maxWidth, 300.0); final effectiveWidth = _resolvedDimension(width, constraints.maxWidth, 300.0);
final effectiveHeight = _resolvedDimension(height, constraints.maxHeight, 450.0); final effectiveHeight = _resolvedDimension(height, constraints.maxHeight, 450.0);
return hasLocal return hasLocal
? _buildLocalFileImage(context, localFile, effectiveWidth, effectiveHeight) ? _buildLocalFileImage(context, localFile!, effectiveWidth, effectiveHeight)
: _buildCachedImage(context, effectiveWidth, effectiveHeight); : _buildCachedImage(context, effectiveWidth, effectiveHeight);
}, },
), ),
@@ -513,11 +516,13 @@ class _FadeInNetworkImageState extends State<_FadeInNetworkImage> with SingleTic
} }
} }
enum _LocalFileResolution { pending, missing, present }
class _ResolvedLocalFile extends StatefulWidget { class _ResolvedLocalFile extends StatefulWidget {
const _ResolvedLocalFile({required this.path, required this.builder}); const _ResolvedLocalFile({required this.path, required this.builder});
final String path; final String path;
final Widget Function(BuildContext context, File? file) builder; final Widget Function(BuildContext context, _LocalFileResolution resolution, File? file) builder;
@override @override
State<_ResolvedLocalFile> createState() => _ResolvedLocalFileState(); State<_ResolvedLocalFile> createState() => _ResolvedLocalFileState();
@@ -525,6 +530,7 @@ class _ResolvedLocalFile extends StatefulWidget {
class _ResolvedLocalFileState extends State<_ResolvedLocalFile> { class _ResolvedLocalFileState extends State<_ResolvedLocalFile> {
File? _file; File? _file;
_LocalFileResolution _resolution = _LocalFileResolution.pending;
int _generation = 0; int _generation = 0;
@override @override
@@ -536,16 +542,20 @@ class _ResolvedLocalFileState extends State<_ResolvedLocalFile> {
@override @override
void didUpdateWidget(_ResolvedLocalFile oldWidget) { void didUpdateWidget(_ResolvedLocalFile oldWidget) {
super.didUpdateWidget(oldWidget); super.didUpdateWidget(oldWidget);
if (oldWidget.path != widget.path) _resolve(); if (oldWidget.path != widget.path || _resolution == _LocalFileResolution.missing) _resolve();
} }
void _resolve() { void _resolve() {
final generation = ++_generation; final generation = ++_generation;
_file = null; _file = null;
_resolution = _LocalFileResolution.pending;
final candidate = File(widget.path); final candidate = File(widget.path);
candidate.exists().then((exists) { candidate.exists().then((exists) {
if (!mounted || generation != _generation) return; if (!mounted || generation != _generation) return;
setState(() => _file = exists ? candidate : null); setState(() {
_file = exists ? candidate : null;
_resolution = exists ? _LocalFileResolution.present : _LocalFileResolution.missing;
});
}); });
} }
@@ -556,5 +566,5 @@ class _ResolvedLocalFileState extends State<_ResolvedLocalFile> {
} }
@override @override
Widget build(BuildContext context) => widget.builder(context, _file); Widget build(BuildContext context) => widget.builder(context, _resolution, _file);
} }
@@ -248,12 +248,23 @@ extension _PlexVideoControlsVisibilityMethods on _PlexVideoControlsState {
_desktopControlsKey.currentState?.hideContentStrip(); _desktopControlsKey.currentState?.hideContentStrip();
_cancelSkipButtonDismissTimer(); _cancelSkipButtonDismissTimer();
_setControlsState(() { _setControlsState(() {
_controlsOpaque = false;
if (_currentMarker != null) _skipButtonDismissed = true; if (_currentMarker != null) _skipButtonDismissed = true;
}); });
_reclaimFocusAfterControlsHide(); _reclaimFocusAfterControlsHide();
} else { } else if (visibilityChanged) {
_setControlsState(() { _setControlsState(() {
if (controlsVisible) _controlsMounted = true; _controlsMounted = true;
_controlsOpaque = false;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted || !_showControls || !_controlsMounted) return;
_setControlsState(() => _controlsOpaque = true);
});
} else if (controlsVisible && !_controlsMounted) {
_setControlsState(() {
_controlsMounted = true;
_controlsOpaque = true;
}); });
} }
@@ -541,6 +541,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
late bool _lastControlsVisible; late bool _lastControlsVisible;
late bool _controlsMounted; late bool _controlsMounted;
late bool _controlsOpaque;
bool _isLoadingExtras = false; bool _isLoadingExtras = false;
// Item key the in-flight extras load belongs to, so a load for a swapped // Item key the in-flight extras load belongs to, so a load for a swapped
// item can start while a stale one is still in flight (and the stale // item can start while a stale one is still in flight (and the stale
@@ -658,6 +659,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
super.initState(); super.initState();
_lastControlsVisible = widget.chromeController.controlsVisible; _lastControlsVisible = widget.chromeController.controlsVisible;
_controlsMounted = _lastControlsVisible; _controlsMounted = _lastControlsVisible;
_controlsOpaque = _lastControlsVisible;
_focusNode = FocusNode(); _focusNode = FocusNode();
_skipMarkerFocusNode = FocusNode(debugLabel: 'SkipMarkerButton'); _skipMarkerFocusNode = FocusNode(debugLabel: 'SkipMarkerButton');
_seekThrottle = throttle( _seekThrottle = throttle(
@@ -750,6 +752,8 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
if (oldWidget.chromeController != widget.chromeController) { if (oldWidget.chromeController != widget.chromeController) {
oldWidget.chromeController.removeListener(_onChromeChanged); oldWidget.chromeController.removeListener(_onChromeChanged);
_lastControlsVisible = widget.chromeController.controlsVisible; _lastControlsVisible = widget.chromeController.controlsVisible;
_controlsMounted = _lastControlsVisible;
_controlsOpaque = _lastControlsVisible;
widget.chromeController.addListener(_onChromeChanged); widget.chromeController.addListener(_onChromeChanged);
} }
// The same controls instance survives in-place episode swaps — re-key // The same controls instance survives in-place episode swaps — re-key
@@ -948,7 +952,7 @@ class _PlexVideoControlsState extends State<PlexVideoControls>
// Prevent focus from entering controls when hidden // Prevent focus from entering controls when hidden
canRequestFocus: _showControls, canRequestFocus: _showControls,
child: AnimatedOpacity( child: AnimatedOpacity(
opacity: _showControls ? 1.0 : 0.0, opacity: _controlsOpaque ? 1.0 : 0.0,
duration: const Duration(milliseconds: 200), duration: const Duration(milliseconds: 200),
onEnd: () { onEnd: () {
if (!_showControls) { if (!_showControls) {
+12
View File
@@ -80,6 +80,18 @@ else
FAILED=1 FAILED=1
fi fi
# 4. Workflow and script regression guards
section "workflow and script guards"
if python3 scripts/check_build_workflow.py &&
python3 scripts/check_update_packages_workflow.py &&
python3 scripts/test_pubspec_version.py &&
python3 scripts/test_clean_translations.py; then
ok "workflow and script guards passed"
else
fail "workflow or script guard failed"
FAILED=1
fi
# 3. Native formatting # 3. Native formatting
section "native format" section "native format"
out="$(mktemp)" out="$(mktemp)"
+18
View File
@@ -73,6 +73,20 @@ void main() {
}); });
}); });
test('advance cleanup failure is contained after the transition', () async {
final core = _AudioCoreMock()..failPlaylistRemove0 = true;
await run(core, (player, transitions) async {
await openFirst(player);
await player.setNext(Media('https://example.test/t2.flac'));
player.handlePlayerEvent('file-loaded', null);
await Future<void>.delayed(Duration.zero);
expect(transitions, ['https://example.test/t2.flac']);
expect(core.commands('playlist-remove').last, ['playlist-remove', '0']);
});
});
test('open() still converts content:// (regression)', () async { test('open() still converts content:// (regression)', () async {
final core = _AudioCoreMock(); final core = _AudioCoreMock();
await run(core, (player, transitions) async { await run(core, (player, transitions) async {
@@ -269,6 +283,7 @@ class _AudioCoreMock {
int _nextFd = 7; int _nextFd = 7;
bool failOpenContentFd = false; bool failOpenContentFd = false;
bool failPlaylistRemove1 = false; bool failPlaylistRemove1 = false;
bool failPlaylistRemove0 = false;
Future<Object?> handle(MethodCall call) async { Future<Object?> handle(MethodCall call) async {
calls.add(call); calls.add(call);
@@ -289,6 +304,9 @@ class _AudioCoreMock {
return null; return null;
case 'command': case 'command':
final args = (_args(call)['args'] as List).cast<Object?>(); final args = (_args(call)['args'] as List).cast<Object?>();
if (failPlaylistRemove0 && args.length >= 2 && args[0] == 'playlist-remove' && args[1] == '0') {
throw PlatformException(code: 'error', message: 'playlist-remove failed');
}
if (failPlaylistRemove1 && args.length >= 2 && args[0] == 'playlist-remove' && args[1] == '1') { if (failPlaylistRemove1 && args.length >= 2 && args[0] == 'playlist-remove' && args[1] == '1') {
throw PlatformException(code: 'error', message: 'playlist-remove failed'); throw PlatformException(code: 'error', message: 'playlist-remove failed');
} }
@@ -789,6 +789,47 @@ void main() {
expect(activeProfile.isBinding, isFalse); expect(activeProfile.isBinding, isFalse);
}); });
test('A to B to A during one pass forces a complete final A bind', () async {
binder.dispose();
multiServerProvider.dispose();
final gated = _GatedJellyfinManager();
manager = gated;
multiServerProvider = MultiServerProvider(manager, DataAggregationService(manager));
binder = ActiveProfileBinder(
activeProfile: activeProfile,
connections: connections,
profileConnections: profileConnections,
serverManager: manager,
multiServerProvider: multiServerProvider,
pinPrompt: (_, {String? errorMessage}) async => null,
shouldDeferInitialBind: (_) async => false,
);
final profileA = await createActiveLocalProfile('local-a');
final profileB = Profile.local(id: 'local-b', displayName: 'B', createdAt: DateTime(2026, 1, 2));
await profiles.upsert(profileB);
await pumpUntil(() async => activeProfile.profiles.any((profile) => profile.id == profileB.id));
final jellyfin = _jellyfinConnection();
await connections.upsert(jellyfin);
await profileConnections.upsert(
ProfileConnection(profileId: profileA.id, connectionId: jellyfin.id, userIdentifier: jellyfin.userId),
);
binder.start();
await pumpUntil(() async => gated.calls == 1);
expect(await activeProfile.activate(profileB), isTrue);
expect(await activeProfile.activate(profileA), isTrue);
gated.gate.complete();
await activeProfile.awaitBindingSettle();
expect(gated.calls, 2);
expect(binder.debugLastBoundProfileId, profileA.id);
expect(multiServerProvider.onlineServerIds, ['jf-machine']);
expect(activeProfile.lastBindingSucceeded, isTrue);
});
test('passive notifications do not retry a failed profile; explicit rebind does', () async { test('passive notifications do not retry a failed profile; explicit rebind does', () async {
binder.dispose(); binder.dispose();
multiServerProvider.dispose(); multiServerProvider.dispose();
+13 -1
View File
@@ -151,6 +151,7 @@ void main() {
late HiddenLibrariesProvider hiddenLibraries; late HiddenLibrariesProvider hiddenLibraries;
late LibrariesProvider libraries; late LibrariesProvider libraries;
late DiscoverProvider provider; late DiscoverProvider provider;
late List<List<MediaItem>> shelfSyncs;
bool isBinding = false; bool isBinding = false;
setUp(() async { setUp(() async {
@@ -158,6 +159,7 @@ void main() {
SettingsService.resetForTesting(); SettingsService.resetForTesting();
await SettingsService.getInstance(); await SettingsService.getInstance();
isBinding = false; isBinding = false;
shelfSyncs = [];
client = _FakeClient(); client = _FakeClient();
final manager = MultiServerManager()..debugRegisterClientForTesting(client); final manager = MultiServerManager()..debugRegisterClientForTesting(client);
@@ -165,7 +167,13 @@ void main() {
multiServer = MultiServerProvider(manager, aggregation); multiServer = MultiServerProvider(manager, aggregation);
hiddenLibraries = HiddenLibrariesProvider(); hiddenLibraries = HiddenLibrariesProvider();
libraries = LibrariesProvider(); libraries = LibrariesProvider();
provider = DiscoverProvider(multiServer, hiddenLibraries, libraries, isProfileBinding: () => isBinding); provider = DiscoverProvider(
multiServer,
hiddenLibraries,
libraries,
isProfileBinding: () => isBinding,
syncSystemShelf: (items) async => shelfSyncs.add(List<MediaItem>.of(items)),
);
}); });
tearDown(() { tearDown(() {
@@ -290,6 +298,8 @@ void main() {
aggregation.onDeckResult = () => [playing, for (var i = 2; i <= 21; i++) _item('ep-$i')]; aggregation.onDeckResult = () => [playing, for (var i = 2; i <= 21; i++) _item('ep-$i')];
aggregation.hubsResult = () => [_hub('hub-1')]; aggregation.hubsResult = () => [_hub('hub-1')];
await provider.load(); await provider.load();
await pumpEventQueue();
shelfSyncs.clear();
final onDeckCallsBefore = aggregation.onDeckCalls; final onDeckCallsBefore = aggregation.onDeckCalls;
final hubCallsBefore = aggregation.hubCalls; final hubCallsBefore = aggregation.hubCalls;
@@ -302,6 +312,8 @@ void main() {
expect(provider.hasMoreContinueWatching, isTrue); expect(provider.hasMoreContinueWatching, isTrue);
expect(aggregation.onDeckCalls, onDeckCallsBefore); expect(aggregation.onDeckCalls, onDeckCallsBefore);
expect(aggregation.hubCalls, hubCallsBefore); expect(aggregation.hubCalls, hubCallsBefore);
expect(shelfSyncs, hasLength(1));
expect(shelfSyncs.single.first.viewOffsetMs, 30000);
}); });
test('watched-threshold progress refreshes continue watching only', () async { test('watched-threshold progress refreshes continue watching only', () async {
+86 -1
View File
@@ -16,6 +16,7 @@ import 'package:plezy/services/download_manager_service.dart';
import 'package:plezy/services/download_storage_service.dart'; import 'package:plezy/services/download_storage_service.dart';
import 'package:plezy/services/jellyfin_api_cache.dart'; import 'package:plezy/services/jellyfin_api_cache.dart';
import 'package:plezy/services/plex_api_cache.dart'; import 'package:plezy/services/plex_api_cache.dart';
import 'package:plezy/utils/deletion_notifier.dart';
import 'package:plezy/utils/watch_state_notifier.dart'; import 'package:plezy/utils/watch_state_notifier.dart';
import '../test_helpers/media_items.dart'; import '../test_helpers/media_items.dart';
@@ -37,14 +38,18 @@ class _ThrowingClient implements MediaServerClient {
/// Returns canned tracks from [fetchPlayableDescendants] (album/artist /// Returns canned tracks from [fetchPlayableDescendants] (album/artist
/// expansion) and records the requested parent ids. /// expansion) and records the requested parent ids.
class _MusicExpansionClient implements MediaServerClient { class _MusicExpansionClient implements MediaServerClient {
_MusicExpansionClient(this.tracks); _MusicExpansionClient(this.tracks, {this.gate, this.started});
final List<MediaItem> tracks; final List<MediaItem> tracks;
final Future<void>? gate;
final Completer<void>? started;
final fetchPlayableDescendantsCalls = <String>[]; final fetchPlayableDescendantsCalls = <String>[];
@override @override
Future<List<MediaItem>> fetchPlayableDescendants(String parentId) async { Future<List<MediaItem>> fetchPlayableDescendants(String parentId) async {
fetchPlayableDescendantsCalls.add(parentId); fetchPlayableDescendantsCalls.add(parentId);
if (started != null && !started!.isCompleted) started!.complete();
if (gate != null) await gate;
return tracks; return tracks;
} }
@@ -630,6 +635,45 @@ void main() {
p.dispose(); p.dispose();
}); });
test('container queue ownership remains claimed until expansion finishes', () async {
final album = testMediaItem(
id: 'album-1',
backend: MediaBackend.plex,
kind: MediaKind.album,
title: 'Album',
serverId: ServerId('srv'),
);
final track = testMediaItem(
id: 't1',
backend: MediaBackend.plex,
kind: MediaKind.track,
title: 'Track',
parentId: album.id,
serverId: ServerId('srv'),
);
final provider = DownloadProvider.forTesting(downloadManager: downloadManager, database: db);
await provider.ensureInitialized();
provider.debugSeedState(
downloads: {track.globalKey: DownloadProgress(globalKey: track.globalKey, status: DownloadStatus.completed)},
metadata: {track.globalKey: track},
ownedDownloadKeys: const {},
);
final started = Completer<void>();
final release = Completer<void>();
final client = _MusicExpansionClient([track], gate: release.future, started: started);
final first = provider.queueDownload(album, client);
await started.future;
expect(await provider.queueDownload(album, client), 0);
expect(client.fetchPlayableDescendantsCalls, ['album-1']);
release.complete();
expect(await first, 1);
expect(client.fetchPlayableDescendantsCalls, ['album-1']);
provider.dispose();
});
test('deleting an album emits one provider notification for all tracks', () async { test('deleting an album emits one provider notification for all tracks', () async {
MediaItem track(String id) => testMediaItem( MediaItem track(String id) => testMediaItem(
id: id, id: id,
@@ -668,11 +712,16 @@ void main() {
); );
var notifications = 0; var notifications = 0;
p.addListener(() => notifications++); p.addListener(() => notifications++);
final deletionEvents = <DeletionEvent>[];
final deletionSubscription = DeletionNotifier().stream.listen(deletionEvents.add);
addTearDown(deletionSubscription.cancel);
await p.deleteDownload(album.globalKey); await p.deleteDownload(album.globalKey);
await pumpEventQueue();
expect(notifications, 1); expect(notifications, 1);
expect(p.downloads, isEmpty); expect(p.downloads, isEmpty);
expect(deletionEvents.map((event) => event.itemId), unorderedEquals(['t1', 't2', 'album-1']));
p.dispose(); p.dispose();
}); });
@@ -1030,6 +1079,42 @@ void main() {
p.dispose(); p.dispose();
}); });
test('offline watch hydration snapshots downloads before database awaits', () async {
final provider = DownloadProvider.forTesting(downloadManager: downloadManager, database: db);
await provider.ensureInitialized();
final item = testMediaItem(
id: '1',
backend: MediaBackend.plex,
kind: MediaKind.movie,
title: 'Movie',
serverId: ServerId('srv'),
viewCount: 0,
);
provider.debugSeedState(
downloads: {
'srv:1': const DownloadProgress(globalKey: 'srv:1', status: DownloadStatus.completed),
'srv:2': const DownloadProgress(globalKey: 'srv:2', status: DownloadStatus.completed),
},
metadata: {item.globalKey: item},
);
await db.insertWatchAction(
profileId: 'test-profile',
serverId: ServerId('srv'),
ratingKey: item.id,
actionType: 'watched',
);
scheduleMicrotask(() {
provider.debugSeedState(
downloads: {'srv:3': const DownloadProgress(globalKey: 'srv:3', status: DownloadStatus.completed)},
);
});
await provider.debugHydrateOfflineWatchOverlay();
expect(provider.getMetadata(item.globalKey)?.isWatched, isTrue);
provider.dispose();
});
test('profile switch discards an in-flight metadata refresh from the old scope', () async { test('profile switch discards an in-flight metadata refresh from the old scope', () async {
await insertJellyfinConnection('user-a'); await insertJellyfinConnection('user-a');
await db.insertDownload( await db.insertDownload(
@@ -0,0 +1,16 @@
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/screens/livetv/live_tv_refresh_lifecycle.dart';
void main() {
test('desktop inactive state leaves Live TV refresh timers running', () {
expect(liveTvRefreshTransition(AppLifecycleState.inactive), LiveTvRefreshLifecycleTransition.ignore);
expect(liveTvRefreshTransition(AppLifecycleState.detached), LiveTvRefreshLifecycleTransition.ignore);
});
test('only actual background states pause and resumed restarts', () {
expect(liveTvRefreshTransition(AppLifecycleState.hidden), LiveTvRefreshLifecycleTransition.pause);
expect(liveTvRefreshTransition(AppLifecycleState.paused), LiveTvRefreshLifecycleTransition.pause);
expect(liveTvRefreshTransition(AppLifecycleState.resumed), LiveTvRefreshLifecycleTransition.resume);
});
}
@@ -11,6 +11,7 @@ void main() {
expect(harness.failures, hasLength(1)); expect(harness.failures, hasLength(1));
expect(harness.finished, isTrue); expect(harness.finished, isTrue);
expect(harness.adopted, isFalse); expect(harness.adopted, isFalse);
expect(harness.discarded, isFalse);
}); });
test('reports stream URL lookup failures and finishes retry state', () async { test('reports stream URL lookup failures and finishes retry state', () async {
@@ -21,6 +22,7 @@ void main() {
expect(harness.failures, hasLength(1)); expect(harness.failures, hasLength(1));
expect(harness.finished, isTrue); expect(harness.finished, isTrue);
expect(harness.adopted, isFalse); expect(harness.adopted, isFalse);
expect(harness.discarded, isTrue);
}); });
test('reports player option failures and finishes retry state', () async { test('reports player option failures and finishes retry state', () async {
@@ -31,6 +33,7 @@ void main() {
expect(harness.failures, hasLength(1)); expect(harness.failures, hasLength(1));
expect(harness.finished, isTrue); expect(harness.finished, isTrue);
expect(harness.adopted, isFalse); expect(harness.adopted, isFalse);
expect(harness.discarded, isTrue);
}); });
test('reports player open failures and finishes retry state', () async { test('reports player open failures and finishes retry state', () async {
@@ -41,6 +44,7 @@ void main() {
expect(harness.failures, hasLength(1)); expect(harness.failures, hasLength(1));
expect(harness.finished, isTrue); expect(harness.finished, isTrue);
expect(harness.adopted, isFalse); expect(harness.adopted, isFalse);
expect(harness.discarded, isTrue);
}); });
test('adopts the recovered session after a successful open', () async { test('adopts the recovered session after a successful open', () async {
@@ -52,6 +56,7 @@ void main() {
expect(harness.failures, isEmpty); expect(harness.failures, isEmpty);
expect(harness.finished, isTrue); expect(harness.finished, isTrue);
expect(harness.adopted, isTrue); expect(harness.adopted, isTrue);
expect(harness.discarded, isFalse);
}); });
test('stale operation does not report an async failure', () async { test('stale operation does not report an async failure', () async {
@@ -63,6 +68,7 @@ void main() {
expect(harness.failures, isEmpty); expect(harness.failures, isEmpty);
expect(harness.finished, isTrue); expect(harness.finished, isTrue);
expect(harness.adopted, isFalse); expect(harness.adopted, isFalse);
expect(harness.discarded, isTrue);
}); });
}); });
} }
@@ -79,6 +85,7 @@ class _RetryHarness {
bool current = true; bool current = true;
bool finished = false; bool finished = false;
bool adopted = false; bool adopted = false;
bool discarded = false;
Future<LiveStreamRetryResult> run() => runLiveStreamRetry<Object>( Future<LiveStreamRetryResult> run() => runLiveStreamRetry<Object>(
recover: () => _stage(_Stage.recover, Object.new), recover: () => _stage(_Stage.recover, Object.new),
@@ -88,6 +95,7 @@ class _RetryHarness {
isCurrent: () => current, isCurrent: () => current,
adoptSession: (_) => adopted = true, adoptSession: (_) => adopted = true,
reportFailure: (error, _) => failures.add(error), reportFailure: (error, _) => failures.add(error),
discardSession: (_) => discarded = true,
onFinished: () => finished = true, onFinished: () => finished = true,
); );
@@ -314,5 +314,45 @@ void main() {
expect((byKey['$machineId/user-a:/Users/user-a/Items/item-1']!['UserData'] as Map)['Played'], isTrue); expect((byKey['$machineId/user-a:/Users/user-a/Items/item-1']!['UserData'] as Map)['Played'], isTrue);
expect((byKey['$machineId/user-b:/Users/user-b/Items/item-1']!['UserData'] as Map)['Played'], isFalse); expect((byKey['$machineId/user-b:/Users/user-b/Items/item-1']!['UserData'] as Map)['Played'], isFalse);
}); });
test('bare Jellyfin server id updates the only cached user scope', () async {
const machineId = 'jf-machine';
await putItemRow(
serverId: ServerId(machineId),
userId: 'user-a',
itemId: 'item-1',
data: {
...jellyfinItem(id: 'item-1'),
'UserData': {'Played': false, 'PlayCount': 0},
},
);
await cache.applyWatchState(serverId: ServerId(machineId), itemId: 'item-1', isWatched: true);
final row = await db.select(db.apiCache).getSingle();
expect((jsonDecode(row.data)['UserData'] as Map)['Played'], isTrue);
});
test('bare Jellyfin server id skips ambiguous multi-user cache updates', () async {
const machineId = 'jf-machine';
for (final userId in ['user-a', 'user-b']) {
await putItemRow(
serverId: ServerId(machineId),
userId: userId,
itemId: 'item-1',
data: {
...jellyfinItem(id: 'item-1'),
'UserData': {'Played': false, 'PlayCount': 0},
},
);
}
await cache.applyWatchState(serverId: ServerId(machineId), itemId: 'item-1', isWatched: true);
final rows = await db.select(db.apiCache).get();
for (final row in rows) {
expect((jsonDecode(row.data)['UserData'] as Map)['Played'], isFalse);
}
});
}); });
} }
@@ -1,6 +1,6 @@
import 'dart:convert'; import 'dart:convert';
import 'package:drift/drift.dart' show Value; import 'package:drift/drift.dart' show ApplyInterceptor, QueryExecutor, QueryInterceptor, Value;
import 'package:drift/native.dart'; import 'package:drift/native.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/database/app_database.dart'; import 'package:plezy/database/app_database.dart';
@@ -9,9 +9,11 @@ import 'package:plezy/services/jellyfin_cache_resolver.dart';
void main() { void main() {
late AppDatabase db; late AppDatabase db;
late JellyfinCacheResolver resolver; late JellyfinCacheResolver resolver;
late _SelectCounter selectCounter;
setUp(() { setUp(() {
db = AppDatabase.forTesting(NativeDatabase.memory()); selectCounter = _SelectCounter();
db = AppDatabase.forTesting(NativeDatabase.memory().interceptWith(selectCounter));
resolver = JellyfinCacheResolver(db); resolver = JellyfinCacheResolver(db);
}); });
@@ -130,4 +132,32 @@ void main() {
expect(matches.map((match) => match.connection.id).toSet(), {'server/user-a', 'server/user-b'}); expect(matches.map((match) => match.connection.id).toSet(), {'server/user-a', 'server/user-b'});
}); });
test('pinned resolution uses a constant number of selects as rows grow', () async {
await insertConnection('server', 'user-a', profileId: 'profile-a');
await insertItem('server/user-a', 'user-a', 'item-1', pinned: true);
selectCounter.count = 0;
expect(await resolver.findPinnedItems(), hasLength(1));
final oneRowSelects = selectCounter.count;
for (var i = 2; i <= 100; i++) {
await insertItem('server/user-a', 'user-a', 'item-$i', pinned: true);
}
selectCounter.count = 0;
expect(await resolver.findPinnedItems(), hasLength(100));
expect(selectCounter.count, oneRowSelects);
expect(oneRowSelects, 3);
});
}
class _SelectCounter extends QueryInterceptor {
int count = 0;
@override
Future<List<Map<String, Object?>>> runSelect(QueryExecutor executor, String statement, List<Object?> args) {
count++;
return executor.runSelect(statement, args);
}
} }
@@ -195,6 +195,62 @@ void main() {
expect(m.authErrorServerIds, isNot(contains('server-1'))); expect(m.authErrorServerIds, isNot(contains('server-1')));
expect(client.config.token, 'new-token'); expect(client.config.token, 'new-token');
}); });
test('concurrent Plex account refreshes do not invalidate each other', () async {
final db = AppDatabase.forTesting(NativeDatabase.memory());
PlexApiCache.initialize(db);
addTearDown(db.close);
final manager = MultiServerManager();
addTearDown(manager.dispose);
PlexClient client(String serverId) => PlexClient.forTesting(
config: PlexConfig(
baseUrl: 'https://$serverId.example',
token: 'old-$serverId',
clientIdentifier: 'client-$serverId',
product: 'Plezy',
version: '1.0.0',
),
serverId: ServerId(serverId),
serverName: serverId,
httpClient: MockClient((_) async => http.Response('{}', 200)),
);
final clientA = client('server-a');
final clientB = client('server-b');
manager.debugRegisterClientForTesting(clientA, online: true);
manager.debugRegisterClientForTesting(clientB, online: true);
PlexAccountConnection account(String accountId, String serverId) => PlexAccountConnection(
id: accountId,
accountToken: 'account-token',
clientIdentifier: 'client-$serverId',
accountLabel: accountId,
servers: [
PlexServer(
name: serverId,
clientIdentifier: serverId,
accessToken: 'new-$serverId',
connections: const [],
owned: true,
),
],
createdAt: DateTime.fromMillisecondsSinceEpoch(0),
);
final results = await Future.wait([
manager.refreshTokensForProfile(account('account-a', 'server-a')),
manager.refreshTokensForProfile(account('account-b', 'server-b')),
]);
expect(results, [
{'server-a'},
{'server-b'},
]);
expect(clientA.config.token, 'new-server-a');
expect(clientB.config.token, 'new-server-b');
});
}); });
group('Jellyfin connection updates', () { group('Jellyfin connection updates', () {
+18
View File
@@ -0,0 +1,18 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/i18n/strings.g.dart';
import 'package:plezy/utils/download_utils.dart';
void main() {
setUpAll(() => LocaleSettings.setLocaleSync(AppLocale.en));
test('episode count validation returns a localized message for invalid input', () {
expect(validateEpisodeCountInput('', allowZero: false), t.downloads.invalidEpisodeCount);
expect(validateEpisodeCountInput('0', allowZero: false), t.downloads.invalidEpisodeCount);
expect(validateEpisodeCountInput('not-a-number', allowZero: true), t.downloads.invalidEpisodeCount);
});
test('episode count validation accepts zero only when requested', () {
expect(validateEpisodeCountInput('0', allowZero: true), isNull);
expect(validateEpisodeCountInput('12', allowZero: false), isNull);
});
}
+39
View File
@@ -0,0 +1,39 @@
import 'dart:async';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/utils/serial_future_queue.dart';
void main() {
test('a failed operation does not poison the next queued operation', () async {
final queue = SerialFutureQueue();
await expectLater(queue.run<void>(() async => throw StateError('failed')), throwsStateError);
expect(await queue.run(() async => 42), 42);
await queue.settled;
});
test('operations remain serialized while callers receive their own results', () async {
final queue = SerialFutureQueue();
final firstStarted = Completer<void>();
final releaseFirst = Completer<void>();
var secondStarted = false;
final first = queue.run(() async {
firstStarted.complete();
await releaseFirst.future;
return 'first';
});
final second = queue.run(() async {
secondStarted = true;
return 'second';
});
await firstStarted.future;
await Future<void>.delayed(Duration.zero);
expect(secondStarted, isFalse);
releaseFirst.complete();
expect(await first, 'first');
expect(await second, 'second');
});
}
@@ -0,0 +1,72 @@
import 'dart:async';
import 'package:drift/native.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/providers/download_provider.dart';
import 'package:plezy/services/download_manager_service.dart';
import 'package:plezy/services/download_storage_service.dart';
import 'package:plezy/services/plex_api_cache.dart';
import 'package:plezy/utils/smart_deletion_handler.dart';
import 'package:provider/provider.dart';
void main() {
testWidgets('completion before the first dialog frame removes the exact pending route', (tester) async {
final database = AppDatabase.forTesting(NativeDatabase.memory());
PlexApiCache.initialize(database);
final manager = DownloadManagerService(
database: database,
storageService: DownloadStorageService.instance,
clientResolver: (_, {clientScopeId}) => null,
)..recoveryFuture = Future<void>.value();
final provider = _GatedDeletionProvider(manager, database);
await provider.ensureInitialized();
addTearDown(() async {
provider.dispose();
manager.dispose();
await database.close();
});
late BuildContext actionContext;
await tester.pumpWidget(
ChangeNotifierProvider<DownloadProvider>.value(
value: provider,
child: MaterialApp(
home: Builder(
builder: (context) {
actionContext = context;
return const Scaffold(body: Text('underlying page'));
},
),
),
),
);
final deletion = SmartDeletionHandler.deleteWithProgress(
context: actionContext,
provider: provider,
globalKey: 'srv:item',
delayMs: 0,
);
await tester.pump();
provider.completeDeletion();
await deletion;
await tester.pump();
expect(find.byType(AlertDialog), findsNothing);
expect(find.text('underlying page'), findsOneWidget);
});
}
class _GatedDeletionProvider extends DownloadProvider {
_GatedDeletionProvider(DownloadManagerService manager, AppDatabase database)
: super.forTesting(downloadManager: manager, database: database);
final Completer<void> _deletion = Completer<void>();
@override
Future<void> deleteDownload(String globalKey) => _deletion.future;
void completeDeletion() => _deletion.complete();
}
@@ -0,0 +1,34 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/i18n/strings.g.dart';
import 'package:plezy/services/base_peer_service.dart';
import 'package:plezy/widgets/companion_remote/discovery_view.dart';
void main() {
setUpAll(() => LocaleSettings.setLocaleSync(AppLocale.en));
test('typed peer errors are classified without parsing localized text', () {
expect(
companionRemotePairingErrorMessage(const PeerError(type: PeerErrorType.timeout, message: 'Délai dépassé')),
t.companionRemote.pairing.connectionTimedOut,
);
expect(
companionRemotePairingErrorMessage(const PeerError(type: PeerErrorType.invalidSession, message: 'Sitzung fehlt')),
t.companionRemote.pairing.sessionNotFound,
);
expect(
companionRemotePairingErrorMessage(
const PeerError(type: PeerErrorType.authFailed, message: 'Échec de lauthentification'),
),
t.companionRemote.pairing.authFailed,
);
});
test('typed fallback errors preserve their localized producer message', () {
expect(
companionRemotePairingErrorMessage(
const PeerError(type: PeerErrorType.networkError, message: 'Localized network failure'),
),
'Localized network failure',
);
});
}
@@ -1,3 +1,6 @@
import 'dart:convert';
import 'dart:io';
import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:cached_network_image_ce/cached_network_image.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@@ -64,4 +67,49 @@ void main() {
expect(placeholder, findsOneWidget); expect(placeholder, findsOneWidget);
expect(tester.getSize(placeholder), const Size(96, 96)); expect(tester.getSize(placeholder), const Size(96, 96));
}); });
testWidgets('same local artwork path re-resolves after the file appears', (tester) async {
final directory = Directory.systemTemp.createTempSync('plezy-image-test');
addTearDown(() => directory.deleteSync(recursive: true));
final file = File('${directory.path}/poster.png');
late StateSetter rebuild;
await tester.pumpWidget(
MaterialApp(
home: StatefulBuilder(
builder: (context, setState) {
rebuild = setState;
return OptimizedMediaImage.thumb(
imagePath: null,
localFilePath: file.path,
width: 80,
height: 120,
fallbackIcon: Symbols.image_not_supported_rounded,
);
},
),
),
);
expect(find.byIcon(Symbols.image_not_supported_rounded), findsNothing);
await tester.runAsync(() => Future<void>.delayed(Duration.zero));
await tester.pump();
expect(find.byIcon(Symbols.image_not_supported_rounded), findsOneWidget);
file.writeAsBytesSync(
base64Decode('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='),
flush: true,
);
rebuild(() {});
await tester.pump();
expect(find.byIcon(Symbols.image_not_supported_rounded), findsNothing);
await tester.runAsync(() => Future<void>.delayed(const Duration(milliseconds: 20)));
await tester.pump();
await tester.pump();
expect(file.existsSync(), isTrue);
expect(find.byIcon(Symbols.image_not_supported_rounded), findsNothing);
expect(find.byType(Image), findsOneWidget);
await tester.pumpWidget(const SizedBox.shrink());
});
} }
@@ -3,6 +3,7 @@
// be found in the LICENSE file. // be found in the LICENSE file.
import Flutter import Flutter
import UIKit
public class ConnectivityPlusPlugin: NSObject, FlutterPlugin, FlutterStreamHandler { public class ConnectivityPlusPlugin: NSObject, FlutterPlugin, FlutterStreamHandler {
private let connectivityProvider: ConnectivityProvider private let connectivityProvider: ConnectivityProvider
@@ -79,8 +80,12 @@ public class ConnectivityPlusPlugin: NSObject, FlutterPlugin, FlutterStreamHandl
} }
private func connectivityUpdateHandler(connectivityTypes: [ConnectivityType]) { private func connectivityUpdateHandler(connectivityTypes: [ConnectivityType]) {
DispatchQueue.main.async { DispatchQueue.main.async { [weak self] in
self.eventSink?(self.statusFrom(connectivityTypes: connectivityTypes)) guard let self = self, let eventSink = self.eventSink else { return }
// NWPathMonitor can emit after the FlutterEngine shell is torn down.
// Do not call its event sink while tvOS is backgrounded.
guard UIApplication.shared.applicationState != .background else { return }
eventSink(self.statusFrom(connectivityTypes: connectivityTypes))
} }
} }