fix: prevent async state and platform regressions
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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} негледани",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}件保持",
|
||||||
|
|||||||
@@ -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}개 유지",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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} непросмотренных",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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} негледани',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}件保持',
|
||||||
|
|||||||
@@ -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}개 유지',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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} непросмотренных',
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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}个未观看',
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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}个未观看",
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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>;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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':
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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)"
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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', () {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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 l’authentification'),
|
||||||
|
),
|
||||||
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user