fix(delete): name the delete target and verify what its files back

"Delete from server" read identically for an episode, a season and a
whole show: same menu label, same dialog title, same red button, and a
body that named nothing. The menu header did not disambiguate either,
because MediaItem.displayTitle collapses an episode to its show name.
A reporter deleted a whole series from the detail hero's ⋮ believing it
acted on the episode he had highlighted, and the confirmation gave him
nothing to catch it with. Every one of those strings now names the kind,
and the body names the exact item — show, season and episode number, and
episode title.

Deleting a single item also destroyed files the confirmation never
mentioned: a Plex multi-episode file (S01E01-E03.mkv) takes its other
episodes with it, and a split item takes every part. The dialog now
reports that up front and, on success, emits deletion events for the
siblings the server destroyed so their rows do not linger.

The scope behind that warning is only asserted when it is established.
MediaItem.allPartFiles drops parts with no path, so a non-empty set
proves nothing about the ones it filtered out; a version is trusted only
when every part reports a file. A browse row that omits paths is missing
evidence rather than proof of a distinct file, so both the target and
each candidate sibling fall back to the detail endpoint before any
conclusion — otherwise a thin row, including the file-less part
PlexMappers fabricates for an empty payload, would look like a server
that withholds paths. When the answer cannot be established the dialog
says so in an error-tinted block and its button reads "Delete anyway",
separating a transient probe failure from a server that never sends
paths. It deliberately does not refuse: Plex withholds paths from
restricted users the server itself authorizes to delete, so failing
closed would take the feature away from them permanently.

Probing a season stays bounded in both directions. Siblings resolve one
at a time, so a season of thin rows cannot fan out a detail request per
episode, and expiry cancels the walk rather than merely abandoning it —
`Future.timeout` completes the future the caller awaits but leaves the
work behind it running, which would resume on the next sibling once the
outstanding request answered. A cooperative flag is checked before each
lookup, so at most the one already in flight outlives the deadline; the
neutral client exposes no abort handle for item lookups, so that one
cannot be recalled.

The spinner covering the probe was only barrierDismissible, which does
not stop system back. Back dismissed it and the cleanup pop then closed
the screen underneath, dropping the user out of the detail page
mid-flow. It now traps back, matching the non-dismissible contract its
own doc claims, which also repairs the log uploader and the file-info
sheet.

Coverage splits by what each layer owns. The dialog, its copy and the
DELETE wiring are backend-neutral and stay in the menu widget tests.
Plex — the backend multi-episode files actually come from — gets the
resolver over a real PlexClient and a mocked transport: a row with no
media at all, scope recovered from /library/metadata/{id}, siblings and
paths from /children, a Part that names no file, a sibling whose path
never resolves, the request count a sixty-episode thin season may cost,
and the rating key the DELETE carries. Those are plain async tests
because the Plex metadata cache is a real database whose I/O the widget
tester's fake clock never drives. Deadline behaviour needs the opposite,
so it is pinned separately under fakeAsync against a gated fake client,
with no wall-clock waiting anywhere.

close #1781
This commit is contained in:
edde746
2026-08-06 03:45:08 +02:00
parent 9d51a040c3
commit 1b6a811c07
52 changed files with 2278 additions and 259 deletions
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Qarışıq oynat", "shufflePlay": "Qarışıq oynat",
"shuffleNotAvailableOffline": "Qarışıq oynatma oflayn rejimdə əlçatan deyil", "shuffleNotAvailableOffline": "Qarışıq oynatma oflayn rejimdə əlçatan deyil",
"fileInfo": "Fayl məlumatı", "fileInfo": "Fayl məlumatı",
"deleteFromServer": "Serverdən sil", "deleteEpisodeFromServer": "",
"confirmDelete": "Bu media və faylları serverinizdən silinsin?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Bu bütün seriyaları və faylları əhatə edir.", "deleteMultipleWarning": "Bu bütün seriyaları və faylları əhatə edir.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Media elementi uğurla silindi", "mediaDeletedSuccessfully": "Media elementi uğurla silindi",
"mediaFailedToDelete": "Media elementi silinə bilmədi", "mediaFailedToDelete": "Media elementi silinə bilmədi",
"rate": "Qiymətləndir", "rate": "Qiymətləndir",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Разбъркано възпроизвеждане", "shufflePlay": "Разбъркано възпроизвеждане",
"shuffleNotAvailableOffline": "Разбърканото възпроизвеждане не е налично офлайн", "shuffleNotAvailableOffline": "Разбърканото възпроизвеждане не е налично офлайн",
"fileInfo": "Информация за файла", "fileInfo": "Информация за файла",
"deleteFromServer": "Изтрий от сървъра", "deleteEpisodeFromServer": "",
"confirmDelete": "Да се изтрият ли този елемент и файловете му от вашия сървър?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Това включва всички епизоди и техните файлове.", "deleteMultipleWarning": "Това включва всички епизоди и техните файлове.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Елементът е изтрит успешно", "mediaDeletedSuccessfully": "Елементът е изтрит успешно",
"mediaFailedToDelete": "Неуспешно изтриване на елемента", "mediaFailedToDelete": "Неуспешно изтриване на елемента",
"rate": "Оцени", "rate": "Оцени",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Afspil tilfældigt", "shufflePlay": "Afspil tilfældigt",
"shuffleNotAvailableOffline": "Tilfældig afspilning er ikke tilgængelig offline", "shuffleNotAvailableOffline": "Tilfældig afspilning er ikke tilgængelig offline",
"fileInfo": "Filinfo", "fileInfo": "Filinfo",
"deleteFromServer": "Slet fra server", "deleteEpisodeFromServer": "",
"confirmDelete": "Slet dette medie og dets filer fra din server?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Dette inkluderer alle episoder og deres filer.", "deleteMultipleWarning": "Dette inkluderer alle episoder og deres filer.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Mediet blev slettet", "mediaDeletedSuccessfully": "Mediet blev slettet",
"mediaFailedToDelete": "Mediet kunne ikke slettes", "mediaFailedToDelete": "Mediet kunne ikke slettes",
"rate": "Bedøm", "rate": "Bedøm",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Zufallswiedergabe", "shufflePlay": "Zufallswiedergabe",
"shuffleNotAvailableOffline": "Zufallswiedergabe ist offline nicht verfügbar", "shuffleNotAvailableOffline": "Zufallswiedergabe ist offline nicht verfügbar",
"fileInfo": "Dateiinfo", "fileInfo": "Dateiinfo",
"deleteFromServer": "Vom Server löschen", "deleteEpisodeFromServer": "",
"confirmDelete": "Dieses Medium und seine Dateien von deinem Server löschen?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Dies umfasst alle Episoden und deren Dateien.", "deleteMultipleWarning": "Dies umfasst alle Episoden und deren Dateien.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Medienelement gelöscht", "mediaDeletedSuccessfully": "Medienelement gelöscht",
"mediaFailedToDelete": "Medienelement konnte nicht gelöscht werden", "mediaFailedToDelete": "Medienelement konnte nicht gelöscht werden",
"rate": "Bewerten", "rate": "Bewerten",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Shuffle Play", "shufflePlay": "Shuffle Play",
"shuffleNotAvailableOffline": "Shuffle not available offline", "shuffleNotAvailableOffline": "Shuffle not available offline",
"fileInfo": "File Info", "fileInfo": "File Info",
"deleteFromServer": "Delete from server", "deleteEpisodeFromServer": "Delete episode from server",
"confirmDelete": "Delete this media and its files from your server?", "deleteSeasonFromServer": "Delete season from server",
"deleteShowFromServer": "Delete show from server",
"deleteMovieFromServer": "Delete movie from server",
"deleteEpisodeTitle": "Delete this episode?",
"deleteSeasonTitle": "Delete this season?",
"deleteShowTitle": "Delete this show?",
"deleteMovieTitle": "Delete this movie?",
"deleteEpisodeConfirm": "Delete episode",
"deleteSeasonConfirm": "Delete season",
"deleteShowConfirm": "Delete show",
"deleteMovieConfirm": "Delete movie",
"deleteAnyway": "Delete anyway",
"confirmDeleteTarget": "Permanently delete ${title} from your server?",
"deleteMultipleWarning": "This includes all episodes and their files.", "deleteMultipleWarning": "This includes all episodes and their files.",
"deleteEpisodeCountWarning": {
"one": "This deletes all ${n} episode in it, and its file.",
"other": "This deletes all ${n} episodes in it, and their files."
},
"deleteMultiPartWarning": {
"one": "This item is stored as ${n} file, which will be deleted.",
"other": "This item is stored across ${n} files, and all of them will be deleted."
},
"deleteSharedFileHeading": {
"one": "${n} other episode is stored in the same file and will be deleted too:",
"other": "${n} other episodes are stored in the same file and will be deleted too:"
},
"deleteScopeUnverifiedProbeFailed": "Plezy could not check which files this will remove, so it may delete more than the item named above. Cancel and try again, or delete anyway.",
"deleteScopeUnverifiedNoFileInfo": "Your server did not provide file details for this item, so Plezy cannot check which files this will remove. It may delete more than the item named above.",
"mediaDeletedSuccessfully": "Media item deleted successfully", "mediaDeletedSuccessfully": "Media item deleted successfully",
"mediaFailedToDelete": "Failed to delete media item", "mediaFailedToDelete": "Failed to delete media item",
"rate": "Rate", "rate": "Rate",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Reproducción aleatoria", "shufflePlay": "Reproducción aleatoria",
"shuffleNotAvailableOffline": "La reproducción aleatoria no está disponible sin conexión", "shuffleNotAvailableOffline": "La reproducción aleatoria no está disponible sin conexión",
"fileInfo": "Información del archivo", "fileInfo": "Información del archivo",
"deleteFromServer": "Eliminar del servidor", "deleteEpisodeFromServer": "",
"confirmDelete": "¿Eliminar este medio y sus archivos de tu servidor?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Esto incluye todos los episodios y sus archivos.", "deleteMultipleWarning": "Esto incluye todos los episodios y sus archivos.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Elemento multimedia eliminado con éxito", "mediaDeletedSuccessfully": "Elemento multimedia eliminado con éxito",
"mediaFailedToDelete": "Error al eliminar el elemento multimedia", "mediaFailedToDelete": "Error al eliminar el elemento multimedia",
"rate": "Calificar", "rate": "Calificar",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Lecture aléatoire", "shufflePlay": "Lecture aléatoire",
"shuffleNotAvailableOffline": "La lecture aléatoire nest pas disponible hors ligne", "shuffleNotAvailableOffline": "La lecture aléatoire nest pas disponible hors ligne",
"fileInfo": "Informations sur le fichier", "fileInfo": "Informations sur le fichier",
"deleteFromServer": "Supprimer du serveur", "deleteEpisodeFromServer": "",
"confirmDelete": "Supprimer ce média et ses fichiers de votre serveur ?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Cela inclut tous les épisodes et leurs fichiers.", "deleteMultipleWarning": "Cela inclut tous les épisodes et leurs fichiers.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Élément média supprimé avec succès", "mediaDeletedSuccessfully": "Élément média supprimé avec succès",
"mediaFailedToDelete": "Échec de la suppression de l'élément média", "mediaFailedToDelete": "Échec de la suppression de l'élément média",
"rate": "Noter", "rate": "Noter",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Véletlenszerű lejátszás", "shufflePlay": "Véletlenszerű lejátszás",
"shuffleNotAvailableOffline": "A véletlenszerű lejátszás nem érhető el offline", "shuffleNotAvailableOffline": "A véletlenszerű lejátszás nem érhető el offline",
"fileInfo": "Fájlinformáció", "fileInfo": "Fájlinformáció",
"deleteFromServer": "Törlés a szerverről", "deleteEpisodeFromServer": "",
"confirmDelete": "Törlöd ezt a médiát és a fájljait a szerveredről?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Ez magában foglalja az összes epizódot és azok fájljait.", "deleteMultipleWarning": "Ez magában foglalja az összes epizódot és azok fájljait.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Médiaelem sikeresen törölve", "mediaDeletedSuccessfully": "Médiaelem sikeresen törölve",
"mediaFailedToDelete": "Nem sikerült a médiaelem törlése", "mediaFailedToDelete": "Nem sikerült a médiaelem törlése",
"rate": "Értékelés", "rate": "Értékelés",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Riproduzione casuale", "shufflePlay": "Riproduzione casuale",
"shuffleNotAvailableOffline": "Riproduzione casuale non disponibile offline", "shuffleNotAvailableOffline": "Riproduzione casuale non disponibile offline",
"fileInfo": "Info sul file", "fileInfo": "Info sul file",
"deleteFromServer": "Elimina dal server", "deleteEpisodeFromServer": "",
"confirmDelete": "Eliminare questo media e i suoi file dal server?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Sono inclusi tutti gli episodi e i relativi file.", "deleteMultipleWarning": "Sono inclusi tutti gli episodi e i relativi file.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Elemento multimediale eliminato correttamente", "mediaDeletedSuccessfully": "Elemento multimediale eliminato correttamente",
"mediaFailedToDelete": "Impossibile eliminare l'elemento multimediale", "mediaFailedToDelete": "Impossibile eliminare l'elemento multimediale",
"rate": "Valuta", "rate": "Valuta",
+25 -2
View File
@@ -518,9 +518,32 @@
"shufflePlay": "シャッフル再生", "shufflePlay": "シャッフル再生",
"shuffleNotAvailableOffline": "オフラインではシャッフルを利用できません", "shuffleNotAvailableOffline": "オフラインではシャッフルを利用できません",
"fileInfo": "ファイル情報", "fileInfo": "ファイル情報",
"deleteFromServer": "サーバーから削除", "deleteEpisodeFromServer": "",
"confirmDelete": "このメディアとそのファイルをサーバーから削除しますか?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "すべてのエピソードとそのファイルが含まれます。", "deleteMultipleWarning": "すべてのエピソードとそのファイルが含まれます。",
"deleteEpisodeCountWarning": {
"other": ""
},
"deleteMultiPartWarning": {
"other": ""
},
"deleteSharedFileHeading": {
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "メディアアイテムを正常に削除しました", "mediaDeletedSuccessfully": "メディアアイテムを正常に削除しました",
"mediaFailedToDelete": "メディアアイテムの削除に失敗しました", "mediaFailedToDelete": "メディアアイテムの削除に失敗しました",
"rate": "評価", "rate": "評価",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Араластырып ойнату", "shufflePlay": "Араластырып ойнату",
"shuffleNotAvailableOffline": "Араластырып ойнату офлайн режимде қолжетімсіз", "shuffleNotAvailableOffline": "Араластырып ойнату офлайн режимде қолжетімсіз",
"fileInfo": "Файл туралы ақпарат", "fileInfo": "Файл туралы ақпарат",
"deleteFromServer": "Серверден өшіру", "deleteEpisodeFromServer": "",
"confirmDelete": "Осы медиа мен файлдар серверден өшірілсін бе?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Бұл барлық бөлімдер мен файлдарға әсер етеді.", "deleteMultipleWarning": "Бұл барлық бөлімдер мен файлдарға әсер етеді.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Медиа элементі сәтті өшірілді", "mediaDeletedSuccessfully": "Медиа элементі сәтті өшірілді",
"mediaFailedToDelete": "Медиа элементін өшіру мүмкін болмады", "mediaFailedToDelete": "Медиа элементін өшіру мүмкін болмады",
"rate": "Бағалау", "rate": "Бағалау",
+25 -2
View File
@@ -518,9 +518,32 @@
"shufflePlay": "무작위 재생", "shufflePlay": "무작위 재생",
"shuffleNotAvailableOffline": "오프라인에서는 무작위 재생을 사용할 수 없습니다", "shuffleNotAvailableOffline": "오프라인에서는 무작위 재생을 사용할 수 없습니다",
"fileInfo": "파일 정보", "fileInfo": "파일 정보",
"deleteFromServer": "서버에서 삭제", "deleteEpisodeFromServer": "",
"confirmDelete": "이 미디어와 해당 파일을 서버에서 삭제할까요?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "모든 에피소드와 파일이 포함됩니다.", "deleteMultipleWarning": "모든 에피소드와 파일이 포함됩니다.",
"deleteEpisodeCountWarning": {
"other": ""
},
"deleteMultiPartWarning": {
"other": ""
},
"deleteSharedFileHeading": {
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "미디어 항목이 성공적으로 삭제되었습니다", "mediaDeletedSuccessfully": "미디어 항목이 성공적으로 삭제되었습니다",
"mediaFailedToDelete": "미디어 항목 삭제 실패", "mediaFailedToDelete": "미디어 항목 삭제 실패",
"rate": "평가", "rate": "평가",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Tilfeldig avspilling", "shufflePlay": "Tilfeldig avspilling",
"shuffleNotAvailableOffline": "Tilfeldig avspilling er ikke tilgjengelig uten nett", "shuffleNotAvailableOffline": "Tilfeldig avspilling er ikke tilgjengelig uten nett",
"fileInfo": "Filinformasjon", "fileInfo": "Filinformasjon",
"deleteFromServer": "Slett fra server", "deleteEpisodeFromServer": "",
"confirmDelete": "Slette dette mediet og filene fra serveren?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Dette inkluderer alle episoder og deres filer.", "deleteMultipleWarning": "Dette inkluderer alle episoder og deres filer.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Medieelement slettet", "mediaDeletedSuccessfully": "Medieelement slettet",
"mediaFailedToDelete": "Kunne ikke slette medieelement", "mediaFailedToDelete": "Kunne ikke slette medieelement",
"rate": "Vurder", "rate": "Vurder",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Willekeurig afspelen", "shufflePlay": "Willekeurig afspelen",
"shuffleNotAvailableOffline": "Willekeurig afspelen is offline niet beschikbaar", "shuffleNotAvailableOffline": "Willekeurig afspelen is offline niet beschikbaar",
"fileInfo": "Bestandsinformatie", "fileInfo": "Bestandsinformatie",
"deleteFromServer": "Van server verwijderen", "deleteEpisodeFromServer": "",
"confirmDelete": "Deze media en bestanden van je server verwijderen?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Dit omvat alle afleveringen en hun bestanden.", "deleteMultipleWarning": "Dit omvat alle afleveringen en hun bestanden.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Media-item succesvol verwijderd", "mediaDeletedSuccessfully": "Media-item succesvol verwijderd",
"mediaFailedToDelete": "Verwijderen van media-item mislukt", "mediaFailedToDelete": "Verwijderen van media-item mislukt",
"rate": "Beoordelen", "rate": "Beoordelen",
+34 -2
View File
@@ -518,9 +518,41 @@
"shufflePlay": "Odtwarzanie losowe", "shufflePlay": "Odtwarzanie losowe",
"shuffleNotAvailableOffline": "Odtwarzanie losowe nie jest dostępne offline", "shuffleNotAvailableOffline": "Odtwarzanie losowe nie jest dostępne offline",
"fileInfo": "Informacje o pliku", "fileInfo": "Informacje o pliku",
"deleteFromServer": "Usuń z serwera", "deleteEpisodeFromServer": "",
"confirmDelete": "Usunąć to medium i jego pliki z serwera?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Obejmuje to wszystkie odcinki i ich pliki.", "deleteMultipleWarning": "Obejmuje to wszystkie odcinki i ich pliki.",
"deleteEpisodeCountWarning": {
"one": "",
"few": "",
"many": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"few": "",
"many": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"few": "",
"many": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Usunięto element multimedialny", "mediaDeletedSuccessfully": "Usunięto element multimedialny",
"mediaFailedToDelete": "Nie udało się usunąć elementu multimedialnego", "mediaFailedToDelete": "Nie udało się usunąć elementu multimedialnego",
"rate": "Oceń", "rate": "Oceń",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Reprodução Aleatória", "shufflePlay": "Reprodução Aleatória",
"shuffleNotAvailableOffline": "Reprodução aleatória indisponível offline", "shuffleNotAvailableOffline": "Reprodução aleatória indisponível offline",
"fileInfo": "Informações do arquivo", "fileInfo": "Informações do arquivo",
"deleteFromServer": "Excluir do servidor", "deleteEpisodeFromServer": "",
"confirmDelete": "Excluir esta mídia e seus arquivos do servidor?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Isso inclui todos os episódios e seus arquivos.", "deleteMultipleWarning": "Isso inclui todos os episódios e seus arquivos.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Item de mídia excluído com sucesso", "mediaDeletedSuccessfully": "Item de mídia excluído com sucesso",
"mediaFailedToDelete": "Falha ao excluir item de mídia", "mediaFailedToDelete": "Falha ao excluir item de mídia",
"rate": "Avaliar", "rate": "Avaliar",
+34 -2
View File
@@ -518,9 +518,41 @@
"shufflePlay": "Случайное воспроизведение", "shufflePlay": "Случайное воспроизведение",
"shuffleNotAvailableOffline": "Перемешивание недоступно офлайн", "shuffleNotAvailableOffline": "Перемешивание недоступно офлайн",
"fileInfo": "Информация о файле", "fileInfo": "Информация о файле",
"deleteFromServer": "Удалить с сервера", "deleteEpisodeFromServer": "",
"confirmDelete": "Удалить это медиа и его файлы с сервера?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Это включает все эпизоды и их файлы.", "deleteMultipleWarning": "Это включает все эпизоды и их файлы.",
"deleteEpisodeCountWarning": {
"one": "",
"few": "",
"many": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"few": "",
"many": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"few": "",
"many": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Медиаэлемент успешно удалён", "mediaDeletedSuccessfully": "Медиаэлемент успешно удалён",
"mediaFailedToDelete": "Не удалось удалить медиаэлемент", "mediaFailedToDelete": "Не удалось удалить медиаэлемент",
"rate": "Оценить", "rate": "Оценить",
+1 -1
View File
@@ -4,7 +4,7 @@
/// To regenerate, run: `dart run slang` /// To regenerate, run: `dart run slang`
/// ///
/// Locales: 22 /// Locales: 22
/// Strings: 32781 (1490 per locale) /// Strings: 32759 (1489 per locale)
// coverage:ignore-file // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // ignore_for_file: type=lint, unused_import
+4 -8
View File
@@ -549,8 +549,6 @@ class _Translations$mediaMenu$az extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Qarışıq oynat'; @override String get shufflePlay => 'Qarışıq oynat';
@override String get shuffleNotAvailableOffline => 'Qarışıq oynatma oflayn rejimdə əlçatan deyil'; @override String get shuffleNotAvailableOffline => 'Qarışıq oynatma oflayn rejimdə əlçatan deyil';
@override String get fileInfo => 'Fayl məlumatı'; @override String get fileInfo => 'Fayl məlumatı';
@override String get deleteFromServer => 'Serverdən sil';
@override String get confirmDelete => 'Bu media və faylları serverinizdən silinsin?';
@override String get deleteMultipleWarning => 'Bu bütün seriyaları və faylları əhatə edir.'; @override String get deleteMultipleWarning => 'Bu bütün seriyaları və faylları əhatə edir.';
@override String get mediaDeletedSuccessfully => 'Media elementi uğurla silindi'; @override String get mediaDeletedSuccessfully => 'Media elementi uğurla silindi';
@override String get mediaFailedToDelete => 'Media elementi silinə bilmədi'; @override String get mediaFailedToDelete => 'Media elementi silinə bilmədi';
@@ -2597,8 +2595,6 @@ extension on TranslationsAz {
'mediaMenu.shufflePlay' => 'Qarışıq oynat', 'mediaMenu.shufflePlay' => 'Qarışıq oynat',
'mediaMenu.shuffleNotAvailableOffline' => 'Qarışıq oynatma oflayn rejimdə əlçatan deyil', 'mediaMenu.shuffleNotAvailableOffline' => 'Qarışıq oynatma oflayn rejimdə əlçatan deyil',
'mediaMenu.fileInfo' => 'Fayl məlumatı', 'mediaMenu.fileInfo' => 'Fayl məlumatı',
'mediaMenu.deleteFromServer' => 'Serverdən sil',
'mediaMenu.confirmDelete' => 'Bu media və faylları serverinizdən silinsin?',
'mediaMenu.deleteMultipleWarning' => 'Bu bütün seriyaları və faylları əhatə edir.', 'mediaMenu.deleteMultipleWarning' => 'Bu bütün seriyaları və faylları əhatə edir.',
'mediaMenu.mediaDeletedSuccessfully' => 'Media elementi uğurla silindi', 'mediaMenu.mediaDeletedSuccessfully' => 'Media elementi uğurla silindi',
'mediaMenu.mediaFailedToDelete' => 'Media elementi silinə bilmədi', 'mediaMenu.mediaFailedToDelete' => 'Media elementi silinə bilmədi',
@@ -2714,10 +2710,10 @@ extension on TranslationsAz {
'videoControls.queue' => 'Növbə', 'videoControls.queue' => 'Növbə',
'videoControls.noQueueItems' => 'Növbədə element yoxdur', 'videoControls.noQueueItems' => 'Növbədə element yoxdur',
'videoControls.searchSubtitles' => 'Altyazı axtar', 'videoControls.searchSubtitles' => 'Altyazı axtar',
_ => null,
} ?? switch (path) {
'videoControls.language' => 'Dil', 'videoControls.language' => 'Dil',
'videoControls.noSubtitlesFound' => 'Altyazı tapılmadı', 'videoControls.noSubtitlesFound' => 'Altyazı tapılmadı',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Altyazı yükləndi', 'videoControls.subtitleDownloaded' => 'Altyazı yükləndi',
'videoControls.subtitleDownloadedNotApplied' => 'Altyazı yükləndi, lakin seçilə bilmədi', 'videoControls.subtitleDownloadedNotApplied' => 'Altyazı yükləndi, lakin seçilə bilmədi',
'videoControls.subtitleDownloadFailed' => 'Altyazı yüklənə bilmədi', 'videoControls.subtitleDownloadFailed' => 'Altyazı yüklənə bilmədi',
@@ -3228,10 +3224,10 @@ extension on TranslationsAz {
'watchTogether.pasteFromClipboard' => 'Buferdən yapışdır', 'watchTogether.pasteFromClipboard' => 'Buferdən yapışdır',
'watchTogether.pleaseEnterCode' => 'Lütfən seans kodunu daxil edin', 'watchTogether.pleaseEnterCode' => 'Lütfən seans kodunu daxil edin',
'watchTogether.codeMustBe5Chars' => 'Seans kodu 5 simvol olmalıdır', 'watchTogether.codeMustBe5Chars' => 'Seans kodu 5 simvol olmalıdır',
_ => null,
} ?? switch (path) {
'watchTogether.joinInstructions' => 'Qoşulmaq üçün təşkilatçının seans kodunu daxil edin.', 'watchTogether.joinInstructions' => 'Qoşulmaq üçün təşkilatçının seans kodunu daxil edin.',
'watchTogether.failedToCreate' => 'Seans yaradıla bilmədi', 'watchTogether.failedToCreate' => 'Seans yaradıla bilmədi',
_ => null,
} ?? switch (path) {
'watchTogether.failedToJoin' => 'Seansa qoşuluna bilmədi', 'watchTogether.failedToJoin' => 'Seansa qoşuluna bilmədi',
'watchTogether.sessionCodeCopied' => 'Seans kodu buferə kopyalandı', 'watchTogether.sessionCodeCopied' => 'Seans kodu buferə kopyalandı',
'watchTogether.relayUnreachable' => 'Rele serverinə çatmaq olmur. İnternet provayderinin bloklaması Birlikdə İzləməyə mane ola bilər.', 'watchTogether.relayUnreachable' => 'Rele serverinə çatmaq olmur. İnternet provayderinin bloklaması Birlikdə İzləməyə mane ola bilər.',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$bg extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Разбъркано възпроизвеждане'; @override String get shufflePlay => 'Разбъркано възпроизвеждане';
@override String get shuffleNotAvailableOffline => 'Разбърканото възпроизвеждане не е налично офлайн'; @override String get shuffleNotAvailableOffline => 'Разбърканото възпроизвеждане не е налично офлайн';
@override String get fileInfo => 'Информация за файла'; @override String get fileInfo => 'Информация за файла';
@override String get deleteFromServer => 'Изтрий от сървъра';
@override String get confirmDelete => 'Да се изтрият ли този елемент и файловете му от вашия сървър?';
@override String get deleteMultipleWarning => 'Това включва всички епизоди и техните файлове.'; @override String get deleteMultipleWarning => 'Това включва всички епизоди и техните файлове.';
@override String get mediaDeletedSuccessfully => 'Елементът е изтрит успешно'; @override String get mediaDeletedSuccessfully => 'Елементът е изтрит успешно';
@override String get mediaFailedToDelete => 'Неуспешно изтриване на елемента'; @override String get mediaFailedToDelete => 'Неуспешно изтриване на елемента';
@@ -2583,8 +2581,6 @@ extension on TranslationsBg {
'mediaMenu.shufflePlay' => 'Разбъркано възпроизвеждане', 'mediaMenu.shufflePlay' => 'Разбъркано възпроизвеждане',
'mediaMenu.shuffleNotAvailableOffline' => 'Разбърканото възпроизвеждане не е налично офлайн', 'mediaMenu.shuffleNotAvailableOffline' => 'Разбърканото възпроизвеждане не е налично офлайн',
'mediaMenu.fileInfo' => 'Информация за файла', 'mediaMenu.fileInfo' => 'Информация за файла',
'mediaMenu.deleteFromServer' => 'Изтрий от сървъра',
'mediaMenu.confirmDelete' => 'Да се изтрият ли този елемент и файловете му от вашия сървър?',
'mediaMenu.deleteMultipleWarning' => 'Това включва всички епизоди и техните файлове.', 'mediaMenu.deleteMultipleWarning' => 'Това включва всички епизоди и техните файлове.',
'mediaMenu.mediaDeletedSuccessfully' => 'Елементът е изтрит успешно', 'mediaMenu.mediaDeletedSuccessfully' => 'Елементът е изтрит успешно',
'mediaMenu.mediaFailedToDelete' => 'Неуспешно изтриване на елемента', 'mediaMenu.mediaFailedToDelete' => 'Неуспешно изтриване на елемента',
@@ -2703,10 +2699,10 @@ extension on TranslationsBg {
'videoControls.language' => 'Език', 'videoControls.language' => 'Език',
'videoControls.noSubtitlesFound' => 'Не са намерени субтитри', 'videoControls.noSubtitlesFound' => 'Не са намерени субтитри',
'videoControls.subtitleDownloaded' => 'Субтитърът е изтеглен', 'videoControls.subtitleDownloaded' => 'Субтитърът е изтеглен',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'Субтитрите са изтеглени, но не можаха да бъдат избрани', 'videoControls.subtitleDownloadedNotApplied' => 'Субтитрите са изтеглени, но не можаха да бъдат избрани',
'videoControls.subtitleDownloadFailed' => 'Неуспешно изтегляне на субтитър', 'videoControls.subtitleDownloadFailed' => 'Неуспешно изтегляне на субтитър',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Търсене на езици...', 'videoControls.searchLanguages' => 'Търсене на езици...',
'messages.markedAsWatched' => 'Маркирано като гледано', 'messages.markedAsWatched' => 'Маркирано като гледано',
'messages.markedAsUnwatched' => 'Маркирано като негледано', 'messages.markedAsUnwatched' => 'Маркирано като негледано',
@@ -3217,10 +3213,10 @@ extension on TranslationsBg {
'watchTogether.failedToJoin' => 'Неуспешно присъединяване към сесия', 'watchTogether.failedToJoin' => 'Неуспешно присъединяване към сесия',
'watchTogether.sessionCodeCopied' => 'Кодът на сесията е копиран в клипборда', 'watchTogether.sessionCodeCopied' => 'Кодът на сесията е копиран в клипборда',
'watchTogether.relayUnreachable' => 'Релейният сървър е недостъпен. Възможно е интернет доставчикът да блокира гледането заедно.', 'watchTogether.relayUnreachable' => 'Релейният сървър е недостъпен. Възможно е интернет доставчикът да блокира гледането заедно.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Повторно свързване с организатора...', 'watchTogether.reconnectingToHost' => 'Повторно свързване с организатора...',
'watchTogether.currentPlayback' => 'Текущо възпроизвеждане', 'watchTogether.currentPlayback' => 'Текущо възпроизвеждане',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Присъедини се към текущото възпроизвеждане', 'watchTogether.joinCurrentPlayback' => 'Присъедини се към текущото възпроизвеждане',
'watchTogether.joinCurrentPlaybackDescription' => 'Върнете се към това, което организаторът гледа в момента', 'watchTogether.joinCurrentPlaybackDescription' => 'Върнете се към това, което организаторът гледа в момента',
'watchTogether.failedToOpenCurrentPlayback' => 'Неуспешно отваряне на текущото възпроизвеждане', 'watchTogether.failedToOpenCurrentPlayback' => 'Неуспешно отваряне на текущото възпроизвеждане',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$da extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Afspil tilfældigt'; @override String get shufflePlay => 'Afspil tilfældigt';
@override String get shuffleNotAvailableOffline => 'Tilfældig afspilning er ikke tilgængelig offline'; @override String get shuffleNotAvailableOffline => 'Tilfældig afspilning er ikke tilgængelig offline';
@override String get fileInfo => 'Filinfo'; @override String get fileInfo => 'Filinfo';
@override String get deleteFromServer => 'Slet fra server';
@override String get confirmDelete => 'Slet dette medie og dets filer fra din server?';
@override String get deleteMultipleWarning => 'Dette inkluderer alle episoder og deres filer.'; @override String get deleteMultipleWarning => 'Dette inkluderer alle episoder og deres filer.';
@override String get mediaDeletedSuccessfully => 'Mediet blev slettet'; @override String get mediaDeletedSuccessfully => 'Mediet blev slettet';
@override String get mediaFailedToDelete => 'Mediet kunne ikke slettes'; @override String get mediaFailedToDelete => 'Mediet kunne ikke slettes';
@@ -2583,8 +2581,6 @@ extension on TranslationsDa {
'mediaMenu.shufflePlay' => 'Afspil tilfældigt', 'mediaMenu.shufflePlay' => 'Afspil tilfældigt',
'mediaMenu.shuffleNotAvailableOffline' => 'Tilfældig afspilning er ikke tilgængelig offline', 'mediaMenu.shuffleNotAvailableOffline' => 'Tilfældig afspilning er ikke tilgængelig offline',
'mediaMenu.fileInfo' => 'Filinfo', 'mediaMenu.fileInfo' => 'Filinfo',
'mediaMenu.deleteFromServer' => 'Slet fra server',
'mediaMenu.confirmDelete' => 'Slet dette medie og dets filer fra din server?',
'mediaMenu.deleteMultipleWarning' => 'Dette inkluderer alle episoder og deres filer.', 'mediaMenu.deleteMultipleWarning' => 'Dette inkluderer alle episoder og deres filer.',
'mediaMenu.mediaDeletedSuccessfully' => 'Mediet blev slettet', 'mediaMenu.mediaDeletedSuccessfully' => 'Mediet blev slettet',
'mediaMenu.mediaFailedToDelete' => 'Mediet kunne ikke slettes', 'mediaMenu.mediaFailedToDelete' => 'Mediet kunne ikke slettes',
@@ -2703,10 +2699,10 @@ extension on TranslationsDa {
'videoControls.language' => 'Sprog', 'videoControls.language' => 'Sprog',
'videoControls.noSubtitlesFound' => 'Ingen undertekster fundet', 'videoControls.noSubtitlesFound' => 'Ingen undertekster fundet',
'videoControls.subtitleDownloaded' => 'Undertekst downloadet', 'videoControls.subtitleDownloaded' => 'Undertekst downloadet',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'Underteksten blev downloadet, men kunne ikke vælges', 'videoControls.subtitleDownloadedNotApplied' => 'Underteksten blev downloadet, men kunne ikke vælges',
'videoControls.subtitleDownloadFailed' => 'Kunne ikke downloade undertekst', 'videoControls.subtitleDownloadFailed' => 'Kunne ikke downloade undertekst',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Søg sprog...', 'videoControls.searchLanguages' => 'Søg sprog...',
'messages.markedAsWatched' => 'Markeret som set', 'messages.markedAsWatched' => 'Markeret som set',
'messages.markedAsUnwatched' => 'Markeret som uset', 'messages.markedAsUnwatched' => 'Markeret som uset',
@@ -3217,10 +3213,10 @@ extension on TranslationsDa {
'watchTogether.failedToJoin' => 'Kunne ikke deltage i session', 'watchTogether.failedToJoin' => 'Kunne ikke deltage i session',
'watchTogether.sessionCodeCopied' => 'Sessionskode kopieret til udklipsholder', 'watchTogether.sessionCodeCopied' => 'Sessionskode kopieret til udklipsholder',
'watchTogether.relayUnreachable' => 'Relayserveren kan ikke nås. Blokering hos internetudbyderen kan forhindre Se sammen.', 'watchTogether.relayUnreachable' => 'Relayserveren kan ikke nås. Blokering hos internetudbyderen kan forhindre Se sammen.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Genopretter forbindelse til vært...', 'watchTogether.reconnectingToHost' => 'Genopretter forbindelse til vært...',
'watchTogether.currentPlayback' => 'Nuværende afspilning', 'watchTogether.currentPlayback' => 'Nuværende afspilning',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Deltag i nuværende afspilning', 'watchTogether.joinCurrentPlayback' => 'Deltag i nuværende afspilning',
'watchTogether.joinCurrentPlaybackDescription' => 'Hop tilbage til det værten ser nu', 'watchTogether.joinCurrentPlaybackDescription' => 'Hop tilbage til det værten ser nu',
'watchTogether.failedToOpenCurrentPlayback' => 'Kunne ikke åbne nuværende afspilning', 'watchTogether.failedToOpenCurrentPlayback' => 'Kunne ikke åbne nuværende afspilning',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$de extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Zufallswiedergabe'; @override String get shufflePlay => 'Zufallswiedergabe';
@override String get shuffleNotAvailableOffline => 'Zufallswiedergabe ist offline nicht verfügbar'; @override String get shuffleNotAvailableOffline => 'Zufallswiedergabe ist offline nicht verfügbar';
@override String get fileInfo => 'Dateiinfo'; @override String get fileInfo => 'Dateiinfo';
@override String get deleteFromServer => 'Vom Server löschen';
@override String get confirmDelete => 'Dieses Medium und seine Dateien von deinem Server löschen?';
@override String get deleteMultipleWarning => 'Dies umfasst alle Episoden und deren Dateien.'; @override String get deleteMultipleWarning => 'Dies umfasst alle Episoden und deren Dateien.';
@override String get mediaDeletedSuccessfully => 'Medienelement gelöscht'; @override String get mediaDeletedSuccessfully => 'Medienelement gelöscht';
@override String get mediaFailedToDelete => 'Medienelement konnte nicht gelöscht werden'; @override String get mediaFailedToDelete => 'Medienelement konnte nicht gelöscht werden';
@@ -2583,8 +2581,6 @@ extension on TranslationsDe {
'mediaMenu.shufflePlay' => 'Zufallswiedergabe', 'mediaMenu.shufflePlay' => 'Zufallswiedergabe',
'mediaMenu.shuffleNotAvailableOffline' => 'Zufallswiedergabe ist offline nicht verfügbar', 'mediaMenu.shuffleNotAvailableOffline' => 'Zufallswiedergabe ist offline nicht verfügbar',
'mediaMenu.fileInfo' => 'Dateiinfo', 'mediaMenu.fileInfo' => 'Dateiinfo',
'mediaMenu.deleteFromServer' => 'Vom Server löschen',
'mediaMenu.confirmDelete' => 'Dieses Medium und seine Dateien von deinem Server löschen?',
'mediaMenu.deleteMultipleWarning' => 'Dies umfasst alle Episoden und deren Dateien.', 'mediaMenu.deleteMultipleWarning' => 'Dies umfasst alle Episoden und deren Dateien.',
'mediaMenu.mediaDeletedSuccessfully' => 'Medienelement gelöscht', 'mediaMenu.mediaDeletedSuccessfully' => 'Medienelement gelöscht',
'mediaMenu.mediaFailedToDelete' => 'Medienelement konnte nicht gelöscht werden', 'mediaMenu.mediaFailedToDelete' => 'Medienelement konnte nicht gelöscht werden',
@@ -2703,10 +2699,10 @@ extension on TranslationsDe {
'videoControls.language' => 'Sprache', 'videoControls.language' => 'Sprache',
'videoControls.noSubtitlesFound' => 'Keine Untertitel gefunden', 'videoControls.noSubtitlesFound' => 'Keine Untertitel gefunden',
'videoControls.subtitleDownloaded' => 'Untertitel heruntergeladen', 'videoControls.subtitleDownloaded' => 'Untertitel heruntergeladen',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'Der Untertitel wurde heruntergeladen, konnte aber nicht ausgewählt werden', 'videoControls.subtitleDownloadedNotApplied' => 'Der Untertitel wurde heruntergeladen, konnte aber nicht ausgewählt werden',
'videoControls.subtitleDownloadFailed' => 'Untertitel konnte nicht heruntergeladen werden', 'videoControls.subtitleDownloadFailed' => 'Untertitel konnte nicht heruntergeladen werden',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Sprachen suchen...', 'videoControls.searchLanguages' => 'Sprachen suchen...',
'messages.markedAsWatched' => 'Als gesehen markiert', 'messages.markedAsWatched' => 'Als gesehen markiert',
'messages.markedAsUnwatched' => 'Als ungesehen markiert', 'messages.markedAsUnwatched' => 'Als ungesehen markiert',
@@ -3217,10 +3213,10 @@ extension on TranslationsDe {
'watchTogether.failedToJoin' => 'Beitritt zur Sitzung fehlgeschlagen', 'watchTogether.failedToJoin' => 'Beitritt zur Sitzung fehlgeschlagen',
'watchTogether.sessionCodeCopied' => 'Sitzungscode in Zwischenablage kopiert', 'watchTogether.sessionCodeCopied' => 'Sitzungscode in Zwischenablage kopiert',
'watchTogether.relayUnreachable' => 'Relay-Server nicht erreichbar. Eine Sperre durch den Internetanbieter kann gemeinsames Schauen verhindern.', 'watchTogether.relayUnreachable' => 'Relay-Server nicht erreichbar. Eine Sperre durch den Internetanbieter kann gemeinsames Schauen verhindern.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Verbindung zum Host wird wiederhergestellt …', 'watchTogether.reconnectingToHost' => 'Verbindung zum Host wird wiederhergestellt …',
'watchTogether.currentPlayback' => 'Aktuelle Wiedergabe', 'watchTogether.currentPlayback' => 'Aktuelle Wiedergabe',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Aktueller Wiedergabe beitreten', 'watchTogether.joinCurrentPlayback' => 'Aktueller Wiedergabe beitreten',
'watchTogether.joinCurrentPlaybackDescription' => 'Zu dem Inhalt wechseln, den der Host gerade ansieht', 'watchTogether.joinCurrentPlaybackDescription' => 'Zu dem Inhalt wechseln, den der Host gerade ansieht',
'watchTogether.failedToOpenCurrentPlayback' => 'Aktuelle Wiedergabe konnte nicht geöffnet werden', 'watchTogether.failedToOpenCurrentPlayback' => 'Aktuelle Wiedergabe konnte nicht geöffnet werden',
+89 -12
View File
@@ -1565,15 +1565,75 @@ class Translations$mediaMenu$en {
/// en: 'File Info' /// en: 'File Info'
String get fileInfo => 'File Info'; String get fileInfo => 'File Info';
/// en: 'Delete from server' /// en: 'Delete episode from server'
String get deleteFromServer => 'Delete from server'; String get deleteEpisodeFromServer => 'Delete episode from server';
/// en: 'Delete this media and its files from your server?' /// en: 'Delete season from server'
String get confirmDelete => 'Delete this media and its files from your server?'; String get deleteSeasonFromServer => 'Delete season from server';
/// en: 'Delete show from server'
String get deleteShowFromServer => 'Delete show from server';
/// en: 'Delete movie from server'
String get deleteMovieFromServer => 'Delete movie from server';
/// en: 'Delete this episode?'
String get deleteEpisodeTitle => 'Delete this episode?';
/// en: 'Delete this season?'
String get deleteSeasonTitle => 'Delete this season?';
/// en: 'Delete this show?'
String get deleteShowTitle => 'Delete this show?';
/// en: 'Delete this movie?'
String get deleteMovieTitle => 'Delete this movie?';
/// en: 'Delete episode'
String get deleteEpisodeConfirm => 'Delete episode';
/// en: 'Delete season'
String get deleteSeasonConfirm => 'Delete season';
/// en: 'Delete show'
String get deleteShowConfirm => 'Delete show';
/// en: 'Delete movie'
String get deleteMovieConfirm => 'Delete movie';
/// en: 'Delete anyway'
String get deleteAnyway => 'Delete anyway';
/// en: 'Permanently delete ${title} from your server?'
String confirmDeleteTarget({required Object title}) => 'Permanently delete ${title} from your server?';
/// en: 'This includes all episodes and their files.' /// en: 'This includes all episodes and their files.'
String get deleteMultipleWarning => 'This includes all episodes and their files.'; String get deleteMultipleWarning => 'This includes all episodes and their files.';
/// en: '(one) {This deletes all ${n} episode in it, and its file.} (other) {This deletes all ${n} episodes in it, and their files.}'
String deleteEpisodeCountWarning({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n,
one: 'This deletes all ${n} episode in it, and its file.',
other: 'This deletes all ${n} episodes in it, and their files.',
);
/// en: '(one) {This item is stored as ${n} file, which will be deleted.} (other) {This item is stored across ${n} files, and all of them will be deleted.}'
String deleteMultiPartWarning({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n,
one: 'This item is stored as ${n} file, which will be deleted.',
other: 'This item is stored across ${n} files, and all of them will be deleted.',
);
/// en: '(one) {${n} other episode is stored in the same file and will be deleted too:} (other) {${n} other episodes are stored in the same file and will be deleted too:}'
String deleteSharedFileHeading({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n,
one: '${n} other episode is stored in the same file and will be deleted too:',
other: '${n} other episodes are stored in the same file and will be deleted too:',
);
/// en: 'Plezy could not check which files this will remove, so it may delete more than the item named above. Cancel and try again, or delete anyway.'
String get deleteScopeUnverifiedProbeFailed => 'Plezy could not check which files this will remove, so it may delete more than the item named above. Cancel and try again, or delete anyway.';
/// en: 'Your server did not provide file details for this item, so Plezy cannot check which files this will remove. It may delete more than the item named above.'
String get deleteScopeUnverifiedNoFileInfo => 'Your server did not provide file details for this item, so Plezy cannot check which files this will remove. It may delete more than the item named above.';
/// en: 'Media item deleted successfully' /// en: 'Media item deleted successfully'
String get mediaDeletedSuccessfully => 'Media item deleted successfully'; String get mediaDeletedSuccessfully => 'Media item deleted successfully';
@@ -6539,9 +6599,28 @@ extension on Translations {
'mediaMenu.shufflePlay' => 'Shuffle Play', 'mediaMenu.shufflePlay' => 'Shuffle Play',
'mediaMenu.shuffleNotAvailableOffline' => 'Shuffle not available offline', 'mediaMenu.shuffleNotAvailableOffline' => 'Shuffle not available offline',
'mediaMenu.fileInfo' => 'File Info', 'mediaMenu.fileInfo' => 'File Info',
'mediaMenu.deleteFromServer' => 'Delete from server', 'mediaMenu.deleteEpisodeFromServer' => 'Delete episode from server',
'mediaMenu.confirmDelete' => 'Delete this media and its files from your server?', 'mediaMenu.deleteSeasonFromServer' => 'Delete season from server',
'mediaMenu.deleteShowFromServer' => 'Delete show from server',
'mediaMenu.deleteMovieFromServer' => 'Delete movie from server',
'mediaMenu.deleteEpisodeTitle' => 'Delete this episode?',
'mediaMenu.deleteSeasonTitle' => 'Delete this season?',
'mediaMenu.deleteShowTitle' => 'Delete this show?',
'mediaMenu.deleteMovieTitle' => 'Delete this movie?',
'mediaMenu.deleteEpisodeConfirm' => 'Delete episode',
'mediaMenu.deleteSeasonConfirm' => 'Delete season',
'mediaMenu.deleteShowConfirm' => 'Delete show',
'mediaMenu.deleteMovieConfirm' => 'Delete movie',
'mediaMenu.deleteAnyway' => 'Delete anyway',
'mediaMenu.confirmDeleteTarget' => ({required Object title}) => 'Permanently delete ${title} from your server?',
'mediaMenu.deleteMultipleWarning' => 'This includes all episodes and their files.', 'mediaMenu.deleteMultipleWarning' => 'This includes all episodes and their files.',
'mediaMenu.deleteEpisodeCountWarning' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: 'This deletes all ${n} episode in it, and its file.', other: 'This deletes all ${n} episodes in it, and their files.', ),
_ => null,
} ?? switch (path) {
'mediaMenu.deleteMultiPartWarning' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: 'This item is stored as ${n} file, which will be deleted.', other: 'This item is stored across ${n} files, and all of them will be deleted.', ),
'mediaMenu.deleteSharedFileHeading' => ({required num n}) => (_root.$meta.cardinalResolver ?? PluralResolvers.cardinal('en'))(n, one: '${n} other episode is stored in the same file and will be deleted too:', other: '${n} other episodes are stored in the same file and will be deleted too:', ),
'mediaMenu.deleteScopeUnverifiedProbeFailed' => 'Plezy could not check which files this will remove, so it may delete more than the item named above. Cancel and try again, or delete anyway.',
'mediaMenu.deleteScopeUnverifiedNoFileInfo' => 'Your server did not provide file details for this item, so Plezy cannot check which files this will remove. It may delete more than the item named above.',
'mediaMenu.mediaDeletedSuccessfully' => 'Media item deleted successfully', 'mediaMenu.mediaDeletedSuccessfully' => 'Media item deleted successfully',
'mediaMenu.mediaFailedToDelete' => 'Failed to delete media item', 'mediaMenu.mediaFailedToDelete' => 'Failed to delete media item',
'mediaMenu.rate' => 'Rate', 'mediaMenu.rate' => 'Rate',
@@ -6555,8 +6634,6 @@ extension on Translations {
'rateSheet.notAvailable' => 'No match found', 'rateSheet.notAvailable' => 'No match found',
'rateSheet.noConnectedServices' => 'Connect a service in Settings to rate there.', 'rateSheet.noConnectedServices' => 'Connect a service in Settings to rate there.',
'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, movie', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, movie',
_ => null,
} ?? switch (path) {
'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV show', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV show',
'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}',
'accessibility.mediaCardSeason' => ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}', 'accessibility.mediaCardSeason' => ({required Object title, required Object seasonInfo}) => '${title}, ${seasonInfo}',
@@ -7052,6 +7129,8 @@ extension on Translations {
'explore.badge.nextEpisodeIn' => ({required Object episode, required Object duration}) => 'Ep ${episode} in ${duration}', 'explore.badge.nextEpisodeIn' => ({required Object episode, required Object duration}) => 'Ep ${episode} in ${duration}',
'explore.badge.nextAiringIn' => ({required Object duration}) => 'Next in ${duration}', 'explore.badge.nextAiringIn' => ({required Object duration}) => 'Next in ${duration}',
'explore.badge.episodesShort' => ({required Object n}) => '${n} eps', 'explore.badge.episodesShort' => ({required Object n}) => '${n} eps',
_ => null,
} ?? switch (path) {
'explore.badge.minutesPerEpisode' => ({required Object n}) => '${n} min/ep', 'explore.badge.minutesPerEpisode' => ({required Object n}) => '${n} min/ep',
'explore.badge.adult' => '18+', 'explore.badge.adult' => '18+',
'explore.stats.listed' => ({required Object n}) => '${n} listed', 'explore.stats.listed' => ({required Object n}) => '${n} listed',
@@ -7069,8 +7148,6 @@ extension on Translations {
'explore.stats.completed' => ({required Object n}) => '${n} completed', 'explore.stats.completed' => ({required Object n}) => '${n} completed',
'explore.stats.onHold' => ({required Object n}) => '${n} on hold', 'explore.stats.onHold' => ({required Object n}) => '${n} on hold',
'explore.stats.dropped' => ({required Object n}) => '${n} dropped', 'explore.stats.dropped' => ({required Object n}) => '${n} dropped',
_ => null,
} ?? switch (path) {
'explore.season.winter' => 'Winter', 'explore.season.winter' => 'Winter',
'explore.season.spring' => 'Spring', 'explore.season.spring' => 'Spring',
'explore.season.summer' => 'Summer', 'explore.season.summer' => 'Summer',
@@ -7566,6 +7643,8 @@ extension on Translations {
'performanceOverlay.maxCll' => 'MaxCLL', 'performanceOverlay.maxCll' => 'MaxCLL',
'performanceOverlay.maxFall' => 'MaxFALL', 'performanceOverlay.maxFall' => 'MaxFALL',
'performanceOverlay.cacheUsed' => 'Cache Used', 'performanceOverlay.cacheUsed' => 'Cache Used',
_ => null,
} ?? switch (path) {
'performanceOverlay.cacheLimit' => 'Cache Limit', 'performanceOverlay.cacheLimit' => 'Cache Limit',
'performanceOverlay.speed' => 'Speed', 'performanceOverlay.speed' => 'Speed',
'performanceOverlay.player' => 'Player', 'performanceOverlay.player' => 'Player',
@@ -7583,8 +7662,6 @@ extension on Translations {
'externalPlayer.playerCommand' => 'Command', 'externalPlayer.playerCommand' => 'Command',
'externalPlayer.playerPackage' => 'Package Name', 'externalPlayer.playerPackage' => 'Package Name',
'externalPlayer.playerUrlScheme' => 'URL Scheme', 'externalPlayer.playerUrlScheme' => 'URL Scheme',
_ => null,
} ?? switch (path) {
'externalPlayer.off' => 'Off', 'externalPlayer.off' => 'Off',
'externalPlayer.launchFailed' => 'Failed to open external player', 'externalPlayer.launchFailed' => 'Failed to open external player',
'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is not installed', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is not installed',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$es extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Reproducción aleatoria'; @override String get shufflePlay => 'Reproducción aleatoria';
@override String get shuffleNotAvailableOffline => 'La reproducción aleatoria no está disponible sin conexión'; @override String get shuffleNotAvailableOffline => 'La reproducción aleatoria no está disponible sin conexión';
@override String get fileInfo => 'Información del archivo'; @override String get fileInfo => 'Información del archivo';
@override String get deleteFromServer => 'Eliminar del servidor';
@override String get confirmDelete => '¿Eliminar este medio y sus archivos de tu servidor?';
@override String get deleteMultipleWarning => 'Esto incluye todos los episodios y sus archivos.'; @override String get deleteMultipleWarning => 'Esto incluye todos los episodios y sus archivos.';
@override String get mediaDeletedSuccessfully => 'Elemento multimedia eliminado con éxito'; @override String get mediaDeletedSuccessfully => 'Elemento multimedia eliminado con éxito';
@override String get mediaFailedToDelete => 'Error al eliminar el elemento multimedia'; @override String get mediaFailedToDelete => 'Error al eliminar el elemento multimedia';
@@ -2583,8 +2581,6 @@ extension on TranslationsEs {
'mediaMenu.shufflePlay' => 'Reproducción aleatoria', 'mediaMenu.shufflePlay' => 'Reproducción aleatoria',
'mediaMenu.shuffleNotAvailableOffline' => 'La reproducción aleatoria no está disponible sin conexión', 'mediaMenu.shuffleNotAvailableOffline' => 'La reproducción aleatoria no está disponible sin conexión',
'mediaMenu.fileInfo' => 'Información del archivo', 'mediaMenu.fileInfo' => 'Información del archivo',
'mediaMenu.deleteFromServer' => 'Eliminar del servidor',
'mediaMenu.confirmDelete' => '¿Eliminar este medio y sus archivos de tu servidor?',
'mediaMenu.deleteMultipleWarning' => 'Esto incluye todos los episodios y sus archivos.', 'mediaMenu.deleteMultipleWarning' => 'Esto incluye todos los episodios y sus archivos.',
'mediaMenu.mediaDeletedSuccessfully' => 'Elemento multimedia eliminado con éxito', 'mediaMenu.mediaDeletedSuccessfully' => 'Elemento multimedia eliminado con éxito',
'mediaMenu.mediaFailedToDelete' => 'Error al eliminar el elemento multimedia', 'mediaMenu.mediaFailedToDelete' => 'Error al eliminar el elemento multimedia',
@@ -2703,10 +2699,10 @@ extension on TranslationsEs {
'videoControls.language' => 'Idioma', 'videoControls.language' => 'Idioma',
'videoControls.noSubtitlesFound' => 'No se encontraron subtítulos', 'videoControls.noSubtitlesFound' => 'No se encontraron subtítulos',
'videoControls.subtitleDownloaded' => 'Subtítulo descargado', 'videoControls.subtitleDownloaded' => 'Subtítulo descargado',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'El subtítulo se descargó, pero no se pudo seleccionar', 'videoControls.subtitleDownloadedNotApplied' => 'El subtítulo se descargó, pero no se pudo seleccionar',
'videoControls.subtitleDownloadFailed' => 'Error al descargar subtítulo', 'videoControls.subtitleDownloadFailed' => 'Error al descargar subtítulo',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Buscar idiomas...', 'videoControls.searchLanguages' => 'Buscar idiomas...',
'messages.markedAsWatched' => 'Marcado como visto', 'messages.markedAsWatched' => 'Marcado como visto',
'messages.markedAsUnwatched' => 'Marcado como no visto', 'messages.markedAsUnwatched' => 'Marcado como no visto',
@@ -3217,10 +3213,10 @@ extension on TranslationsEs {
'watchTogether.failedToJoin' => 'Error al unirse a la sesión', 'watchTogether.failedToJoin' => 'Error al unirse a la sesión',
'watchTogether.sessionCodeCopied' => 'Código de sesión copiado al portapapeles', 'watchTogether.sessionCodeCopied' => 'Código de sesión copiado al portapapeles',
'watchTogether.relayUnreachable' => 'No se puede acceder al servidor de retransmisión. Es posible que tu proveedor de internet esté bloqueando Ver juntos.', 'watchTogether.relayUnreachable' => 'No se puede acceder al servidor de retransmisión. Es posible que tu proveedor de internet esté bloqueando Ver juntos.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Reconectando con el anfitrión...', 'watchTogether.reconnectingToHost' => 'Reconectando con el anfitrión...',
'watchTogether.currentPlayback' => 'Reproducción actual', 'watchTogether.currentPlayback' => 'Reproducción actual',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Unirse a la reproducción actual', 'watchTogether.joinCurrentPlayback' => 'Unirse a la reproducción actual',
'watchTogether.joinCurrentPlaybackDescription' => 'Vuelve a lo que el anfitrión está viendo ahora mismo', 'watchTogether.joinCurrentPlaybackDescription' => 'Vuelve a lo que el anfitrión está viendo ahora mismo',
'watchTogether.failedToOpenCurrentPlayback' => 'No se pudo abrir la reproducción actual', 'watchTogether.failedToOpenCurrentPlayback' => 'No se pudo abrir la reproducción actual',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$fr extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Lecture aléatoire'; @override String get shufflePlay => 'Lecture aléatoire';
@override String get shuffleNotAvailableOffline => 'La lecture aléatoire nest pas disponible hors ligne'; @override String get shuffleNotAvailableOffline => 'La lecture aléatoire nest pas disponible hors ligne';
@override String get fileInfo => 'Informations sur le fichier'; @override String get fileInfo => 'Informations sur le fichier';
@override String get deleteFromServer => 'Supprimer du serveur';
@override String get confirmDelete => 'Supprimer ce média et ses fichiers de votre serveur ?';
@override String get deleteMultipleWarning => 'Cela inclut tous les épisodes et leurs fichiers.'; @override String get deleteMultipleWarning => 'Cela inclut tous les épisodes et leurs fichiers.';
@override String get mediaDeletedSuccessfully => 'Élément média supprimé avec succès'; @override String get mediaDeletedSuccessfully => 'Élément média supprimé avec succès';
@override String get mediaFailedToDelete => 'Échec de la suppression de l\'élément média'; @override String get mediaFailedToDelete => 'Échec de la suppression de l\'élément média';
@@ -2583,8 +2581,6 @@ extension on TranslationsFr {
'mediaMenu.shufflePlay' => 'Lecture aléatoire', 'mediaMenu.shufflePlay' => 'Lecture aléatoire',
'mediaMenu.shuffleNotAvailableOffline' => 'La lecture aléatoire nest pas disponible hors ligne', 'mediaMenu.shuffleNotAvailableOffline' => 'La lecture aléatoire nest pas disponible hors ligne',
'mediaMenu.fileInfo' => 'Informations sur le fichier', 'mediaMenu.fileInfo' => 'Informations sur le fichier',
'mediaMenu.deleteFromServer' => 'Supprimer du serveur',
'mediaMenu.confirmDelete' => 'Supprimer ce média et ses fichiers de votre serveur ?',
'mediaMenu.deleteMultipleWarning' => 'Cela inclut tous les épisodes et leurs fichiers.', 'mediaMenu.deleteMultipleWarning' => 'Cela inclut tous les épisodes et leurs fichiers.',
'mediaMenu.mediaDeletedSuccessfully' => 'Élément média supprimé avec succès', 'mediaMenu.mediaDeletedSuccessfully' => 'Élément média supprimé avec succès',
'mediaMenu.mediaFailedToDelete' => 'Échec de la suppression de l\'élément média', 'mediaMenu.mediaFailedToDelete' => 'Échec de la suppression de l\'élément média',
@@ -2703,10 +2699,10 @@ extension on TranslationsFr {
'videoControls.language' => 'Langue', 'videoControls.language' => 'Langue',
'videoControls.noSubtitlesFound' => 'Aucun sous-titre trouvé', 'videoControls.noSubtitlesFound' => 'Aucun sous-titre trouvé',
'videoControls.subtitleDownloaded' => 'Sous-titre téléchargé', 'videoControls.subtitleDownloaded' => 'Sous-titre téléchargé',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'Le sous-titre a été téléchargé, mais na pas pu être sélectionné', 'videoControls.subtitleDownloadedNotApplied' => 'Le sous-titre a été téléchargé, mais na pas pu être sélectionné',
'videoControls.subtitleDownloadFailed' => 'Échec du téléchargement du sous-titre', 'videoControls.subtitleDownloadFailed' => 'Échec du téléchargement du sous-titre',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Rechercher des langues...', 'videoControls.searchLanguages' => 'Rechercher des langues...',
'messages.markedAsWatched' => 'Marqué comme vu', 'messages.markedAsWatched' => 'Marqué comme vu',
'messages.markedAsUnwatched' => 'Marqué comme non vu', 'messages.markedAsUnwatched' => 'Marqué comme non vu',
@@ -3217,10 +3213,10 @@ extension on TranslationsFr {
'watchTogether.failedToJoin' => 'Échec de la connexion à la session', 'watchTogether.failedToJoin' => 'Échec de la connexion à la session',
'watchTogether.sessionCodeCopied' => 'Code de session copié dans le presse-papiers', 'watchTogether.sessionCodeCopied' => 'Code de session copié dans le presse-papiers',
'watchTogether.relayUnreachable' => 'Serveur relais inaccessible. Un blocage par le fournisseur daccès peut empêcher le fonctionnement de Regarder ensemble.', 'watchTogether.relayUnreachable' => 'Serveur relais inaccessible. Un blocage par le fournisseur daccès peut empêcher le fonctionnement de Regarder ensemble.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Reconnexion à l\'hôte...', 'watchTogether.reconnectingToHost' => 'Reconnexion à l\'hôte...',
'watchTogether.currentPlayback' => 'Lecture en cours', 'watchTogether.currentPlayback' => 'Lecture en cours',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Rejoindre la lecture en cours', 'watchTogether.joinCurrentPlayback' => 'Rejoindre la lecture en cours',
'watchTogether.joinCurrentPlaybackDescription' => 'Reprendre le contenu que lhôte regarde actuellement', 'watchTogether.joinCurrentPlaybackDescription' => 'Reprendre le contenu que lhôte regarde actuellement',
'watchTogether.failedToOpenCurrentPlayback' => 'Impossible d\'ouvrir la lecture en cours', 'watchTogether.failedToOpenCurrentPlayback' => 'Impossible d\'ouvrir la lecture en cours',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$hu extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Véletlenszerű lejátszás'; @override String get shufflePlay => 'Véletlenszerű lejátszás';
@override String get shuffleNotAvailableOffline => 'A véletlenszerű lejátszás nem érhető el offline'; @override String get shuffleNotAvailableOffline => 'A véletlenszerű lejátszás nem érhető el offline';
@override String get fileInfo => 'Fájlinformáció'; @override String get fileInfo => 'Fájlinformáció';
@override String get deleteFromServer => 'Törlés a szerverről';
@override String get confirmDelete => 'Törlöd ezt a médiát és a fájljait a szerveredről?';
@override String get deleteMultipleWarning => 'Ez magában foglalja az összes epizódot és azok fájljait.'; @override String get deleteMultipleWarning => 'Ez magában foglalja az összes epizódot és azok fájljait.';
@override String get mediaDeletedSuccessfully => 'Médiaelem sikeresen törölve'; @override String get mediaDeletedSuccessfully => 'Médiaelem sikeresen törölve';
@override String get mediaFailedToDelete => 'Nem sikerült a médiaelem törlése'; @override String get mediaFailedToDelete => 'Nem sikerült a médiaelem törlése';
@@ -2583,8 +2581,6 @@ extension on TranslationsHu {
'mediaMenu.shufflePlay' => 'Véletlenszerű lejátszás', 'mediaMenu.shufflePlay' => 'Véletlenszerű lejátszás',
'mediaMenu.shuffleNotAvailableOffline' => 'A véletlenszerű lejátszás nem érhető el offline', 'mediaMenu.shuffleNotAvailableOffline' => 'A véletlenszerű lejátszás nem érhető el offline',
'mediaMenu.fileInfo' => 'Fájlinformáció', 'mediaMenu.fileInfo' => 'Fájlinformáció',
'mediaMenu.deleteFromServer' => 'Törlés a szerverről',
'mediaMenu.confirmDelete' => 'Törlöd ezt a médiát és a fájljait a szerveredről?',
'mediaMenu.deleteMultipleWarning' => 'Ez magában foglalja az összes epizódot és azok fájljait.', 'mediaMenu.deleteMultipleWarning' => 'Ez magában foglalja az összes epizódot és azok fájljait.',
'mediaMenu.mediaDeletedSuccessfully' => 'Médiaelem sikeresen törölve', 'mediaMenu.mediaDeletedSuccessfully' => 'Médiaelem sikeresen törölve',
'mediaMenu.mediaFailedToDelete' => 'Nem sikerült a médiaelem törlése', 'mediaMenu.mediaFailedToDelete' => 'Nem sikerült a médiaelem törlése',
@@ -2703,10 +2699,10 @@ extension on TranslationsHu {
'videoControls.language' => 'Nyelv', 'videoControls.language' => 'Nyelv',
'videoControls.noSubtitlesFound' => 'Nem találhatók feliratok', 'videoControls.noSubtitlesFound' => 'Nem találhatók feliratok',
'videoControls.subtitleDownloaded' => 'Felirat letöltve', 'videoControls.subtitleDownloaded' => 'Felirat letöltve',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'Felirat letöltve, de nem sikerült kiválasztani', 'videoControls.subtitleDownloadedNotApplied' => 'Felirat letöltve, de nem sikerült kiválasztani',
'videoControls.subtitleDownloadFailed' => 'Nem sikerült a felirat letöltése', 'videoControls.subtitleDownloadFailed' => 'Nem sikerült a felirat letöltése',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Nyelvek keresése...', 'videoControls.searchLanguages' => 'Nyelvek keresése...',
'messages.markedAsWatched' => 'Megjelölve megtekintettként', 'messages.markedAsWatched' => 'Megjelölve megtekintettként',
'messages.markedAsUnwatched' => 'Megjelölve nem megtekintettként', 'messages.markedAsUnwatched' => 'Megjelölve nem megtekintettként',
@@ -3217,10 +3213,10 @@ extension on TranslationsHu {
'watchTogether.failedToJoin' => 'Nem sikerült csatlakozni a munkamenethez', 'watchTogether.failedToJoin' => 'Nem sikerült csatlakozni a munkamenethez',
'watchTogether.sessionCodeCopied' => 'A munkamenetkód a vágólapra másolva', 'watchTogether.sessionCodeCopied' => 'A munkamenetkód a vágólapra másolva',
'watchTogether.relayUnreachable' => 'A relészerver nem érhető el. Az internetszolgáltató blokkolása megakadályozhatja a közös nézést.', 'watchTogether.relayUnreachable' => 'A relészerver nem érhető el. Az internetszolgáltató blokkolása megakadályozhatja a közös nézést.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Újracsatlakozás a házigazdához...', 'watchTogether.reconnectingToHost' => 'Újracsatlakozás a házigazdához...',
'watchTogether.currentPlayback' => 'Jelenlegi lejátszás', 'watchTogether.currentPlayback' => 'Jelenlegi lejátszás',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Csatlakozás a jelenlegi lejátszáshoz', 'watchTogether.joinCurrentPlayback' => 'Csatlakozás a jelenlegi lejátszáshoz',
'watchTogether.joinCurrentPlaybackDescription' => 'Visszatérés ahhoz, amit a házigazda éppen néz', 'watchTogether.joinCurrentPlaybackDescription' => 'Visszatérés ahhoz, amit a házigazda éppen néz',
'watchTogether.failedToOpenCurrentPlayback' => 'Nem sikerült megnyitni a jelenlegi lejátszást', 'watchTogether.failedToOpenCurrentPlayback' => 'Nem sikerült megnyitni a jelenlegi lejátszást',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$it extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Riproduzione casuale'; @override String get shufflePlay => 'Riproduzione casuale';
@override String get shuffleNotAvailableOffline => 'Riproduzione casuale non disponibile offline'; @override String get shuffleNotAvailableOffline => 'Riproduzione casuale non disponibile offline';
@override String get fileInfo => 'Info sul file'; @override String get fileInfo => 'Info sul file';
@override String get deleteFromServer => 'Elimina dal server';
@override String get confirmDelete => 'Eliminare questo media e i suoi file dal server?';
@override String get deleteMultipleWarning => 'Sono inclusi tutti gli episodi e i relativi file.'; @override String get deleteMultipleWarning => 'Sono inclusi tutti gli episodi e i relativi file.';
@override String get mediaDeletedSuccessfully => 'Elemento multimediale eliminato correttamente'; @override String get mediaDeletedSuccessfully => 'Elemento multimediale eliminato correttamente';
@override String get mediaFailedToDelete => 'Impossibile eliminare l\'elemento multimediale'; @override String get mediaFailedToDelete => 'Impossibile eliminare l\'elemento multimediale';
@@ -2583,8 +2581,6 @@ extension on TranslationsIt {
'mediaMenu.shufflePlay' => 'Riproduzione casuale', 'mediaMenu.shufflePlay' => 'Riproduzione casuale',
'mediaMenu.shuffleNotAvailableOffline' => 'Riproduzione casuale non disponibile offline', 'mediaMenu.shuffleNotAvailableOffline' => 'Riproduzione casuale non disponibile offline',
'mediaMenu.fileInfo' => 'Info sul file', 'mediaMenu.fileInfo' => 'Info sul file',
'mediaMenu.deleteFromServer' => 'Elimina dal server',
'mediaMenu.confirmDelete' => 'Eliminare questo media e i suoi file dal server?',
'mediaMenu.deleteMultipleWarning' => 'Sono inclusi tutti gli episodi e i relativi file.', 'mediaMenu.deleteMultipleWarning' => 'Sono inclusi tutti gli episodi e i relativi file.',
'mediaMenu.mediaDeletedSuccessfully' => 'Elemento multimediale eliminato correttamente', 'mediaMenu.mediaDeletedSuccessfully' => 'Elemento multimediale eliminato correttamente',
'mediaMenu.mediaFailedToDelete' => 'Impossibile eliminare l\'elemento multimediale', 'mediaMenu.mediaFailedToDelete' => 'Impossibile eliminare l\'elemento multimediale',
@@ -2703,10 +2699,10 @@ extension on TranslationsIt {
'videoControls.language' => 'Lingua', 'videoControls.language' => 'Lingua',
'videoControls.noSubtitlesFound' => 'Nessun sottotitolo trovato', 'videoControls.noSubtitlesFound' => 'Nessun sottotitolo trovato',
'videoControls.subtitleDownloaded' => 'Sottotitolo scaricato', 'videoControls.subtitleDownloaded' => 'Sottotitolo scaricato',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'Il sottotitolo è stato scaricato, ma non è stato possibile selezionarlo', 'videoControls.subtitleDownloadedNotApplied' => 'Il sottotitolo è stato scaricato, ma non è stato possibile selezionarlo',
'videoControls.subtitleDownloadFailed' => 'Impossibile scaricare il sottotitolo', 'videoControls.subtitleDownloadFailed' => 'Impossibile scaricare il sottotitolo',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Cerca lingue...', 'videoControls.searchLanguages' => 'Cerca lingue...',
'messages.markedAsWatched' => 'Segnato come visto', 'messages.markedAsWatched' => 'Segnato come visto',
'messages.markedAsUnwatched' => 'Segnato come non visto', 'messages.markedAsUnwatched' => 'Segnato come non visto',
@@ -3217,10 +3213,10 @@ extension on TranslationsIt {
'watchTogether.failedToJoin' => 'Impossibile unirsi alla sessione', 'watchTogether.failedToJoin' => 'Impossibile unirsi alla sessione',
'watchTogether.sessionCodeCopied' => 'Codice della sessione copiato negli appunti', 'watchTogether.sessionCodeCopied' => 'Codice della sessione copiato negli appunti',
'watchTogether.relayUnreachable' => 'Il server relay non è raggiungibile. Eventuali blocchi dell\'ISP potrebbero impedire l\'uso di Guarda insieme.', 'watchTogether.relayUnreachable' => 'Il server relay non è raggiungibile. Eventuali blocchi dell\'ISP potrebbero impedire l\'uso di Guarda insieme.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Riconnessione all\'host...', 'watchTogether.reconnectingToHost' => 'Riconnessione all\'host...',
'watchTogether.currentPlayback' => 'Riproduzione corrente', 'watchTogether.currentPlayback' => 'Riproduzione corrente',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Unisciti alla riproduzione corrente', 'watchTogether.joinCurrentPlayback' => 'Unisciti alla riproduzione corrente',
'watchTogether.joinCurrentPlaybackDescription' => 'Torna a ciò che l\'host sta guardando in questo momento', 'watchTogether.joinCurrentPlaybackDescription' => 'Torna a ciò che l\'host sta guardando in questo momento',
'watchTogether.failedToOpenCurrentPlayback' => 'Impossibile aprire la riproduzione corrente', 'watchTogether.failedToOpenCurrentPlayback' => 'Impossibile aprire la riproduzione corrente',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$ja extends Translations$mediaMenu$en {
@override String get shufflePlay => 'シャッフル再生'; @override String get shufflePlay => 'シャッフル再生';
@override String get shuffleNotAvailableOffline => 'オフラインではシャッフルを利用できません'; @override String get shuffleNotAvailableOffline => 'オフラインではシャッフルを利用できません';
@override String get fileInfo => 'ファイル情報'; @override String get fileInfo => 'ファイル情報';
@override String get deleteFromServer => 'サーバーから削除';
@override String get confirmDelete => 'このメディアとそのファイルをサーバーから削除しますか?';
@override String get deleteMultipleWarning => 'すべてのエピソードとそのファイルが含まれます。'; @override String get deleteMultipleWarning => 'すべてのエピソードとそのファイルが含まれます。';
@override String get mediaDeletedSuccessfully => 'メディアアイテムを正常に削除しました'; @override String get mediaDeletedSuccessfully => 'メディアアイテムを正常に削除しました';
@override String get mediaFailedToDelete => 'メディアアイテムの削除に失敗しました'; @override String get mediaFailedToDelete => 'メディアアイテムの削除に失敗しました';
@@ -2580,8 +2578,6 @@ extension on TranslationsJa {
'mediaMenu.shufflePlay' => 'シャッフル再生', 'mediaMenu.shufflePlay' => 'シャッフル再生',
'mediaMenu.shuffleNotAvailableOffline' => 'オフラインではシャッフルを利用できません', 'mediaMenu.shuffleNotAvailableOffline' => 'オフラインではシャッフルを利用できません',
'mediaMenu.fileInfo' => 'ファイル情報', 'mediaMenu.fileInfo' => 'ファイル情報',
'mediaMenu.deleteFromServer' => 'サーバーから削除',
'mediaMenu.confirmDelete' => 'このメディアとそのファイルをサーバーから削除しますか?',
'mediaMenu.deleteMultipleWarning' => 'すべてのエピソードとそのファイルが含まれます。', 'mediaMenu.deleteMultipleWarning' => 'すべてのエピソードとそのファイルが含まれます。',
'mediaMenu.mediaDeletedSuccessfully' => 'メディアアイテムを正常に削除しました', 'mediaMenu.mediaDeletedSuccessfully' => 'メディアアイテムを正常に削除しました',
'mediaMenu.mediaFailedToDelete' => 'メディアアイテムの削除に失敗しました', 'mediaMenu.mediaFailedToDelete' => 'メディアアイテムの削除に失敗しました',
@@ -2700,10 +2696,10 @@ extension on TranslationsJa {
'videoControls.language' => '言語', 'videoControls.language' => '言語',
'videoControls.noSubtitlesFound' => '字幕が見つかりません', 'videoControls.noSubtitlesFound' => '字幕が見つかりません',
'videoControls.subtitleDownloaded' => '字幕をダウンロードしました', 'videoControls.subtitleDownloaded' => '字幕をダウンロードしました',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => '字幕はダウンロードされましたが、選択できませんでした', 'videoControls.subtitleDownloadedNotApplied' => '字幕はダウンロードされましたが、選択できませんでした',
'videoControls.subtitleDownloadFailed' => '字幕のダウンロードに失敗しました', 'videoControls.subtitleDownloadFailed' => '字幕のダウンロードに失敗しました',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => '言語を検索…', 'videoControls.searchLanguages' => '言語を検索…',
'messages.markedAsWatched' => '視聴済みにしました', 'messages.markedAsWatched' => '視聴済みにしました',
'messages.markedAsUnwatched' => '未視聴にしました', 'messages.markedAsUnwatched' => '未視聴にしました',
@@ -3214,10 +3210,10 @@ extension on TranslationsJa {
'watchTogether.failedToJoin' => 'セッションへの参加に失敗しました', 'watchTogether.failedToJoin' => 'セッションへの参加に失敗しました',
'watchTogether.sessionCodeCopied' => 'セッションコードをクリップボードにコピーしました', 'watchTogether.sessionCodeCopied' => 'セッションコードをクリップボードにコピーしました',
'watchTogether.relayUnreachable' => 'リレーサーバーに接続できません。ISPによるブロックのため「一緒に見る」を利用できない可能性があります。', 'watchTogether.relayUnreachable' => 'リレーサーバーに接続できません。ISPによるブロックのため「一緒に見る」を利用できない可能性があります。',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'ホストに再接続中…', 'watchTogether.reconnectingToHost' => 'ホストに再接続中…',
'watchTogether.currentPlayback' => '現在の再生', 'watchTogether.currentPlayback' => '現在の再生',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => '現在の再生に参加', 'watchTogether.joinCurrentPlayback' => '現在の再生に参加',
'watchTogether.joinCurrentPlaybackDescription' => 'ホストが現在視聴中のコンテンツに戻る', 'watchTogether.joinCurrentPlaybackDescription' => 'ホストが現在視聴中のコンテンツに戻る',
'watchTogether.failedToOpenCurrentPlayback' => '現在の再生を開けませんでした', 'watchTogether.failedToOpenCurrentPlayback' => '現在の再生を開けませんでした',
+4 -8
View File
@@ -549,8 +549,6 @@ class _Translations$mediaMenu$kk extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Араластырып ойнату'; @override String get shufflePlay => 'Араластырып ойнату';
@override String get shuffleNotAvailableOffline => 'Араластырып ойнату офлайн режимде қолжетімсіз'; @override String get shuffleNotAvailableOffline => 'Араластырып ойнату офлайн режимде қолжетімсіз';
@override String get fileInfo => 'Файл туралы ақпарат'; @override String get fileInfo => 'Файл туралы ақпарат';
@override String get deleteFromServer => 'Серверден өшіру';
@override String get confirmDelete => 'Осы медиа мен файлдар серверден өшірілсін бе?';
@override String get deleteMultipleWarning => 'Бұл барлық бөлімдер мен файлдарға әсер етеді.'; @override String get deleteMultipleWarning => 'Бұл барлық бөлімдер мен файлдарға әсер етеді.';
@override String get mediaDeletedSuccessfully => 'Медиа элементі сәтті өшірілді'; @override String get mediaDeletedSuccessfully => 'Медиа элементі сәтті өшірілді';
@override String get mediaFailedToDelete => 'Медиа элементін өшіру мүмкін болмады'; @override String get mediaFailedToDelete => 'Медиа элементін өшіру мүмкін болмады';
@@ -2597,8 +2595,6 @@ extension on TranslationsKk {
'mediaMenu.shufflePlay' => 'Араластырып ойнату', 'mediaMenu.shufflePlay' => 'Араластырып ойнату',
'mediaMenu.shuffleNotAvailableOffline' => 'Араластырып ойнату офлайн режимде қолжетімсіз', 'mediaMenu.shuffleNotAvailableOffline' => 'Араластырып ойнату офлайн режимде қолжетімсіз',
'mediaMenu.fileInfo' => 'Файл туралы ақпарат', 'mediaMenu.fileInfo' => 'Файл туралы ақпарат',
'mediaMenu.deleteFromServer' => 'Серверден өшіру',
'mediaMenu.confirmDelete' => 'Осы медиа мен файлдар серверден өшірілсін бе?',
'mediaMenu.deleteMultipleWarning' => 'Бұл барлық бөлімдер мен файлдарға әсер етеді.', 'mediaMenu.deleteMultipleWarning' => 'Бұл барлық бөлімдер мен файлдарға әсер етеді.',
'mediaMenu.mediaDeletedSuccessfully' => 'Медиа элементі сәтті өшірілді', 'mediaMenu.mediaDeletedSuccessfully' => 'Медиа элементі сәтті өшірілді',
'mediaMenu.mediaFailedToDelete' => 'Медиа элементін өшіру мүмкін болмады', 'mediaMenu.mediaFailedToDelete' => 'Медиа элементін өшіру мүмкін болмады',
@@ -2714,10 +2710,10 @@ extension on TranslationsKk {
'videoControls.queue' => 'Кезек', 'videoControls.queue' => 'Кезек',
'videoControls.noQueueItems' => 'Кезекте элементтер жоқ', 'videoControls.noQueueItems' => 'Кезекте элементтер жоқ',
'videoControls.searchSubtitles' => 'Субтитр іздеу', 'videoControls.searchSubtitles' => 'Субтитр іздеу',
_ => null,
} ?? switch (path) {
'videoControls.language' => 'Тіл', 'videoControls.language' => 'Тіл',
'videoControls.noSubtitlesFound' => 'Субтитр табылмады', 'videoControls.noSubtitlesFound' => 'Субтитр табылмады',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Субтитр жүктелді', 'videoControls.subtitleDownloaded' => 'Субтитр жүктелді',
'videoControls.subtitleDownloadedNotApplied' => 'Субтитр жүктелді, бірақ қолданылмады', 'videoControls.subtitleDownloadedNotApplied' => 'Субтитр жүктелді, бірақ қолданылмады',
'videoControls.subtitleDownloadFailed' => 'Субтитрді жүктеу мүмкін болмады', 'videoControls.subtitleDownloadFailed' => 'Субтитрді жүктеу мүмкін болмады',
@@ -3228,10 +3224,10 @@ extension on TranslationsKk {
'watchTogether.pasteFromClipboard' => 'Алмасу буферінен қою', 'watchTogether.pasteFromClipboard' => 'Алмасу буферінен қою',
'watchTogether.pleaseEnterCode' => 'Сеанс кодын енгізіңіз', 'watchTogether.pleaseEnterCode' => 'Сеанс кодын енгізіңіз',
'watchTogether.codeMustBe5Chars' => 'Сеанс коды 5 таңбадан тұруы керек', 'watchTogether.codeMustBe5Chars' => 'Сеанс коды 5 таңбадан тұруы керек',
_ => null,
} ?? switch (path) {
'watchTogether.joinInstructions' => 'Ұйымдастырушының сеанс кодын енгізіңіз.', 'watchTogether.joinInstructions' => 'Ұйымдастырушының сеанс кодын енгізіңіз.',
'watchTogether.failedToCreate' => 'Сеансты жасау мүмкін болмады', 'watchTogether.failedToCreate' => 'Сеансты жасау мүмкін болмады',
_ => null,
} ?? switch (path) {
'watchTogether.failedToJoin' => 'Сеансқа қосылу мүмкін болмады', 'watchTogether.failedToJoin' => 'Сеансқа қосылу мүмкін болмады',
'watchTogether.sessionCodeCopied' => 'Сеанс коды көшірілді', 'watchTogether.sessionCodeCopied' => 'Сеанс коды көшірілді',
'watchTogether.relayUnreachable' => 'Реле сервері қолжетімсіз.', 'watchTogether.relayUnreachable' => 'Реле сервері қолжетімсіз.',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$ko extends Translations$mediaMenu$en {
@override String get shufflePlay => '무작위 재생'; @override String get shufflePlay => '무작위 재생';
@override String get shuffleNotAvailableOffline => '오프라인에서는 무작위 재생을 사용할 수 없습니다'; @override String get shuffleNotAvailableOffline => '오프라인에서는 무작위 재생을 사용할 수 없습니다';
@override String get fileInfo => '파일 정보'; @override String get fileInfo => '파일 정보';
@override String get deleteFromServer => '서버에서 삭제';
@override String get confirmDelete => '이 미디어와 해당 파일을 서버에서 삭제할까요?';
@override String get deleteMultipleWarning => '모든 에피소드와 파일이 포함됩니다.'; @override String get deleteMultipleWarning => '모든 에피소드와 파일이 포함됩니다.';
@override String get mediaDeletedSuccessfully => '미디어 항목이 성공적으로 삭제되었습니다'; @override String get mediaDeletedSuccessfully => '미디어 항목이 성공적으로 삭제되었습니다';
@override String get mediaFailedToDelete => '미디어 항목 삭제 실패'; @override String get mediaFailedToDelete => '미디어 항목 삭제 실패';
@@ -2580,8 +2578,6 @@ extension on TranslationsKo {
'mediaMenu.shufflePlay' => '무작위 재생', 'mediaMenu.shufflePlay' => '무작위 재생',
'mediaMenu.shuffleNotAvailableOffline' => '오프라인에서는 무작위 재생을 사용할 수 없습니다', 'mediaMenu.shuffleNotAvailableOffline' => '오프라인에서는 무작위 재생을 사용할 수 없습니다',
'mediaMenu.fileInfo' => '파일 정보', 'mediaMenu.fileInfo' => '파일 정보',
'mediaMenu.deleteFromServer' => '서버에서 삭제',
'mediaMenu.confirmDelete' => '이 미디어와 해당 파일을 서버에서 삭제할까요?',
'mediaMenu.deleteMultipleWarning' => '모든 에피소드와 파일이 포함됩니다.', 'mediaMenu.deleteMultipleWarning' => '모든 에피소드와 파일이 포함됩니다.',
'mediaMenu.mediaDeletedSuccessfully' => '미디어 항목이 성공적으로 삭제되었습니다', 'mediaMenu.mediaDeletedSuccessfully' => '미디어 항목이 성공적으로 삭제되었습니다',
'mediaMenu.mediaFailedToDelete' => '미디어 항목 삭제 실패', 'mediaMenu.mediaFailedToDelete' => '미디어 항목 삭제 실패',
@@ -2700,10 +2696,10 @@ extension on TranslationsKo {
'videoControls.language' => '언어', 'videoControls.language' => '언어',
'videoControls.noSubtitlesFound' => '자막을 찾을 수 없습니다', 'videoControls.noSubtitlesFound' => '자막을 찾을 수 없습니다',
'videoControls.subtitleDownloaded' => '자막이 다운로드되었습니다', 'videoControls.subtitleDownloaded' => '자막이 다운로드되었습니다',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => '자막을 다운로드했지만 선택할 수 없습니다', 'videoControls.subtitleDownloadedNotApplied' => '자막을 다운로드했지만 선택할 수 없습니다',
'videoControls.subtitleDownloadFailed' => '자막 다운로드에 실패했습니다', 'videoControls.subtitleDownloadFailed' => '자막 다운로드에 실패했습니다',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => '언어 검색...', 'videoControls.searchLanguages' => '언어 검색...',
'messages.markedAsWatched' => '시청 완료로 표시됨', 'messages.markedAsWatched' => '시청 완료로 표시됨',
'messages.markedAsUnwatched' => '미시청으로 표시됨', 'messages.markedAsUnwatched' => '미시청으로 표시됨',
@@ -3214,10 +3210,10 @@ extension on TranslationsKo {
'watchTogether.failedToJoin' => '세션 참여 실패', 'watchTogether.failedToJoin' => '세션 참여 실패',
'watchTogether.sessionCodeCopied' => '세션 코드가 클립보드에 복사되었습니다', 'watchTogether.sessionCodeCopied' => '세션 코드가 클립보드에 복사되었습니다',
'watchTogether.relayUnreachable' => '릴레이 서버에 연결할 수 없습니다. ISP 차단으로 함께 보기를 사용하지 못할 수 있습니다.', 'watchTogether.relayUnreachable' => '릴레이 서버에 연결할 수 없습니다. ISP 차단으로 함께 보기를 사용하지 못할 수 있습니다.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => '호스트에 재연결 중...', 'watchTogether.reconnectingToHost' => '호스트에 재연결 중...',
'watchTogether.currentPlayback' => '현재 재생', 'watchTogether.currentPlayback' => '현재 재생',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => '현재 재생 참여', 'watchTogether.joinCurrentPlayback' => '현재 재생 참여',
'watchTogether.joinCurrentPlaybackDescription' => '호스트가 현재 시청 중인 콘텐츠로 이동합니다', 'watchTogether.joinCurrentPlaybackDescription' => '호스트가 현재 시청 중인 콘텐츠로 이동합니다',
'watchTogether.failedToOpenCurrentPlayback' => '현재 재생을 열 수 없습니다', 'watchTogether.failedToOpenCurrentPlayback' => '현재 재생을 열 수 없습니다',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$nb extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Tilfeldig avspilling'; @override String get shufflePlay => 'Tilfeldig avspilling';
@override String get shuffleNotAvailableOffline => 'Tilfeldig avspilling er ikke tilgjengelig uten nett'; @override String get shuffleNotAvailableOffline => 'Tilfeldig avspilling er ikke tilgjengelig uten nett';
@override String get fileInfo => 'Filinformasjon'; @override String get fileInfo => 'Filinformasjon';
@override String get deleteFromServer => 'Slett fra server';
@override String get confirmDelete => 'Slette dette mediet og filene fra serveren?';
@override String get deleteMultipleWarning => 'Dette inkluderer alle episoder og deres filer.'; @override String get deleteMultipleWarning => 'Dette inkluderer alle episoder og deres filer.';
@override String get mediaDeletedSuccessfully => 'Medieelement slettet'; @override String get mediaDeletedSuccessfully => 'Medieelement slettet';
@override String get mediaFailedToDelete => 'Kunne ikke slette medieelement'; @override String get mediaFailedToDelete => 'Kunne ikke slette medieelement';
@@ -2583,8 +2581,6 @@ extension on TranslationsNb {
'mediaMenu.shufflePlay' => 'Tilfeldig avspilling', 'mediaMenu.shufflePlay' => 'Tilfeldig avspilling',
'mediaMenu.shuffleNotAvailableOffline' => 'Tilfeldig avspilling er ikke tilgjengelig uten nett', 'mediaMenu.shuffleNotAvailableOffline' => 'Tilfeldig avspilling er ikke tilgjengelig uten nett',
'mediaMenu.fileInfo' => 'Filinformasjon', 'mediaMenu.fileInfo' => 'Filinformasjon',
'mediaMenu.deleteFromServer' => 'Slett fra server',
'mediaMenu.confirmDelete' => 'Slette dette mediet og filene fra serveren?',
'mediaMenu.deleteMultipleWarning' => 'Dette inkluderer alle episoder og deres filer.', 'mediaMenu.deleteMultipleWarning' => 'Dette inkluderer alle episoder og deres filer.',
'mediaMenu.mediaDeletedSuccessfully' => 'Medieelement slettet', 'mediaMenu.mediaDeletedSuccessfully' => 'Medieelement slettet',
'mediaMenu.mediaFailedToDelete' => 'Kunne ikke slette medieelement', 'mediaMenu.mediaFailedToDelete' => 'Kunne ikke slette medieelement',
@@ -2703,10 +2699,10 @@ extension on TranslationsNb {
'videoControls.language' => 'Språk', 'videoControls.language' => 'Språk',
'videoControls.noSubtitlesFound' => 'Ingen undertekster funnet', 'videoControls.noSubtitlesFound' => 'Ingen undertekster funnet',
'videoControls.subtitleDownloaded' => 'Undertekst lastet ned', 'videoControls.subtitleDownloaded' => 'Undertekst lastet ned',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'Underteksten ble lastet ned, men kunne ikke velges', 'videoControls.subtitleDownloadedNotApplied' => 'Underteksten ble lastet ned, men kunne ikke velges',
'videoControls.subtitleDownloadFailed' => 'Kunne ikke laste ned undertekst', 'videoControls.subtitleDownloadFailed' => 'Kunne ikke laste ned undertekst',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Søk etter språk...', 'videoControls.searchLanguages' => 'Søk etter språk...',
'messages.markedAsWatched' => 'Merket som sett', 'messages.markedAsWatched' => 'Merket som sett',
'messages.markedAsUnwatched' => 'Merket som usett', 'messages.markedAsUnwatched' => 'Merket som usett',
@@ -3217,10 +3213,10 @@ extension on TranslationsNb {
'watchTogether.failedToJoin' => 'Kunne ikke bli med i økt', 'watchTogether.failedToJoin' => 'Kunne ikke bli med i økt',
'watchTogether.sessionCodeCopied' => 'Øktkode kopiert til utklippstavle', 'watchTogether.sessionCodeCopied' => 'Øktkode kopiert til utklippstavle',
'watchTogether.relayUnreachable' => 'Reléserveren kan ikke nås. Blokkering hos internettleverandøren kan hindre Se sammen.', 'watchTogether.relayUnreachable' => 'Reléserveren kan ikke nås. Blokkering hos internettleverandøren kan hindre Se sammen.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Kobler til verten på nytt...', 'watchTogether.reconnectingToHost' => 'Kobler til verten på nytt...',
'watchTogether.currentPlayback' => 'Gjeldende avspilling', 'watchTogether.currentPlayback' => 'Gjeldende avspilling',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Bli med i gjeldende avspilling', 'watchTogether.joinCurrentPlayback' => 'Bli med i gjeldende avspilling',
'watchTogether.joinCurrentPlaybackDescription' => 'Hopp tilbake til det verten ser på nå', 'watchTogether.joinCurrentPlaybackDescription' => 'Hopp tilbake til det verten ser på nå',
'watchTogether.failedToOpenCurrentPlayback' => 'Kunne ikke åpne gjeldende avspilling', 'watchTogether.failedToOpenCurrentPlayback' => 'Kunne ikke åpne gjeldende avspilling',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$nl extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Willekeurig afspelen'; @override String get shufflePlay => 'Willekeurig afspelen';
@override String get shuffleNotAvailableOffline => 'Willekeurig afspelen is offline niet beschikbaar'; @override String get shuffleNotAvailableOffline => 'Willekeurig afspelen is offline niet beschikbaar';
@override String get fileInfo => 'Bestandsinformatie'; @override String get fileInfo => 'Bestandsinformatie';
@override String get deleteFromServer => 'Van server verwijderen';
@override String get confirmDelete => 'Deze media en bestanden van je server verwijderen?';
@override String get deleteMultipleWarning => 'Dit omvat alle afleveringen en hun bestanden.'; @override String get deleteMultipleWarning => 'Dit omvat alle afleveringen en hun bestanden.';
@override String get mediaDeletedSuccessfully => 'Media-item succesvol verwijderd'; @override String get mediaDeletedSuccessfully => 'Media-item succesvol verwijderd';
@override String get mediaFailedToDelete => 'Verwijderen van media-item mislukt'; @override String get mediaFailedToDelete => 'Verwijderen van media-item mislukt';
@@ -2583,8 +2581,6 @@ extension on TranslationsNl {
'mediaMenu.shufflePlay' => 'Willekeurig afspelen', 'mediaMenu.shufflePlay' => 'Willekeurig afspelen',
'mediaMenu.shuffleNotAvailableOffline' => 'Willekeurig afspelen is offline niet beschikbaar', 'mediaMenu.shuffleNotAvailableOffline' => 'Willekeurig afspelen is offline niet beschikbaar',
'mediaMenu.fileInfo' => 'Bestandsinformatie', 'mediaMenu.fileInfo' => 'Bestandsinformatie',
'mediaMenu.deleteFromServer' => 'Van server verwijderen',
'mediaMenu.confirmDelete' => 'Deze media en bestanden van je server verwijderen?',
'mediaMenu.deleteMultipleWarning' => 'Dit omvat alle afleveringen en hun bestanden.', 'mediaMenu.deleteMultipleWarning' => 'Dit omvat alle afleveringen en hun bestanden.',
'mediaMenu.mediaDeletedSuccessfully' => 'Media-item succesvol verwijderd', 'mediaMenu.mediaDeletedSuccessfully' => 'Media-item succesvol verwijderd',
'mediaMenu.mediaFailedToDelete' => 'Verwijderen van media-item mislukt', 'mediaMenu.mediaFailedToDelete' => 'Verwijderen van media-item mislukt',
@@ -2703,10 +2699,10 @@ extension on TranslationsNl {
'videoControls.language' => 'Taal', 'videoControls.language' => 'Taal',
'videoControls.noSubtitlesFound' => 'Geen ondertitels gevonden', 'videoControls.noSubtitlesFound' => 'Geen ondertitels gevonden',
'videoControls.subtitleDownloaded' => 'Ondertitel gedownload', 'videoControls.subtitleDownloaded' => 'Ondertitel gedownload',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'De ondertiteling is gedownload, maar kon niet worden geselecteerd', 'videoControls.subtitleDownloadedNotApplied' => 'De ondertiteling is gedownload, maar kon niet worden geselecteerd',
'videoControls.subtitleDownloadFailed' => 'Ondertitel downloaden mislukt', 'videoControls.subtitleDownloadFailed' => 'Ondertitel downloaden mislukt',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Talen zoeken...', 'videoControls.searchLanguages' => 'Talen zoeken...',
'messages.markedAsWatched' => 'Gemarkeerd als gekeken', 'messages.markedAsWatched' => 'Gemarkeerd als gekeken',
'messages.markedAsUnwatched' => 'Gemarkeerd als ongekeken', 'messages.markedAsUnwatched' => 'Gemarkeerd als ongekeken',
@@ -3217,10 +3213,10 @@ extension on TranslationsNl {
'watchTogether.failedToJoin' => 'Deelnemen aan sessie mislukt', 'watchTogether.failedToJoin' => 'Deelnemen aan sessie mislukt',
'watchTogether.sessionCodeCopied' => 'Sessiecode naar het klembord gekopieerd', 'watchTogether.sessionCodeCopied' => 'Sessiecode naar het klembord gekopieerd',
'watchTogether.relayUnreachable' => 'De relayserver is onbereikbaar. Een blokkering door je internetprovider kan Samen kijken verhinderen.', 'watchTogether.relayUnreachable' => 'De relayserver is onbereikbaar. Een blokkering door je internetprovider kan Samen kijken verhinderen.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Opnieuw verbinden met host...', 'watchTogether.reconnectingToHost' => 'Opnieuw verbinden met host...',
'watchTogether.currentPlayback' => 'Wat nu wordt afgespeeld', 'watchTogether.currentPlayback' => 'Wat nu wordt afgespeeld',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Deelnemen aan huidige weergave', 'watchTogether.joinCurrentPlayback' => 'Deelnemen aan huidige weergave',
'watchTogether.joinCurrentPlaybackDescription' => 'Ga terug naar wat de host nu kijkt', 'watchTogether.joinCurrentPlaybackDescription' => 'Ga terug naar wat de host nu kijkt',
'watchTogether.failedToOpenCurrentPlayback' => 'Wat nu wordt afgespeeld kon niet worden geopend', 'watchTogether.failedToOpenCurrentPlayback' => 'Wat nu wordt afgespeeld kon niet worden geopend',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$pl extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Odtwarzanie losowe'; @override String get shufflePlay => 'Odtwarzanie losowe';
@override String get shuffleNotAvailableOffline => 'Odtwarzanie losowe nie jest dostępne offline'; @override String get shuffleNotAvailableOffline => 'Odtwarzanie losowe nie jest dostępne offline';
@override String get fileInfo => 'Informacje o pliku'; @override String get fileInfo => 'Informacje o pliku';
@override String get deleteFromServer => 'Usuń z serwera';
@override String get confirmDelete => 'Usunąć to medium i jego pliki z serwera?';
@override String get deleteMultipleWarning => 'Obejmuje to wszystkie odcinki i ich pliki.'; @override String get deleteMultipleWarning => 'Obejmuje to wszystkie odcinki i ich pliki.';
@override String get mediaDeletedSuccessfully => 'Usunięto element multimedialny'; @override String get mediaDeletedSuccessfully => 'Usunięto element multimedialny';
@override String get mediaFailedToDelete => 'Nie udało się usunąć elementu multimedialnego'; @override String get mediaFailedToDelete => 'Nie udało się usunąć elementu multimedialnego';
@@ -2589,8 +2587,6 @@ extension on TranslationsPl {
'mediaMenu.shufflePlay' => 'Odtwarzanie losowe', 'mediaMenu.shufflePlay' => 'Odtwarzanie losowe',
'mediaMenu.shuffleNotAvailableOffline' => 'Odtwarzanie losowe nie jest dostępne offline', 'mediaMenu.shuffleNotAvailableOffline' => 'Odtwarzanie losowe nie jest dostępne offline',
'mediaMenu.fileInfo' => 'Informacje o pliku', 'mediaMenu.fileInfo' => 'Informacje o pliku',
'mediaMenu.deleteFromServer' => 'Usuń z serwera',
'mediaMenu.confirmDelete' => 'Usunąć to medium i jego pliki z serwera?',
'mediaMenu.deleteMultipleWarning' => 'Obejmuje to wszystkie odcinki i ich pliki.', 'mediaMenu.deleteMultipleWarning' => 'Obejmuje to wszystkie odcinki i ich pliki.',
'mediaMenu.mediaDeletedSuccessfully' => 'Usunięto element multimedialny', 'mediaMenu.mediaDeletedSuccessfully' => 'Usunięto element multimedialny',
'mediaMenu.mediaFailedToDelete' => 'Nie udało się usunąć elementu multimedialnego', 'mediaMenu.mediaFailedToDelete' => 'Nie udało się usunąć elementu multimedialnego',
@@ -2709,10 +2705,10 @@ extension on TranslationsPl {
'videoControls.language' => 'Język', 'videoControls.language' => 'Język',
'videoControls.noSubtitlesFound' => 'Nie znaleziono napisów', 'videoControls.noSubtitlesFound' => 'Nie znaleziono napisów',
'videoControls.subtitleDownloaded' => 'Napisy pobrane', 'videoControls.subtitleDownloaded' => 'Napisy pobrane',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'Napisy zostały pobrane, ale nie można ich było wybrać', 'videoControls.subtitleDownloadedNotApplied' => 'Napisy zostały pobrane, ale nie można ich było wybrać',
'videoControls.subtitleDownloadFailed' => 'Nie udało się pobrać napisów', 'videoControls.subtitleDownloadFailed' => 'Nie udało się pobrać napisów',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Szukaj języków...', 'videoControls.searchLanguages' => 'Szukaj języków...',
'messages.markedAsWatched' => 'Oznaczono jako obejrzane', 'messages.markedAsWatched' => 'Oznaczono jako obejrzane',
'messages.markedAsUnwatched' => 'Oznaczono jako nieobejrzane', 'messages.markedAsUnwatched' => 'Oznaczono jako nieobejrzane',
@@ -3223,10 +3219,10 @@ extension on TranslationsPl {
'watchTogether.failedToJoin' => 'Nie udało się dołączyć do sesji', 'watchTogether.failedToJoin' => 'Nie udało się dołączyć do sesji',
'watchTogether.sessionCodeCopied' => 'Kod sesji skopiowany do schowka', 'watchTogether.sessionCodeCopied' => 'Kod sesji skopiowany do schowka',
'watchTogether.relayUnreachable' => 'Serwer pośredniczący jest nieosiągalny. Blokada operatora internetowego może uniemożliwiać korzystanie z funkcji „Oglądaj razem”.', 'watchTogether.relayUnreachable' => 'Serwer pośredniczący jest nieosiągalny. Blokada operatora internetowego może uniemożliwiać korzystanie z funkcji „Oglądaj razem”.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Ponowne łączenie z gospodarzem...', 'watchTogether.reconnectingToHost' => 'Ponowne łączenie z gospodarzem...',
'watchTogether.currentPlayback' => 'Bieżące odtwarzanie', 'watchTogether.currentPlayback' => 'Bieżące odtwarzanie',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Dołącz do bieżącego odtwarzania', 'watchTogether.joinCurrentPlayback' => 'Dołącz do bieżącego odtwarzania',
'watchTogether.joinCurrentPlaybackDescription' => 'Wróć do treści oglądanej obecnie przez gospodarza', 'watchTogether.joinCurrentPlaybackDescription' => 'Wróć do treści oglądanej obecnie przez gospodarza',
'watchTogether.failedToOpenCurrentPlayback' => 'Nie udało się otworzyć bieżącego odtwarzania', 'watchTogether.failedToOpenCurrentPlayback' => 'Nie udało się otworzyć bieżącego odtwarzania',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$pt extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Reprodução Aleatória'; @override String get shufflePlay => 'Reprodução Aleatória';
@override String get shuffleNotAvailableOffline => 'Reprodução aleatória indisponível offline'; @override String get shuffleNotAvailableOffline => 'Reprodução aleatória indisponível offline';
@override String get fileInfo => 'Informações do arquivo'; @override String get fileInfo => 'Informações do arquivo';
@override String get deleteFromServer => 'Excluir do servidor';
@override String get confirmDelete => 'Excluir esta mídia e seus arquivos do servidor?';
@override String get deleteMultipleWarning => 'Isso inclui todos os episódios e seus arquivos.'; @override String get deleteMultipleWarning => 'Isso inclui todos os episódios e seus arquivos.';
@override String get mediaDeletedSuccessfully => 'Item de mídia excluído com sucesso'; @override String get mediaDeletedSuccessfully => 'Item de mídia excluído com sucesso';
@override String get mediaFailedToDelete => 'Falha ao excluir item de mídia'; @override String get mediaFailedToDelete => 'Falha ao excluir item de mídia';
@@ -2583,8 +2581,6 @@ extension on TranslationsPt {
'mediaMenu.shufflePlay' => 'Reprodução Aleatória', 'mediaMenu.shufflePlay' => 'Reprodução Aleatória',
'mediaMenu.shuffleNotAvailableOffline' => 'Reprodução aleatória indisponível offline', 'mediaMenu.shuffleNotAvailableOffline' => 'Reprodução aleatória indisponível offline',
'mediaMenu.fileInfo' => 'Informações do arquivo', 'mediaMenu.fileInfo' => 'Informações do arquivo',
'mediaMenu.deleteFromServer' => 'Excluir do servidor',
'mediaMenu.confirmDelete' => 'Excluir esta mídia e seus arquivos do servidor?',
'mediaMenu.deleteMultipleWarning' => 'Isso inclui todos os episódios e seus arquivos.', 'mediaMenu.deleteMultipleWarning' => 'Isso inclui todos os episódios e seus arquivos.',
'mediaMenu.mediaDeletedSuccessfully' => 'Item de mídia excluído com sucesso', 'mediaMenu.mediaDeletedSuccessfully' => 'Item de mídia excluído com sucesso',
'mediaMenu.mediaFailedToDelete' => 'Falha ao excluir item de mídia', 'mediaMenu.mediaFailedToDelete' => 'Falha ao excluir item de mídia',
@@ -2703,10 +2699,10 @@ extension on TranslationsPt {
'videoControls.language' => 'Idioma', 'videoControls.language' => 'Idioma',
'videoControls.noSubtitlesFound' => 'Nenhuma legenda encontrada', 'videoControls.noSubtitlesFound' => 'Nenhuma legenda encontrada',
'videoControls.subtitleDownloaded' => 'Legenda baixada', 'videoControls.subtitleDownloaded' => 'Legenda baixada',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'A legenda foi baixada, mas não foi possível selecioná-la', 'videoControls.subtitleDownloadedNotApplied' => 'A legenda foi baixada, mas não foi possível selecioná-la',
'videoControls.subtitleDownloadFailed' => 'Falha ao baixar legenda', 'videoControls.subtitleDownloadFailed' => 'Falha ao baixar legenda',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Pesquisar idiomas...', 'videoControls.searchLanguages' => 'Pesquisar idiomas...',
'messages.markedAsWatched' => 'Marcado como assistido', 'messages.markedAsWatched' => 'Marcado como assistido',
'messages.markedAsUnwatched' => 'Marcado como não assistido', 'messages.markedAsUnwatched' => 'Marcado como não assistido',
@@ -3217,10 +3213,10 @@ extension on TranslationsPt {
'watchTogether.failedToJoin' => 'Falha ao entrar na sessão', 'watchTogether.failedToJoin' => 'Falha ao entrar na sessão',
'watchTogether.sessionCodeCopied' => 'Código da sessão copiado para a área de transferência', 'watchTogether.sessionCodeCopied' => 'Código da sessão copiado para a área de transferência',
'watchTogether.relayUnreachable' => 'Servidor de retransmissão inacessível. O bloqueio pelo provedor de internet pode impedir o uso do Assistir Juntos.', 'watchTogether.relayUnreachable' => 'Servidor de retransmissão inacessível. O bloqueio pelo provedor de internet pode impedir o uso do Assistir Juntos.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Reconectando ao anfitrião...', 'watchTogether.reconnectingToHost' => 'Reconectando ao anfitrião...',
'watchTogether.currentPlayback' => 'Reprodução atual', 'watchTogether.currentPlayback' => 'Reprodução atual',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Entrar na reprodução atual', 'watchTogether.joinCurrentPlayback' => 'Entrar na reprodução atual',
'watchTogether.joinCurrentPlaybackDescription' => 'Voltar ao conteúdo que o anfitrião está assistindo agora', 'watchTogether.joinCurrentPlaybackDescription' => 'Voltar ao conteúdo que o anfitrião está assistindo agora',
'watchTogether.failedToOpenCurrentPlayback' => 'Falha ao abrir a reprodução atual', 'watchTogether.failedToOpenCurrentPlayback' => 'Falha ao abrir a reprodução atual',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$ru extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Случайное воспроизведение'; @override String get shufflePlay => 'Случайное воспроизведение';
@override String get shuffleNotAvailableOffline => 'Перемешивание недоступно офлайн'; @override String get shuffleNotAvailableOffline => 'Перемешивание недоступно офлайн';
@override String get fileInfo => 'Информация о файле'; @override String get fileInfo => 'Информация о файле';
@override String get deleteFromServer => 'Удалить с сервера';
@override String get confirmDelete => 'Удалить это медиа и его файлы с сервера?';
@override String get deleteMultipleWarning => 'Это включает все эпизоды и их файлы.'; @override String get deleteMultipleWarning => 'Это включает все эпизоды и их файлы.';
@override String get mediaDeletedSuccessfully => 'Медиаэлемент успешно удалён'; @override String get mediaDeletedSuccessfully => 'Медиаэлемент успешно удалён';
@override String get mediaFailedToDelete => 'Не удалось удалить медиаэлемент'; @override String get mediaFailedToDelete => 'Не удалось удалить медиаэлемент';
@@ -2589,8 +2587,6 @@ extension on TranslationsRu {
'mediaMenu.shufflePlay' => 'Случайное воспроизведение', 'mediaMenu.shufflePlay' => 'Случайное воспроизведение',
'mediaMenu.shuffleNotAvailableOffline' => 'Перемешивание недоступно офлайн', 'mediaMenu.shuffleNotAvailableOffline' => 'Перемешивание недоступно офлайн',
'mediaMenu.fileInfo' => 'Информация о файле', 'mediaMenu.fileInfo' => 'Информация о файле',
'mediaMenu.deleteFromServer' => 'Удалить с сервера',
'mediaMenu.confirmDelete' => 'Удалить это медиа и его файлы с сервера?',
'mediaMenu.deleteMultipleWarning' => 'Это включает все эпизоды и их файлы.', 'mediaMenu.deleteMultipleWarning' => 'Это включает все эпизоды и их файлы.',
'mediaMenu.mediaDeletedSuccessfully' => 'Медиаэлемент успешно удалён', 'mediaMenu.mediaDeletedSuccessfully' => 'Медиаэлемент успешно удалён',
'mediaMenu.mediaFailedToDelete' => 'Не удалось удалить медиаэлемент', 'mediaMenu.mediaFailedToDelete' => 'Не удалось удалить медиаэлемент',
@@ -2709,10 +2705,10 @@ extension on TranslationsRu {
'videoControls.language' => 'Язык', 'videoControls.language' => 'Язык',
'videoControls.noSubtitlesFound' => 'Субтитры не найдены', 'videoControls.noSubtitlesFound' => 'Субтитры не найдены',
'videoControls.subtitleDownloaded' => 'Субтитры загружены', 'videoControls.subtitleDownloaded' => 'Субтитры загружены',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'Субтитры загружены, но их не удалось выбрать', 'videoControls.subtitleDownloadedNotApplied' => 'Субтитры загружены, но их не удалось выбрать',
'videoControls.subtitleDownloadFailed' => 'Не удалось загрузить субтитры', 'videoControls.subtitleDownloadFailed' => 'Не удалось загрузить субтитры',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Поиск языков...', 'videoControls.searchLanguages' => 'Поиск языков...',
'messages.markedAsWatched' => 'Отмечено как просмотренное', 'messages.markedAsWatched' => 'Отмечено как просмотренное',
'messages.markedAsUnwatched' => 'Отмечено как непросмотренное', 'messages.markedAsUnwatched' => 'Отмечено как непросмотренное',
@@ -3223,10 +3219,10 @@ extension on TranslationsRu {
'watchTogether.failedToJoin' => 'Не удалось присоединиться к сессии', 'watchTogether.failedToJoin' => 'Не удалось присоединиться к сессии',
'watchTogether.sessionCodeCopied' => 'Код сессии скопирован в буфер обмена', 'watchTogether.sessionCodeCopied' => 'Код сессии скопирован в буфер обмена',
'watchTogether.relayUnreachable' => 'Сервер ретрансляции недоступен. Блокировка интернет-провайдером может помешать совместному просмотру.', 'watchTogether.relayUnreachable' => 'Сервер ретрансляции недоступен. Блокировка интернет-провайдером может помешать совместному просмотру.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Повторное подключение к организатору...', 'watchTogether.reconnectingToHost' => 'Повторное подключение к организатору...',
'watchTogether.currentPlayback' => 'Текущее воспроизведение', 'watchTogether.currentPlayback' => 'Текущее воспроизведение',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Присоединиться к текущему воспроизведению', 'watchTogether.joinCurrentPlayback' => 'Присоединиться к текущему воспроизведению',
'watchTogether.joinCurrentPlaybackDescription' => 'Вернуться к материалу, который сейчас смотрит организатор', 'watchTogether.joinCurrentPlaybackDescription' => 'Вернуться к материалу, который сейчас смотрит организатор',
'watchTogether.failedToOpenCurrentPlayback' => 'Не удалось открыть текущее воспроизведение', 'watchTogether.failedToOpenCurrentPlayback' => 'Не удалось открыть текущее воспроизведение',
+4 -8
View File
@@ -546,8 +546,6 @@ class _Translations$mediaMenu$sv extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Blanda uppspelning'; @override String get shufflePlay => 'Blanda uppspelning';
@override String get shuffleNotAvailableOffline => 'Blandad uppspelning är inte tillgänglig offline'; @override String get shuffleNotAvailableOffline => 'Blandad uppspelning är inte tillgänglig offline';
@override String get fileInfo => 'Filinformation'; @override String get fileInfo => 'Filinformation';
@override String get deleteFromServer => 'Ta bort från servern';
@override String get confirmDelete => 'Ta bort det här medieobjektet och dess filer från servern?';
@override String get deleteMultipleWarning => 'Detta omfattar alla avsnitt och deras filer.'; @override String get deleteMultipleWarning => 'Detta omfattar alla avsnitt och deras filer.';
@override String get mediaDeletedSuccessfully => 'Medieobjektet har tagits bort'; @override String get mediaDeletedSuccessfully => 'Medieobjektet har tagits bort';
@override String get mediaFailedToDelete => 'Det gick inte att ta bort medieobjektet'; @override String get mediaFailedToDelete => 'Det gick inte att ta bort medieobjektet';
@@ -2583,8 +2581,6 @@ extension on TranslationsSv {
'mediaMenu.shufflePlay' => 'Blanda uppspelning', 'mediaMenu.shufflePlay' => 'Blanda uppspelning',
'mediaMenu.shuffleNotAvailableOffline' => 'Blandad uppspelning är inte tillgänglig offline', 'mediaMenu.shuffleNotAvailableOffline' => 'Blandad uppspelning är inte tillgänglig offline',
'mediaMenu.fileInfo' => 'Filinformation', 'mediaMenu.fileInfo' => 'Filinformation',
'mediaMenu.deleteFromServer' => 'Ta bort från servern',
'mediaMenu.confirmDelete' => 'Ta bort det här medieobjektet och dess filer från servern?',
'mediaMenu.deleteMultipleWarning' => 'Detta omfattar alla avsnitt och deras filer.', 'mediaMenu.deleteMultipleWarning' => 'Detta omfattar alla avsnitt och deras filer.',
'mediaMenu.mediaDeletedSuccessfully' => 'Medieobjektet har tagits bort', 'mediaMenu.mediaDeletedSuccessfully' => 'Medieobjektet har tagits bort',
'mediaMenu.mediaFailedToDelete' => 'Det gick inte att ta bort medieobjektet', 'mediaMenu.mediaFailedToDelete' => 'Det gick inte att ta bort medieobjektet',
@@ -2703,10 +2699,10 @@ extension on TranslationsSv {
'videoControls.language' => 'Språk', 'videoControls.language' => 'Språk',
'videoControls.noSubtitlesFound' => 'Inga undertexter hittades', 'videoControls.noSubtitlesFound' => 'Inga undertexter hittades',
'videoControls.subtitleDownloaded' => 'Undertexten har laddats ned', 'videoControls.subtitleDownloaded' => 'Undertexten har laddats ned',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => 'Undertexten laddades ned men kunde inte väljas', 'videoControls.subtitleDownloadedNotApplied' => 'Undertexten laddades ned men kunde inte väljas',
'videoControls.subtitleDownloadFailed' => 'Det gick inte att ladda ned undertexten', 'videoControls.subtitleDownloadFailed' => 'Det gick inte att ladda ned undertexten',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => 'Sök språk...', 'videoControls.searchLanguages' => 'Sök språk...',
'messages.markedAsWatched' => 'Markerad som sedd', 'messages.markedAsWatched' => 'Markerad som sedd',
'messages.markedAsUnwatched' => 'Markerad som osedd', 'messages.markedAsUnwatched' => 'Markerad som osedd',
@@ -3217,10 +3213,10 @@ extension on TranslationsSv {
'watchTogether.failedToJoin' => 'Det gick inte att gå med i sessionen', 'watchTogether.failedToJoin' => 'Det gick inte att gå med i sessionen',
'watchTogether.sessionCodeCopied' => 'Sessionskoden har kopierats till urklipp', 'watchTogether.sessionCodeCopied' => 'Sessionskoden har kopierats till urklipp',
'watchTogether.relayUnreachable' => 'Reläservern kan inte nås. Din internetleverantör kan blockera Titta tillsammans.', 'watchTogether.relayUnreachable' => 'Reläservern kan inte nås. Din internetleverantör kan blockera Titta tillsammans.',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => 'Återansluter till värden...', 'watchTogether.reconnectingToHost' => 'Återansluter till värden...',
'watchTogether.currentPlayback' => 'Aktuell uppspelning', 'watchTogether.currentPlayback' => 'Aktuell uppspelning',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => 'Gå med i aktuell uppspelning', 'watchTogether.joinCurrentPlayback' => 'Gå med i aktuell uppspelning',
'watchTogether.joinCurrentPlaybackDescription' => 'Hoppa tillbaka till det värden tittar på just nu', 'watchTogether.joinCurrentPlaybackDescription' => 'Hoppa tillbaka till det värden tittar på just nu',
'watchTogether.failedToOpenCurrentPlayback' => 'Kunde inte öppna aktuell uppspelning', 'watchTogether.failedToOpenCurrentPlayback' => 'Kunde inte öppna aktuell uppspelning',
+4 -8
View File
@@ -549,8 +549,6 @@ class _Translations$mediaMenu$tr extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Karışık Oynat'; @override String get shufflePlay => 'Karışık Oynat';
@override String get shuffleNotAvailableOffline => 'Karışık oynatma çevrimdışıyken kullanılamaz'; @override String get shuffleNotAvailableOffline => 'Karışık oynatma çevrimdışıyken kullanılamaz';
@override String get fileInfo => 'Dosya Bilgisi'; @override String get fileInfo => 'Dosya Bilgisi';
@override String get deleteFromServer => 'Sunucudan sil';
@override String get confirmDelete => 'Bu medya ve dosyaları sunucunuzdan silinsin mi?';
@override String get deleteMultipleWarning => 'Bu işlem tüm bölümleri ve dosyalarını içerir.'; @override String get deleteMultipleWarning => 'Bu işlem tüm bölümleri ve dosyalarını içerir.';
@override String get mediaDeletedSuccessfully => 'Medya ögesi başarıyla silindi'; @override String get mediaDeletedSuccessfully => 'Medya ögesi başarıyla silindi';
@override String get mediaFailedToDelete => 'Medya ögesi silinemedi'; @override String get mediaFailedToDelete => 'Medya ögesi silinemedi';
@@ -2597,8 +2595,6 @@ extension on TranslationsTr {
'mediaMenu.shufflePlay' => 'Karışık Oynat', 'mediaMenu.shufflePlay' => 'Karışık Oynat',
'mediaMenu.shuffleNotAvailableOffline' => 'Karışık oynatma çevrimdışıyken kullanılamaz', 'mediaMenu.shuffleNotAvailableOffline' => 'Karışık oynatma çevrimdışıyken kullanılamaz',
'mediaMenu.fileInfo' => 'Dosya Bilgisi', 'mediaMenu.fileInfo' => 'Dosya Bilgisi',
'mediaMenu.deleteFromServer' => 'Sunucudan sil',
'mediaMenu.confirmDelete' => 'Bu medya ve dosyaları sunucunuzdan silinsin mi?',
'mediaMenu.deleteMultipleWarning' => 'Bu işlem tüm bölümleri ve dosyalarını içerir.', 'mediaMenu.deleteMultipleWarning' => 'Bu işlem tüm bölümleri ve dosyalarını içerir.',
'mediaMenu.mediaDeletedSuccessfully' => 'Medya ögesi başarıyla silindi', 'mediaMenu.mediaDeletedSuccessfully' => 'Medya ögesi başarıyla silindi',
'mediaMenu.mediaFailedToDelete' => 'Medya ögesi silinemedi', 'mediaMenu.mediaFailedToDelete' => 'Medya ögesi silinemedi',
@@ -2714,10 +2710,10 @@ extension on TranslationsTr {
'videoControls.queue' => 'Kuyruk', 'videoControls.queue' => 'Kuyruk',
'videoControls.noQueueItems' => 'Kuyrukta öge yok', 'videoControls.noQueueItems' => 'Kuyrukta öge yok',
'videoControls.searchSubtitles' => 'Altyazı Ara', 'videoControls.searchSubtitles' => 'Altyazı Ara',
_ => null,
} ?? switch (path) {
'videoControls.language' => 'Dil', 'videoControls.language' => 'Dil',
'videoControls.noSubtitlesFound' => 'Altyazı bulunamadı', 'videoControls.noSubtitlesFound' => 'Altyazı bulunamadı',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Altyazı indirildi', 'videoControls.subtitleDownloaded' => 'Altyazı indirildi',
'videoControls.subtitleDownloadedNotApplied' => 'Altyazı indirildi ancak seçilemedi', 'videoControls.subtitleDownloadedNotApplied' => 'Altyazı indirildi ancak seçilemedi',
'videoControls.subtitleDownloadFailed' => 'Altyazı indirilemedi', 'videoControls.subtitleDownloadFailed' => 'Altyazı indirilemedi',
@@ -3228,10 +3224,10 @@ extension on TranslationsTr {
'watchTogether.pasteFromClipboard' => 'Panodan yapıştır', 'watchTogether.pasteFromClipboard' => 'Panodan yapıştır',
'watchTogether.pleaseEnterCode' => 'Lütfen bir oturum kodu girin', 'watchTogether.pleaseEnterCode' => 'Lütfen bir oturum kodu girin',
'watchTogether.codeMustBe5Chars' => 'Oturum kodu 5 karakter olmalıdır', 'watchTogether.codeMustBe5Chars' => 'Oturum kodu 5 karakter olmalıdır',
_ => null,
} ?? switch (path) {
'watchTogether.joinInstructions' => 'Katılmak için kurucunun oturum kodunu girin.', 'watchTogether.joinInstructions' => 'Katılmak için kurucunun oturum kodunu girin.',
'watchTogether.failedToCreate' => 'Oturum oluşturulamadı', 'watchTogether.failedToCreate' => 'Oturum oluşturulamadı',
_ => null,
} ?? switch (path) {
'watchTogether.failedToJoin' => 'Oturuma katılınamadı', 'watchTogether.failedToJoin' => 'Oturuma katılınamadı',
'watchTogether.sessionCodeCopied' => 'Oturum kodu panoya kopyalandı', 'watchTogether.sessionCodeCopied' => 'Oturum kodu panoya kopyalandı',
'watchTogether.relayUnreachable' => 'Aktarıcı sunucusuna ulaşılamıyor. İSS engellemesi Birlikte İzle\'yi önleyebilir.', 'watchTogether.relayUnreachable' => 'Aktarıcı sunucusuna ulaşılamıyor. İSS engellemesi Birlikte İzle\'yi önleyebilir.',
+4 -8
View File
@@ -549,8 +549,6 @@ class _Translations$mediaMenu$uz extends Translations$mediaMenu$en {
@override String get shufflePlay => 'Aralashtirib ijro etish'; @override String get shufflePlay => 'Aralashtirib ijro etish';
@override String get shuffleNotAvailableOffline => 'Aralashtirib ijro etish oflayn rejimda mavjud emas'; @override String get shuffleNotAvailableOffline => 'Aralashtirib ijro etish oflayn rejimda mavjud emas';
@override String get fileInfo => 'Fayl haqida maʼlumot'; @override String get fileInfo => 'Fayl haqida maʼlumot';
@override String get deleteFromServer => 'Serverdan oʻchirish';
@override String get confirmDelete => 'Ushbu media va fayllar serverdan oʻchirilsinmi?';
@override String get deleteMultipleWarning => 'Bu barcha qismlar va fayllarga taʼsir qiladi.'; @override String get deleteMultipleWarning => 'Bu barcha qismlar va fayllarga taʼsir qiladi.';
@override String get mediaDeletedSuccessfully => 'Media elementi muvaffaqiyatli oʻchirildi'; @override String get mediaDeletedSuccessfully => 'Media elementi muvaffaqiyatli oʻchirildi';
@override String get mediaFailedToDelete => 'Media elementini oʻchirib boʻlmadi'; @override String get mediaFailedToDelete => 'Media elementini oʻchirib boʻlmadi';
@@ -2597,8 +2595,6 @@ extension on TranslationsUz {
'mediaMenu.shufflePlay' => 'Aralashtirib ijro etish', 'mediaMenu.shufflePlay' => 'Aralashtirib ijro etish',
'mediaMenu.shuffleNotAvailableOffline' => 'Aralashtirib ijro etish oflayn rejimda mavjud emas', 'mediaMenu.shuffleNotAvailableOffline' => 'Aralashtirib ijro etish oflayn rejimda mavjud emas',
'mediaMenu.fileInfo' => 'Fayl haqida maʼlumot', 'mediaMenu.fileInfo' => 'Fayl haqida maʼlumot',
'mediaMenu.deleteFromServer' => 'Serverdan oʻchirish',
'mediaMenu.confirmDelete' => 'Ushbu media va fayllar serverdan oʻchirilsinmi?',
'mediaMenu.deleteMultipleWarning' => 'Bu barcha qismlar va fayllarga taʼsir qiladi.', 'mediaMenu.deleteMultipleWarning' => 'Bu barcha qismlar va fayllarga taʼsir qiladi.',
'mediaMenu.mediaDeletedSuccessfully' => 'Media elementi muvaffaqiyatli oʻchirildi', 'mediaMenu.mediaDeletedSuccessfully' => 'Media elementi muvaffaqiyatli oʻchirildi',
'mediaMenu.mediaFailedToDelete' => 'Media elementini oʻchirib boʻlmadi', 'mediaMenu.mediaFailedToDelete' => 'Media elementini oʻchirib boʻlmadi',
@@ -2714,10 +2710,10 @@ extension on TranslationsUz {
'videoControls.queue' => 'Navbat', 'videoControls.queue' => 'Navbat',
'videoControls.noQueueItems' => 'Navbatda elementlar yoʻq', 'videoControls.noQueueItems' => 'Navbatda elementlar yoʻq',
'videoControls.searchSubtitles' => 'Subtitr qidirish', 'videoControls.searchSubtitles' => 'Subtitr qidirish',
_ => null,
} ?? switch (path) {
'videoControls.language' => 'Til', 'videoControls.language' => 'Til',
'videoControls.noSubtitlesFound' => 'Subtitr topilmadi', 'videoControls.noSubtitlesFound' => 'Subtitr topilmadi',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloaded' => 'Subtitr yuklab olindi', 'videoControls.subtitleDownloaded' => 'Subtitr yuklab olindi',
'videoControls.subtitleDownloadedNotApplied' => 'Subtitr yuklab olindi, lekin qoʻllanilmadi', 'videoControls.subtitleDownloadedNotApplied' => 'Subtitr yuklab olindi, lekin qoʻllanilmadi',
'videoControls.subtitleDownloadFailed' => 'Subtitrni yuklab boʻlmadi', 'videoControls.subtitleDownloadFailed' => 'Subtitrni yuklab boʻlmadi',
@@ -3228,10 +3224,10 @@ extension on TranslationsUz {
'watchTogether.pasteFromClipboard' => 'Xotiradan joylash', 'watchTogether.pasteFromClipboard' => 'Xotiradan joylash',
'watchTogether.pleaseEnterCode' => 'Seans kodini kiriting', 'watchTogether.pleaseEnterCode' => 'Seans kodini kiriting',
'watchTogether.codeMustBe5Chars' => 'Seans kodi 5 ta belgidan iborat boʻlishi kerak', 'watchTogether.codeMustBe5Chars' => 'Seans kodi 5 ta belgidan iborat boʻlishi kerak',
_ => null,
} ?? switch (path) {
'watchTogether.joinInstructions' => 'Tashkilotchining seans kodini kiriting.', 'watchTogether.joinInstructions' => 'Tashkilotchining seans kodini kiriting.',
'watchTogether.failedToCreate' => 'Seansni yaratib boʻlmadi', 'watchTogether.failedToCreate' => 'Seansni yaratib boʻlmadi',
_ => null,
} ?? switch (path) {
'watchTogether.failedToJoin' => 'Seansga qoʻshilib boʻlmadi', 'watchTogether.failedToJoin' => 'Seansga qoʻshilib boʻlmadi',
'watchTogether.sessionCodeCopied' => 'Seans kodi nusxalandi', 'watchTogether.sessionCodeCopied' => 'Seans kodi nusxalandi',
'watchTogether.relayUnreachable' => 'Rele serveriga ulanib boʻlmadi.', 'watchTogether.relayUnreachable' => 'Rele serveriga ulanib boʻlmadi.',
+4 -8
View File
@@ -546,8 +546,6 @@ class Translations$mediaMenu$zh extends Translations$mediaMenu$en {
@override String get shufflePlay => '随机播放'; @override String get shufflePlay => '随机播放';
@override String get shuffleNotAvailableOffline => '离线时无法随机播放'; @override String get shuffleNotAvailableOffline => '离线时无法随机播放';
@override String get fileInfo => '文件信息'; @override String get fileInfo => '文件信息';
@override String get deleteFromServer => '从服务器删除';
@override String get confirmDelete => '要从服务器删除此媒体及其文件吗?';
@override String get deleteMultipleWarning => '这包括所有剧集及其文件。'; @override String get deleteMultipleWarning => '这包括所有剧集及其文件。';
@override String get mediaDeletedSuccessfully => '媒体项已成功删除'; @override String get mediaDeletedSuccessfully => '媒体项已成功删除';
@override String get mediaFailedToDelete => '删除媒体项失败'; @override String get mediaFailedToDelete => '删除媒体项失败';
@@ -2580,8 +2578,6 @@ extension on TranslationsZh {
'mediaMenu.shufflePlay' => '随机播放', 'mediaMenu.shufflePlay' => '随机播放',
'mediaMenu.shuffleNotAvailableOffline' => '离线时无法随机播放', 'mediaMenu.shuffleNotAvailableOffline' => '离线时无法随机播放',
'mediaMenu.fileInfo' => '文件信息', 'mediaMenu.fileInfo' => '文件信息',
'mediaMenu.deleteFromServer' => '从服务器删除',
'mediaMenu.confirmDelete' => '要从服务器删除此媒体及其文件吗?',
'mediaMenu.deleteMultipleWarning' => '这包括所有剧集及其文件。', 'mediaMenu.deleteMultipleWarning' => '这包括所有剧集及其文件。',
'mediaMenu.mediaDeletedSuccessfully' => '媒体项已成功删除', 'mediaMenu.mediaDeletedSuccessfully' => '媒体项已成功删除',
'mediaMenu.mediaFailedToDelete' => '删除媒体项失败', 'mediaMenu.mediaFailedToDelete' => '删除媒体项失败',
@@ -2700,10 +2696,10 @@ extension on TranslationsZh {
'videoControls.language' => '语言', 'videoControls.language' => '语言',
'videoControls.noSubtitlesFound' => '未找到字幕', 'videoControls.noSubtitlesFound' => '未找到字幕',
'videoControls.subtitleDownloaded' => '字幕已下载', 'videoControls.subtitleDownloaded' => '字幕已下载',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => '字幕已下载,但无法选择', 'videoControls.subtitleDownloadedNotApplied' => '字幕已下载,但无法选择',
'videoControls.subtitleDownloadFailed' => '字幕下载失败', 'videoControls.subtitleDownloadFailed' => '字幕下载失败',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => '搜索语言…', 'videoControls.searchLanguages' => '搜索语言…',
'messages.markedAsWatched' => '已标记为已观看', 'messages.markedAsWatched' => '已标记为已观看',
'messages.markedAsUnwatched' => '已标记为未观看', 'messages.markedAsUnwatched' => '已标记为未观看',
@@ -3214,10 +3210,10 @@ extension on TranslationsZh {
'watchTogether.failedToJoin' => '加入会话失败', 'watchTogether.failedToJoin' => '加入会话失败',
'watchTogether.sessionCodeCopied' => '会话代码已复制到剪贴板', 'watchTogether.sessionCodeCopied' => '会话代码已复制到剪贴板',
'watchTogether.relayUnreachable' => '无法访问中继服务器。网络运营商的屏蔽可能导致“一起看”不可用。', 'watchTogether.relayUnreachable' => '无法访问中继服务器。网络运营商的屏蔽可能导致“一起看”不可用。',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => '正在重新连接到主持人…', 'watchTogether.reconnectingToHost' => '正在重新连接到主持人…',
'watchTogether.currentPlayback' => '当前播放', 'watchTogether.currentPlayback' => '当前播放',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => '加入当前播放', 'watchTogether.joinCurrentPlayback' => '加入当前播放',
'watchTogether.joinCurrentPlaybackDescription' => '加入主持人当前正在观看的内容', 'watchTogether.joinCurrentPlaybackDescription' => '加入主持人当前正在观看的内容',
'watchTogether.failedToOpenCurrentPlayback' => '无法打开当前播放', 'watchTogether.failedToOpenCurrentPlayback' => '无法打开当前播放',
+4 -8
View File
@@ -547,8 +547,6 @@ class _Translations$mediaMenu$zh_Hant extends Translations$mediaMenu$zh {
@override String get shufflePlay => '隨機播放'; @override String get shufflePlay => '隨機播放';
@override String get shuffleNotAvailableOffline => '離線時無法隨機播放'; @override String get shuffleNotAvailableOffline => '離線時無法隨機播放';
@override String get fileInfo => '檔案資訊'; @override String get fileInfo => '檔案資訊';
@override String get deleteFromServer => '從伺服器刪除';
@override String get confirmDelete => '確定要從伺服器刪除此媒體及其檔案嗎?';
@override String get deleteMultipleWarning => '這將會刪除所有單集及其檔案。'; @override String get deleteMultipleWarning => '這將會刪除所有單集及其檔案。';
@override String get mediaDeletedSuccessfully => '媒體已成功刪除'; @override String get mediaDeletedSuccessfully => '媒體已成功刪除';
@override String get mediaFailedToDelete => '刪除媒體失敗'; @override String get mediaFailedToDelete => '刪除媒體失敗';
@@ -2581,8 +2579,6 @@ extension on TranslationsZhHant {
'mediaMenu.shufflePlay' => '隨機播放', 'mediaMenu.shufflePlay' => '隨機播放',
'mediaMenu.shuffleNotAvailableOffline' => '離線時無法隨機播放', 'mediaMenu.shuffleNotAvailableOffline' => '離線時無法隨機播放',
'mediaMenu.fileInfo' => '檔案資訊', 'mediaMenu.fileInfo' => '檔案資訊',
'mediaMenu.deleteFromServer' => '從伺服器刪除',
'mediaMenu.confirmDelete' => '確定要從伺服器刪除此媒體及其檔案嗎?',
'mediaMenu.deleteMultipleWarning' => '這將會刪除所有單集及其檔案。', 'mediaMenu.deleteMultipleWarning' => '這將會刪除所有單集及其檔案。',
'mediaMenu.mediaDeletedSuccessfully' => '媒體已成功刪除', 'mediaMenu.mediaDeletedSuccessfully' => '媒體已成功刪除',
'mediaMenu.mediaFailedToDelete' => '刪除媒體失敗', 'mediaMenu.mediaFailedToDelete' => '刪除媒體失敗',
@@ -2701,10 +2697,10 @@ extension on TranslationsZhHant {
'videoControls.language' => '語言', 'videoControls.language' => '語言',
'videoControls.noSubtitlesFound' => '找不到字幕', 'videoControls.noSubtitlesFound' => '找不到字幕',
'videoControls.subtitleDownloaded' => '字幕下載成功', 'videoControls.subtitleDownloaded' => '字幕下載成功',
_ => null,
} ?? switch (path) {
'videoControls.subtitleDownloadedNotApplied' => '字幕已下載,但無法套用', 'videoControls.subtitleDownloadedNotApplied' => '字幕已下載,但無法套用',
'videoControls.subtitleDownloadFailed' => '字幕下載失敗', 'videoControls.subtitleDownloadFailed' => '字幕下載失敗',
_ => null,
} ?? switch (path) {
'videoControls.searchLanguages' => '搜尋語言…', 'videoControls.searchLanguages' => '搜尋語言…',
'messages.markedAsWatched' => '已標記為已觀看', 'messages.markedAsWatched' => '已標記為已觀看',
'messages.markedAsUnwatched' => '已標記為未觀看', 'messages.markedAsUnwatched' => '已標記為未觀看',
@@ -3215,10 +3211,10 @@ extension on TranslationsZhHant {
'watchTogether.failedToJoin' => '加入工作階段失敗', 'watchTogether.failedToJoin' => '加入工作階段失敗',
'watchTogether.sessionCodeCopied' => '工作階段代碼已複製到剪貼簿', 'watchTogether.sessionCodeCopied' => '工作階段代碼已複製到剪貼簿',
'watchTogether.relayUnreachable' => '無法連線至中繼伺服器。ISP 封鎖可能會導致「一起看」無法使用。', 'watchTogether.relayUnreachable' => '無法連線至中繼伺服器。ISP 封鎖可能會導致「一起看」無法使用。',
_ => null,
} ?? switch (path) {
'watchTogether.reconnectingToHost' => '正在重新連線至主持人…', 'watchTogether.reconnectingToHost' => '正在重新連線至主持人…',
'watchTogether.currentPlayback' => '目前播放內容', 'watchTogether.currentPlayback' => '目前播放內容',
_ => null,
} ?? switch (path) {
'watchTogether.joinCurrentPlayback' => '加入目前播放點', 'watchTogether.joinCurrentPlayback' => '加入目前播放點',
'watchTogether.joinCurrentPlaybackDescription' => '同步至主持人目前的觀看進度', 'watchTogether.joinCurrentPlaybackDescription' => '同步至主持人目前的觀看進度',
'watchTogether.failedToOpenCurrentPlayback' => '無法開啟目前播放點', 'watchTogether.failedToOpenCurrentPlayback' => '無法開啟目前播放點',
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Blanda uppspelning", "shufflePlay": "Blanda uppspelning",
"shuffleNotAvailableOffline": "Blandad uppspelning är inte tillgänglig offline", "shuffleNotAvailableOffline": "Blandad uppspelning är inte tillgänglig offline",
"fileInfo": "Filinformation", "fileInfo": "Filinformation",
"deleteFromServer": "Ta bort från servern", "deleteEpisodeFromServer": "",
"confirmDelete": "Ta bort det här medieobjektet och dess filer från servern?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Detta omfattar alla avsnitt och deras filer.", "deleteMultipleWarning": "Detta omfattar alla avsnitt och deras filer.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Medieobjektet har tagits bort", "mediaDeletedSuccessfully": "Medieobjektet har tagits bort",
"mediaFailedToDelete": "Det gick inte att ta bort medieobjektet", "mediaFailedToDelete": "Det gick inte att ta bort medieobjektet",
"rate": "Betygsätt", "rate": "Betygsätt",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Karışık Oynat", "shufflePlay": "Karışık Oynat",
"shuffleNotAvailableOffline": "Karışık oynatma çevrimdışıyken kullanılamaz", "shuffleNotAvailableOffline": "Karışık oynatma çevrimdışıyken kullanılamaz",
"fileInfo": "Dosya Bilgisi", "fileInfo": "Dosya Bilgisi",
"deleteFromServer": "Sunucudan sil", "deleteEpisodeFromServer": "",
"confirmDelete": "Bu medya ve dosyaları sunucunuzdan silinsin mi?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Bu işlem tüm bölümleri ve dosyalarını içerir.", "deleteMultipleWarning": "Bu işlem tüm bölümleri ve dosyalarını içerir.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Medya ögesi başarıyla silindi", "mediaDeletedSuccessfully": "Medya ögesi başarıyla silindi",
"mediaFailedToDelete": "Medya ögesi silinemedi", "mediaFailedToDelete": "Medya ögesi silinemedi",
"rate": "Oyla", "rate": "Oyla",
+28 -2
View File
@@ -518,9 +518,35 @@
"shufflePlay": "Aralashtirib ijro etish", "shufflePlay": "Aralashtirib ijro etish",
"shuffleNotAvailableOffline": "Aralashtirib ijro etish oflayn rejimda mavjud emas", "shuffleNotAvailableOffline": "Aralashtirib ijro etish oflayn rejimda mavjud emas",
"fileInfo": "Fayl haqida maʼlumot", "fileInfo": "Fayl haqida maʼlumot",
"deleteFromServer": "Serverdan oʻchirish", "deleteEpisodeFromServer": "",
"confirmDelete": "Ushbu media va fayllar serverdan oʻchirilsinmi?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "Bu barcha qismlar va fayllarga taʼsir qiladi.", "deleteMultipleWarning": "Bu barcha qismlar va fayllarga taʼsir qiladi.",
"deleteEpisodeCountWarning": {
"one": "",
"other": ""
},
"deleteMultiPartWarning": {
"one": "",
"other": ""
},
"deleteSharedFileHeading": {
"one": "",
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "Media elementi muvaffaqiyatli oʻchirildi", "mediaDeletedSuccessfully": "Media elementi muvaffaqiyatli oʻchirildi",
"mediaFailedToDelete": "Media elementini oʻchirib boʻlmadi", "mediaFailedToDelete": "Media elementini oʻchirib boʻlmadi",
"rate": "Baho berish", "rate": "Baho berish",
+25 -2
View File
@@ -518,9 +518,32 @@
"shufflePlay": "隨機播放", "shufflePlay": "隨機播放",
"shuffleNotAvailableOffline": "離線時無法隨機播放", "shuffleNotAvailableOffline": "離線時無法隨機播放",
"fileInfo": "檔案資訊", "fileInfo": "檔案資訊",
"deleteFromServer": "從伺服器刪除", "deleteEpisodeFromServer": "",
"confirmDelete": "確定要從伺服器刪除此媒體及其檔案嗎?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "這將會刪除所有單集及其檔案。", "deleteMultipleWarning": "這將會刪除所有單集及其檔案。",
"deleteEpisodeCountWarning": {
"other": ""
},
"deleteMultiPartWarning": {
"other": ""
},
"deleteSharedFileHeading": {
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "媒體已成功刪除", "mediaDeletedSuccessfully": "媒體已成功刪除",
"mediaFailedToDelete": "刪除媒體失敗", "mediaFailedToDelete": "刪除媒體失敗",
"rate": "評分", "rate": "評分",
+25 -2
View File
@@ -518,9 +518,32 @@
"shufflePlay": "随机播放", "shufflePlay": "随机播放",
"shuffleNotAvailableOffline": "离线时无法随机播放", "shuffleNotAvailableOffline": "离线时无法随机播放",
"fileInfo": "文件信息", "fileInfo": "文件信息",
"deleteFromServer": "从服务器删除", "deleteEpisodeFromServer": "",
"confirmDelete": "要从服务器删除此媒体及其文件吗?", "deleteSeasonFromServer": "",
"deleteShowFromServer": "",
"deleteMovieFromServer": "",
"deleteEpisodeTitle": "",
"deleteSeasonTitle": "",
"deleteShowTitle": "",
"deleteMovieTitle": "",
"deleteEpisodeConfirm": "",
"deleteSeasonConfirm": "",
"deleteShowConfirm": "",
"deleteMovieConfirm": "",
"deleteAnyway": "",
"confirmDeleteTarget": "",
"deleteMultipleWarning": "这包括所有剧集及其文件。", "deleteMultipleWarning": "这包括所有剧集及其文件。",
"deleteEpisodeCountWarning": {
"other": ""
},
"deleteMultiPartWarning": {
"other": ""
},
"deleteSharedFileHeading": {
"other": ""
},
"deleteScopeUnverifiedProbeFailed": "",
"deleteScopeUnverifiedNoFileInfo": "",
"mediaDeletedSuccessfully": "媒体项已成功删除", "mediaDeletedSuccessfully": "媒体项已成功删除",
"mediaFailedToDelete": "删除媒体项失败", "mediaFailedToDelete": "删除媒体项失败",
"rate": "评分", "rate": "评分",
+32
View File
@@ -1,4 +1,6 @@
import '../utils/formatters.dart';
import 'media_item.dart'; import 'media_item.dart';
import 'media_kind.dart';
String formatQueueItemSubtitle(MediaItem item) { String formatQueueItemSubtitle(MediaItem item) {
final grandparentTitle = item.grandparentTitle; final grandparentTitle = item.grandparentTitle;
@@ -12,3 +14,33 @@ String formatQueueItemSubtitle(MediaItem item) {
} }
return item.kind.name; return item.kind.name;
} }
/// Unambiguous label for the exact item a destructive action will affect.
///
/// [MediaItem.displayTitle] deliberately collapses an episode or season to its
/// show name so cards can headline the series. That makes it actively
/// dangerous in a delete confirmation: a show and one of its episodes render
/// the identical string, which is how #1781 was reported as "deleting one
/// episode deleted them all". This names the target instead.
String formatMediaTargetLabel(MediaItem item) {
final segments = <String>[];
void add(String? value) {
if (value != null && value.isNotEmpty) segments.add(value);
}
switch (item.kind) {
case MediaKind.episode:
add(item.grandparentTitle);
add(formatSeasonEpisodeLabel(item.parentIndex, item.index));
add(item.title);
case MediaKind.season:
add(item.grandparentTitle ?? item.parentTitle);
add(item.title);
default:
add(item.title);
}
// Every segment can be absent on a sparse row; the collapsed show name still
// beats an empty confirmation.
return segments.isEmpty ? item.displayTitle : toBulletedString(segments);
}
+200
View File
@@ -0,0 +1,200 @@
import 'dart:async';
import '../media/media_item.dart';
import '../media/media_kind.dart';
import '../media/media_server_client.dart';
import '../media/media_version.dart';
import 'app_logger.dart';
import 'media_server_timeouts.dart';
/// How much a server-side delete will actually destroy.
enum DeleteImpactScope {
/// Verified: the delete removes exactly one file and no other library item
/// is backed by it.
exclusive,
/// Verified: the delete removes more than one file, or files that also back
/// other items (a Plex `S01E01-E03.mkv` multi-episode file).
broad,
/// Not established. The confirmation must not claim a scope.
unverified,
}
/// Why a scope could not be established, which decides whether retrying helps.
enum DeleteImpactUnverifiedReason {
/// A metadata request failed or timed out. Transient; retrying may work.
probeFailed,
/// The server answered but withheld file paths — Plex hides them from
/// restricted users. Retrying will never help.
noFileInfo,
}
/// The verified blast radius of deleting one item.
class DeleteImpact {
final DeleteImpactScope scope;
/// Unique server-side paths this delete removes. Empty when [scope] is
/// [DeleteImpactScope.unverified].
final Set<String> files;
/// Other library items backed by one of [files], which the server will
/// destroy alongside the target.
final List<MediaItem> sharedWith;
final DeleteImpactUnverifiedReason? reason;
const DeleteImpact._({required this.scope, required this.files, required this.sharedWith, this.reason});
const DeleteImpact._unverified(DeleteImpactUnverifiedReason reason)
: this._(scope: DeleteImpactScope.unverified, files: const {}, sharedWith: const [], reason: reason);
bool get isUnverified => scope == DeleteImpactScope.unverified;
bool get isBroad => scope == DeleteImpactScope.broad;
}
/// Every distinct file backing [versions], or null when the list cannot be
/// trusted to be complete.
///
/// Deliberately NOT [MediaItem.allPartFiles]: that getter drops parts whose
/// `file` is null, so a three-part version exposing one path yields a
/// one-entry set that silently understates the delete. Anything a
/// confirmation asserts must survive that case, so a single path-less part
/// invalidates the whole answer. `null` versions, an empty version list, and a
/// version whose parts were synthesized by `PlexMappers.mediaVersion` (which
/// fabricates one file-less part when the payload carried none) all fail here,
/// which is the intent.
Set<String>? completePartFiles(List<MediaVersion> versions) {
// No early return for an empty list: the `sawPart` gate below already
// rejects it, and every other "nothing to trust" shape with it.
final files = <String>{};
var sawPart = false;
for (final version in versions) {
for (final part in version.parts) {
sawPart = true;
final file = part.file;
if (file == null || file.isEmpty) return null;
files.add(file);
}
}
return sawPart ? files : null;
}
/// Cooperative stop signal. `Future.timeout` completes the future the caller
/// awaits but leaves the work behind it running, so the probe has to be told
/// to stand down explicitly.
class _ProbeCancellation {
bool isCancelled = false;
}
/// Establish what deleting [item] will destroy, before asking the user.
///
/// Returns [DeleteImpactScope.unverified] rather than guessing whenever any
/// input is missing. Callers must surface that as an explicit danger, never as
/// a normal confirmation: a silent fall-through would let a dialog assert
/// single-file scope on unknown data, which is the failure mode behind #1781.
///
/// Bounded by [timeout] because it runs behind a modal spinner. Expiry also
/// cancels the probe, so a season of thin rows cannot keep walking itself
/// after the confirmation has already given up on it. The request already in
/// flight cannot be recalled — the neutral client exposes no abort handle for
/// item lookups — so exactly one may outlive the deadline, and no further one
/// starts.
Future<DeleteImpact> resolveDeleteImpact({
required MediaItem item,
required MediaServerClient client,
Duration timeout = MediaServerTimeouts.deleteImpactProbe,
}) async {
final cancellation = _ProbeCancellation();
try {
return await _resolve(item, client, cancellation).timeout(
timeout,
onTimeout: () {
cancellation.isCancelled = true;
appLogger.w('Delete impact probe timed out for ${item.backend.id} item ${item.id}');
return const DeleteImpact._unverified(DeleteImpactUnverifiedReason.probeFailed);
},
);
} catch (e, st) {
cancellation.isCancelled = true;
appLogger.w('Delete impact probe failed for ${item.backend.id} item ${item.id}', error: e, stackTrace: st);
return const DeleteImpact._unverified(DeleteImpactUnverifiedReason.probeFailed);
}
}
/// The value every abandoned path returns. Nothing reads it — the caller's
/// future has already completed — it only has to stop the walk.
const _abandoned = DeleteImpact._unverified(DeleteImpactUnverifiedReason.probeFailed);
Future<DeleteImpact> _resolve(MediaItem item, MediaServerClient client, _ProbeCancellation cancellation) async {
final own = await _completeFilesFor(item, client);
// Cancelled while the item's own lookup was outstanding: stop before asking
// the server for the season's children.
if (cancellation.isCancelled) return _abandoned;
final ownFiles = own.$1;
if (ownFiles == null) return DeleteImpact._unverified(own.$2!);
// Only an episode can be one of several items sharing a file. A movie's
// blast radius is its own part list.
final parentId = item.kind == MediaKind.episode ? item.parentId : null;
if (parentId == null) return _verified(ownFiles, const []);
final siblings = (await client.fetchChildren(parentId)).where((sibling) => sibling.id != item.id).toList();
// Resolved one sibling at a time, bailing on the first whose paths stay
// unknown. Both backends normally carry paths on browse rows (Plex
// `/children` includes `Part.file`; Jellyfin episode rows request
// `MediaSources`), so a detail refetch here is the degraded path — but on a
// thin season it is the rule, and `Future.wait` would then fan out one
// request per episode. Rows that already carry paths cost no request at all.
final shared = <MediaItem>[];
for (final sibling in siblings) {
// Re-checked every iteration, because the deadline may have expired while
// the previous sibling's lookup was outstanding. This is the gate that
// keeps an abandoned probe from walking the rest of the season; a check
// after the await would only skip discarded bookkeeping.
if (cancellation.isCancelled) return _abandoned;
final (files, reason) = await _completeFilesFor(sibling, client);
// A sibling whose paths stayed unknown could be backed by this very file.
// Absence of evidence is not evidence of absence.
if (files == null) return DeleteImpact._unverified(reason!);
if (files.intersection(ownFiles).isNotEmpty) shared.add(sibling);
}
return _verified(ownFiles, shared);
}
/// Every file backing [item], or the reason the list could not be trusted.
///
/// Deliberately not `resolveMediaVersions`: that helper returns inline
/// versions without ever consulting the detail endpoint, so a thin browse row
/// — including the single file-less part `PlexMappers.mediaVersion`
/// fabricates when a payload carried none — would be mistaken for a server
/// that withholds paths. It also flattens fetch failures into an empty list,
/// erasing the transient-vs-permanent distinction the confirmation copy needs.
Future<(Set<String>?, DeleteImpactUnverifiedReason?)> _completeFilesFor(
MediaItem item,
MediaServerClient client,
) async {
final inline = completePartFiles(item.mediaVersions ?? const <MediaVersion>[]);
if (inline != null) return (inline, null);
final MediaItem? full;
try {
full = await client.fetchItem(item.id);
} catch (e, st) {
appLogger.w('Delete impact: failed to resolve files for item ${item.id}', error: e, stackTrace: st);
return (null, DeleteImpactUnverifiedReason.probeFailed);
}
if (full == null) return (null, DeleteImpactUnverifiedReason.probeFailed);
final resolved = completePartFiles(full.mediaVersions ?? const <MediaVersion>[]);
return resolved != null ? (resolved, null) : (null, DeleteImpactUnverifiedReason.noFileInfo);
}
DeleteImpact _verified(Set<String> files, List<MediaItem> shared) => DeleteImpact._(
scope: files.length > 1 || shared.isNotEmpty ? DeleteImpactScope.broad : DeleteImpactScope.exclusive,
files: files,
sharedWith: shared,
);
+30 -2
View File
@@ -37,6 +37,7 @@ Future<bool> showConfirmDialog(
required String confirmText, required String confirmText,
String? cancelText, String? cancelText,
bool isDestructive = false, bool isDestructive = false,
String? warning,
}) async { }) async {
final confirmed = await showScopedDialog<bool>( final confirmed = await showScopedDialog<bool>(
context: context, context: context,
@@ -44,7 +45,27 @@ Future<bool> showConfirmDialog(
final colorScheme = Theme.of(dialogContext).colorScheme; final colorScheme = Theme.of(dialogContext).colorScheme;
return AlertDialog( return AlertDialog(
title: Text(title), title: Text(title),
content: Text(message), content: warning == null
? Text(message)
: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(message),
const SizedBox(height: 16),
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: colorScheme.errorContainer,
borderRadius: BorderRadius.circular(8),
),
child: Text(warning, style: TextStyle(color: colorScheme.onErrorContainer)),
),
],
),
),
actions: [ actions: [
DialogActionButton( DialogActionButton(
autofocus: true, autofocus: true,
@@ -75,11 +96,16 @@ Future<bool> showConfirmDialog(
/// Shows a non-dismissible loading-spinner dialog. Caller is responsible for /// Shows a non-dismissible loading-spinner dialog. Caller is responsible for
/// closing it via `Navigator.pop(context)` when the work completes. /// closing it via `Navigator.pop(context)` when the work completes.
///
/// `barrierDismissible: false` only blocks the barrier, so the spinner also
/// traps system back. Without that, back would dismiss the spinner and the
/// caller's cleanup pop would land on the route underneath — closing the
/// screen the user was working in.
void showLoadingDialog(BuildContext context) { void showLoadingDialog(BuildContext context) {
showScopedDialog<void>( showScopedDialog<void>(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (_) => const Center(child: CircularProgressIndicator()), builder: (_) => const PopScope(canPop: false, child: Center(child: CircularProgressIndicator())),
); );
} }
@@ -133,6 +159,7 @@ Future<bool> showDeleteConfirmation(
required String title, required String title,
required String message, required String message,
String? confirmText, String? confirmText,
String? warning,
}) { }) {
return showConfirmDialog( return showConfirmDialog(
context, context,
@@ -140,6 +167,7 @@ Future<bool> showDeleteConfirmation(
message: message, message: message,
confirmText: confirmText ?? t.common.delete, confirmText: confirmText ?? t.common.delete,
isDestructive: true, isDestructive: true,
warning: warning,
); );
} }
+14
View File
@@ -72,6 +72,20 @@ class MediaServerTimeouts {
/// only ever costs an entry, never safety. /// only ever costs an entry, never safety.
static const jellyfinDeletePermission = Duration(seconds: 3); static const jellyfinDeletePermission = Duration(seconds: 3);
/// Whole-probe deadline for establishing what a server-side delete will
/// destroy (see `resolveDeleteImpact`). Generous compared to
/// [jellyfinDeletePermission] for two reasons: it runs behind a modal
/// spinner *after* the user asked to delete, not while a menu is opening,
/// and it may fan out one detail request per sibling episode when browse
/// rows omit file paths.
///
/// Unlike the permission probe, expiry cannot fail closed — Plex never
/// sends file paths to restricted users, so refusing the delete would
/// permanently remove a feature the server itself authorizes. Expiry
/// instead downgrades the confirmation to its explicit "scope unverified"
/// form.
static const deleteImpactProbe = Duration(seconds: 10);
/// Best-effort `/Sessions/Logout` timeout — short because the call is /// Best-effort `/Sessions/Logout` timeout — short because the call is
/// fire-and-forget; the token is removed locally regardless. /// fire-and-forget; the token is removed locally regardless.
static const jellyfinSignOut = Duration(seconds: 5); static const jellyfinSignOut = Duration(seconds: 5);
+108 -10
View File
@@ -8,6 +8,7 @@ import 'package:provider/provider.dart';
import '../providers/watch_state_store.dart'; import '../providers/watch_state_store.dart';
import '../media/media_backend.dart'; import '../media/media_backend.dart';
import '../media/media_item.dart'; import '../media/media_item.dart';
import '../media/media_item_labels.dart';
import '../media/media_item_types.dart'; import '../media/media_item_types.dart';
import '../media/media_kind.dart'; import '../media/media_kind.dart';
import '../media/library_query.dart'; import '../media/library_query.dart';
@@ -24,6 +25,7 @@ import '../services/playlist_items_loader.dart';
import '../services/watch_actions.dart'; import '../services/watch_actions.dart';
import '../models/transcode_quality_preset.dart'; import '../models/transcode_quality_preset.dart';
import '../utils/content_utils.dart'; import '../utils/content_utils.dart';
import '../utils/delete_impact.dart';
import '../utils/download_version_utils.dart'; import '../utils/download_version_utils.dart';
import '../utils/download_utils.dart'; import '../utils/download_utils.dart';
import '../utils/quality_preset_labels.dart'; import '../utils/quality_preset_labels.dart';
@@ -634,12 +636,17 @@ class MediaContextMenuState extends State<MediaContextMenu> {
// implements (DELETE /library/metadata/{id} for Plex and // implements (DELETE /library/metadata/{id} for Plex and
// DELETE /Items/{id} for MediaBrowser servers); the kind and permission checks were // DELETE /Items/{id} for MediaBrowser servers); the kind and permission checks were
// resolved together above. // resolved together above.
//
// The label names the kind. One shared "Delete from server" string for
// an episode, a season and a whole show is what let #1781 happen: the
// reporter hit the show-level entry believing it acted on the episode
// he had highlighted.
if (canDeleteFromServer) { if (canDeleteFromServer) {
menuActions.add( menuActions.add(
_MenuAction( _MenuAction(
value: 'delete_media', value: 'delete_media',
icon: Symbols.delete_forever_rounded, icon: Symbols.delete_forever_rounded,
label: t.mediaMenu.deleteFromServer, label: _deleteMenuLabel(mediaKind),
destructive: true, destructive: true,
), ),
); );
@@ -667,7 +674,7 @@ class MediaContextMenuState extends State<MediaContextMenu> {
// fall back to a hostless modal sheet). // fall back to a hostless modal sheet).
final selected = await showAdaptiveAppMenu<String>( final selected = await showAdaptiveAppMenu<String>(
this.context, this.context,
title: _itemDisplayTitle(), title: _itemMenuTitle(),
entries: _menuEntries(menuActions), entries: _menuEntries(menuActions),
position: position, position: position,
focusFirstItem: openedFromKeyboard, focusFirstItem: openedFromKeyboard,
@@ -1744,6 +1751,23 @@ class MediaContextMenuState extends State<MediaContextMenu> {
_ => '', _ => '',
}; };
/// Header for the menu itself. Unlike [_itemDisplayTitle] — which stays on
/// [MediaItem.displayTitle] for snackbars and the file-info sheet — this
/// names the exact item the entries will act on, so an episode's menu is no
/// longer headed by its show's name (#1781).
String _itemMenuTitle() => switch (widget.item) {
final MediaItem item => formatMediaTargetLabel(item),
MediaPlaylist(:final displayTitle) => displayTitle,
_ => '',
};
String _deleteMenuLabel(MediaKind? kind) => switch (kind) {
MediaKind.season => t.mediaMenu.deleteSeasonFromServer,
MediaKind.show => t.mediaMenu.deleteShowFromServer,
MediaKind.movie => t.mediaMenu.deleteMovieFromServer,
_ => t.mediaMenu.deleteEpisodeFromServer,
};
Future<void> _handleManageSyncRule(BuildContext context) => manageSyncRule( Future<void> _handleManageSyncRule(BuildContext context) => manageSyncRule(
context, context,
downloadProvider: context.read<DownloadProvider>(), downloadProvider: context.read<DownloadProvider>(),
@@ -1781,24 +1805,47 @@ class MediaContextMenuState extends State<MediaContextMenu> {
displayTitle: _itemDisplayTitle(), displayTitle: _itemDisplayTitle(),
); );
/// Handle delete media item action /// Handle delete media item action.
/// This permanently removes the media item and its associated files from the server ///
/// Permanently removes the item and its files from the server. Everything
/// before the DELETE exists to make the blast radius legible: the dialog
/// names the exact target, states the kind, and — for a single playable
/// item — reports what the server will actually destroy. When that cannot
/// be established, the dialog says so instead of implying single-file
/// scope (#1781).
Future<void> _handleDeleteMediaItem(BuildContext context, MediaKind? mediaKind) async { Future<void> _handleDeleteMediaItem(BuildContext context, MediaKind? mediaKind) async {
final item = _mediaItem!; final item = _mediaItem!;
final isMultipleMediaItems = mediaKind == MediaKind.show || mediaKind == MediaKind.season; final client = _getMediaClientForItem();
// Shows and seasons are known-broad by definition; probing their parts
// would be a per-episode fan-out to restate what the copy already says.
final probesImpact = mediaKind == MediaKind.episode || mediaKind == MediaKind.movie;
DeleteImpact? impact;
if (probesImpact) {
showLoadingDialog(context);
try {
impact = await resolveDeleteImpact(item: item, client: client);
} finally {
// The spinner traps system back and nothing else pushes during the
// probe, so it is still the top route here. A `Navigator.canPop`
// guard would only test "is anything poppable", which is true of the
// screen underneath — exactly the route that must not close.
if (context.mounted) Navigator.pop(context);
}
if (!context.mounted) return;
}
// Show confirmation dialog
final confirmed = await showDeleteConfirmation( final confirmed = await showDeleteConfirmation(
context, context,
title: t.mediaMenu.deleteFromServer, title: _deleteDialogTitle(mediaKind),
message: "${t.mediaMenu.confirmDelete}${isMultipleMediaItems ? "\n\n${t.mediaMenu.deleteMultipleWarning}" : ""}", message: t.mediaMenu.confirmDeleteTarget(title: formatMediaTargetLabel(item)),
confirmText: t.mediaMenu.deleteFromServer, warning: _deleteWarning(item, mediaKind, impact),
confirmText: impact != null && impact.isUnverified ? t.mediaMenu.deleteAnyway : _deleteConfirmLabel(mediaKind),
); );
if (!confirmed || !context.mounted) return; if (!confirmed || !context.mounted) return;
try { try {
final client = _getMediaClientForItem();
final success = await client.deleteMediaItem(item); final success = await client.deleteMediaItem(item);
if (context.mounted) { if (context.mounted) {
@@ -1806,6 +1853,11 @@ class MediaContextMenuState extends State<MediaContextMenu> {
showSuccessSnackBar(context, t.mediaMenu.mediaDeletedSuccessfully); showSuccessSnackBar(context, t.mediaMenu.mediaDeletedSuccessfully);
// Broadcast deletion event for cross-screen propagation // Broadcast deletion event for cross-screen propagation
DeletionNotifier().notifyDeletedItem(item: item); DeletionNotifier().notifyDeletedItem(item: item);
// Siblings sharing the deleted file died with it server-side. Without
// their own events their rows linger until a full refresh.
for (final sibling in impact?.sharedWith ?? const <MediaItem>[]) {
DeletionNotifier().notifyDeletedItem(item: sibling);
}
// Backward-compatible list refresh for screens that are not DeletionAware yet // Backward-compatible list refresh for screens that are not DeletionAware yet
_notifyListRefresh(); _notifyListRefresh();
} else { } else {
@@ -1820,6 +1872,52 @@ class MediaContextMenuState extends State<MediaContextMenu> {
} }
} }
String _deleteDialogTitle(MediaKind? kind) => switch (kind) {
MediaKind.season => t.mediaMenu.deleteSeasonTitle,
MediaKind.show => t.mediaMenu.deleteShowTitle,
MediaKind.movie => t.mediaMenu.deleteMovieTitle,
_ => t.mediaMenu.deleteEpisodeTitle,
};
String _deleteConfirmLabel(MediaKind? kind) => switch (kind) {
MediaKind.season => t.mediaMenu.deleteSeasonConfirm,
MediaKind.show => t.mediaMenu.deleteShowConfirm,
MediaKind.movie => t.mediaMenu.deleteMovieConfirm,
_ => t.mediaMenu.deleteEpisodeConfirm,
};
/// The danger block under the confirmation message, or null when the delete
/// is verified to remove exactly the named item.
String? _deleteWarning(MediaItem item, MediaKind? kind, DeleteImpact? impact) {
if (kind == MediaKind.show || kind == MediaKind.season) {
// leafCount is the episode total; a season falls back to its direct
// children. Neither is guaranteed, so keep the countless sentence.
final episodes = item.leafCount ?? (kind == MediaKind.season ? item.childCount : null);
return episodes != null && episodes > 0
? t.mediaMenu.deleteEpisodeCountWarning(n: episodes)
: t.mediaMenu.deleteMultipleWarning;
}
if (impact == null) return null;
if (impact.isUnverified) {
return switch (impact.reason) {
DeleteImpactUnverifiedReason.noFileInfo => t.mediaMenu.deleteScopeUnverifiedNoFileInfo,
_ => t.mediaMenu.deleteScopeUnverifiedProbeFailed,
};
}
if (!impact.isBroad) return null;
final lines = <String>[];
if (impact.files.length > 1) lines.add(t.mediaMenu.deleteMultiPartWarning(n: impact.files.length));
if (impact.sharedWith.isNotEmpty) {
lines.add(t.mediaMenu.deleteSharedFileHeading(n: impact.sharedWith.length));
lines.addAll(impact.sharedWith.map((sibling) => '\u2022 ${formatMediaTargetLabel(sibling)}'));
}
return lines.join('\n');
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// GestureDetector wrapping removed — gesture callbacks are now on InkWell // GestureDetector wrapping removed — gesture callbacks are now on InkWell
+602
View File
@@ -0,0 +1,602 @@
import 'dart:async';
import 'dart:convert';
import 'package:drift/native.dart';
import 'package:fake_async/fake_async.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:plezy/database/app_database.dart';
import 'package:plezy/media/ids.dart';
import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_part.dart';
import 'package:plezy/media/media_server_client.dart';
import 'package:plezy/media/media_version.dart';
import 'package:plezy/models/plex/plex_config.dart';
import 'package:plezy/services/plex_api_cache.dart';
import 'package:plezy/services/plex_client.dart';
import 'package:plezy/utils/delete_impact.dart';
import '../test_helpers/backend_client_fixtures.dart';
import '../test_helpers/media_items.dart';
/// Plex is the backend that actually produces multi-episode files, and #1781
/// was reported against it. These drive the real [PlexClient] and Plex mappers
/// over a [MockClient], so the wire shape Plezy depends on — `Media` → `Part` →
/// `file` on both `/library/metadata/{id}` and `/library/metadata/{id}/children`
/// — is asserted rather than assumed.
///
/// Plain `test`, not `testWidgets`: the Plex metadata cache is a real Drift
/// database, and its I/O never completes under the widget tester's fake clock.
/// Awaiting the resolver directly keeps this deterministic with no clock
/// pumping and no wall-clock waiting.
void main() {
const sharedFile = '/tv/Breaking Bad/Season 01/S01E02-E03.mkv';
/// Episode `Metadata` entry. [file]/[files] land on nested `Part` entries,
/// which is the attribute the shared-file signal reads.
///
/// [partWithoutPath] emits a `Media` whose `Part` carries no `file` — what a
/// Plex restricted user receives. Passing none of the three omits `Media`
/// entirely, the thin-row shape.
Map<String, Object?> episodeMetadata({
required String id,
required int index,
String? file,
List<String>? files,
bool partWithoutPath = false,
}) => {
'ratingKey': id,
'key': '/library/metadata/$id',
'type': 'episode',
'title': 'Episode $index',
'index': index,
'parentIndex': 1,
'parentRatingKey': 'season-1',
'grandparentRatingKey': 'show-1',
'grandparentTitle': 'Breaking Bad',
if (file != null || files != null || partWithoutPath)
'Media': [
{
'id': 'media-$id',
'container': 'mkv',
'Part': [
if (partWithoutPath)
{'id': 'part-$id', 'key': '/library/parts/$id/file.mkv', 'size': 1024}
else
for (final path in files ?? <String>[file!])
{'id': 'part-$id-${path.hashCode}', 'key': '/library/parts/$id/file.mkv', 'file': path, 'size': 1024},
],
},
],
};
http.Response container(List<Map<String, Object?>> metadata) => http.Response(
jsonEncode({
'MediaContainer': {'size': metadata.length, 'totalSize': metadata.length, 'Metadata': metadata},
}),
200,
headers: const {'content-type': 'application/json'},
);
/// A real PlexClient over [handler], with the metadata cache backed by an
/// in-memory database. Recorded request paths let a test prove which
/// endpoints the resolver actually consulted.
({PlexClient client, List<String> paths, List<String> deleted}) plexClient(
Future<http.Response> Function(String path) handler,
) {
final db = AppDatabase.forTesting(NativeDatabase.memory());
PlexApiCache.initialize(db);
final paths = <String>[];
final deleted = <String>[];
final client = testPlexClient(
config: PlexConfig(
baseUrl: 'https://plex.example.com',
token: 'token',
clientIdentifier: 'client-id',
product: 'Plezy',
version: '1',
),
serverId: ServerId('plex-1'),
httpClient: MockClient((request) async {
paths.add(request.url.path);
if (request.method == 'DELETE') {
deleted.add(request.url.path.split('/').last);
return http.Response('', 200);
}
return handler(request.url.path);
}),
);
addTearDown(() async {
client.close();
await db.close();
});
return (client: client, paths: paths, deleted: deleted);
}
/// The reported shape: an episode row Plezy holds with no media at all.
final thinEpisode = testMediaItem(
id: 'ep-2',
backend: MediaBackend.plex,
kind: MediaKind.episode,
title: 'Cat in the Bag',
parentId: 'season-1',
parentIndex: 1,
index: 2,
grandparentId: 'show-1',
grandparentTitle: 'Breaking Bad',
serverId: 'plex-1',
);
test('a thin row recovers scope from detail and finds the episode sharing its file', () async {
final plex = plexClient(
(path) async => switch (path) {
'/library/metadata/ep-2' => container([episodeMetadata(id: 'ep-2', index: 2, file: sharedFile)]),
'/library/metadata/season-1/children' => container([
episodeMetadata(id: 'ep-1', index: 1, file: '/tv/Breaking Bad/Season 01/S01E01.mkv'),
episodeMetadata(id: 'ep-2', index: 2, file: sharedFile),
episodeMetadata(id: 'ep-3', index: 3, file: sharedFile),
]),
_ => http.Response('not found', 404),
},
);
final impact = await resolveDeleteImpact(item: thinEpisode, client: plex.client);
expect(impact.scope, DeleteImpactScope.broad);
expect(impact.files, {sharedFile});
expect(impact.sharedWith.map((item) => item.id), ['ep-3']);
// The inline row carried nothing, so both endpoints had to be consulted.
expect(plex.paths, contains('/library/metadata/ep-2'));
expect(plex.paths, contains('/library/metadata/season-1/children'));
});
test('deletes only the resolved episode rating key', () async {
final plex = plexClient(
(path) async => switch (path) {
'/library/metadata/ep-2' => container([episodeMetadata(id: 'ep-2', index: 2, file: sharedFile)]),
'/library/metadata/season-1/children' => container([
episodeMetadata(id: 'ep-2', index: 2, file: sharedFile),
episodeMetadata(id: 'ep-3', index: 3, file: sharedFile),
]),
_ => http.Response('not found', 404),
},
);
expect(await plex.client.deleteMediaItem(thinEpisode), isTrue);
expect(plex.deleted, ['ep-2'], reason: 'a shared file must not widen the DELETE to its siblings');
expect(plex.paths, contains('/library/metadata/ep-2'));
});
test('an episode alone in its file resolves as exclusive', () async {
const ownFile = '/tv/Breaking Bad/Season 01/S01E02.mkv';
final plex = plexClient(
(path) async => switch (path) {
'/library/metadata/ep-2' => container([episodeMetadata(id: 'ep-2', index: 2, file: ownFile)]),
'/library/metadata/season-1/children' => container([
episodeMetadata(id: 'ep-2', index: 2, file: ownFile),
episodeMetadata(id: 'ep-3', index: 3, file: '/tv/Breaking Bad/Season 01/S01E03.mkv'),
]),
_ => http.Response('not found', 404),
},
);
final impact = await resolveDeleteImpact(item: thinEpisode, client: plex.client);
expect(impact.scope, DeleteImpactScope.exclusive);
expect(impact.sharedWith, isEmpty);
});
test('a multi-part episode counts every file it will destroy', () async {
const parts = ['/tv/bb/S01E02-cd1.mkv', '/tv/bb/S01E02-cd2.mkv'];
final plex = plexClient(
(path) async => switch (path) {
'/library/metadata/ep-2' => container([episodeMetadata(id: 'ep-2', index: 2, files: parts)]),
'/library/metadata/season-1/children' => container([episodeMetadata(id: 'ep-2', index: 2, files: parts)]),
_ => http.Response('not found', 404),
},
);
final impact = await resolveDeleteImpact(item: thinEpisode, client: plex.client);
expect(impact.scope, DeleteImpactScope.broad);
expect(impact.files, parts.toSet());
expect(impact.sharedWith, isEmpty);
});
test('a Part carrying no path is unverified, not a single known file', () async {
// A restricted user gets Media and Part but never `file`. Skipping such a
// part instead of invalidating the answer would report one path-less part
// as an exclusive single-file delete.
final plex = plexClient(
(path) async => switch (path) {
'/library/metadata/ep-2' => container([episodeMetadata(id: 'ep-2', index: 2, partWithoutPath: true)]),
'/library/metadata/season-1/children' => container([
episodeMetadata(id: 'ep-2', index: 2, partWithoutPath: true),
]),
_ => http.Response('not found', 404),
},
);
final impact = await resolveDeleteImpact(item: thinEpisode, client: plex.client);
expect(impact.scope, DeleteImpactScope.unverified);
expect(impact.reason, DeleteImpactUnverifiedReason.noFileInfo);
expect(impact.files, isEmpty);
});
test('a payload with no media at all is unverified', () async {
final plex = plexClient(
(path) async => switch (path) {
'/library/metadata/ep-2' => container([episodeMetadata(id: 'ep-2', index: 2)]),
'/library/metadata/season-1/children' => container([episodeMetadata(id: 'ep-2', index: 2)]),
_ => http.Response('not found', 404),
},
);
final impact = await resolveDeleteImpact(item: thinEpisode, client: plex.client);
expect(impact.scope, DeleteImpactScope.unverified);
expect(impact.reason, DeleteImpactUnverifiedReason.noFileInfo);
});
test('a sibling whose path stays unknown blocks an exclusive verdict', () async {
// Neither the browse row nor the detail refetch names ep-3's file, so it
// could be backed by this very file. Assuming it is distinct would hide a
// shared-file delete behind a confident confirmation.
final plex = plexClient(
(path) async => switch (path) {
'/library/metadata/ep-2' => container([episodeMetadata(id: 'ep-2', index: 2, file: sharedFile)]),
'/library/metadata/season-1/children' => container([
episodeMetadata(id: 'ep-2', index: 2, file: sharedFile),
episodeMetadata(id: 'ep-3', index: 3, partWithoutPath: true),
]),
'/library/metadata/ep-3' => container([episodeMetadata(id: 'ep-3', index: 3, partWithoutPath: true)]),
_ => http.Response('not found', 404),
},
);
final impact = await resolveDeleteImpact(item: thinEpisode, client: plex.client);
expect(impact.scope, DeleteImpactScope.unverified);
expect(impact.reason, DeleteImpactUnverifiedReason.noFileInfo);
expect(plex.paths, contains('/library/metadata/ep-3'));
});
test('a sibling row without a path is resolved before ruling out a shared file', () async {
final plex = plexClient(
(path) async => switch (path) {
'/library/metadata/ep-2' => container([episodeMetadata(id: 'ep-2', index: 2, file: sharedFile)]),
// The browse row omits ep-3's path; trusting it would report the delete
// as exclusive while the server destroys ep-3 with the same file.
'/library/metadata/season-1/children' => container([
episodeMetadata(id: 'ep-2', index: 2, file: sharedFile),
episodeMetadata(id: 'ep-3', index: 3),
]),
'/library/metadata/ep-3' => container([episodeMetadata(id: 'ep-3', index: 3, file: sharedFile)]),
_ => http.Response('not found', 404),
},
);
final impact = await resolveDeleteImpact(item: thinEpisode, client: plex.client);
expect(impact.scope, DeleteImpactScope.broad);
expect(impact.sharedWith.map((item) => item.id), ['ep-3']);
expect(plex.paths, contains('/library/metadata/ep-3'));
});
// Direct contract of the predicate every verdict above rests on. Neither
// mapper can currently produce a version with zero parts, so this is the
// only place that shape is pinned.
group('completePartFiles', () {
test('rejects an empty version list', () {
expect(completePartFiles(const []), isNull);
});
test('rejects a version with no parts rather than reporting zero files', () {
expect(completePartFiles(const [MediaVersion(id: 'v-1')]), isNull);
});
test('rejects a version where any single part lacks a path', () {
expect(
completePartFiles(const [
MediaVersion(
id: 'v-1',
parts: [
MediaPart(id: 'p-1', file: '/tv/a.mkv'),
MediaPart(id: 'p-2'),
],
),
]),
isNull,
reason: 'one unknown path makes the whole file list untrustworthy',
);
});
test('collapses duplicate paths across versions into one file', () {
expect(
completePartFiles(const [
MediaVersion(
id: 'v-1',
parts: [MediaPart(id: 'p-1', file: '/tv/a.mkv')],
),
MediaVersion(
id: 'v-2',
parts: [MediaPart(id: 'p-2', file: '/tv/a.mkv')],
),
]),
{'/tv/a.mkv'},
);
});
test('returns every distinct path of a multi-part version', () {
expect(
completePartFiles(const [
MediaVersion(
id: 'v-1',
parts: [
MediaPart(id: 'p-1', file: '/tv/cd1.mkv'),
MediaPart(id: 'p-2', file: '/tv/cd2.mkv'),
],
),
]),
{'/tv/cd1.mkv', '/tv/cd2.mkv'},
);
});
});
test('a failed sibling lookup is a transient probe failure', () async {
final plex = plexClient(
(path) async => switch (path) {
'/library/metadata/ep-2' => container([episodeMetadata(id: 'ep-2', index: 2, file: sharedFile)]),
_ => http.Response('boom', 500),
},
);
final impact = await resolveDeleteImpact(item: thinEpisode, client: plex.client);
expect(impact.scope, DeleteImpactScope.unverified);
expect(impact.reason, DeleteImpactUnverifiedReason.probeFailed);
});
group('bounded probing', () {
/// A season whose rows all arrive without paths — the shape that would fan
/// out one detail request per episode.
List<Map<String, Object?>> pathlessSeason(int count) => [
episodeMetadata(id: 'ep-2', index: 2, file: sharedFile),
for (var index = 3; index < 3 + count; index++)
episodeMetadata(id: 'ep-$index', index: index, partWithoutPath: true),
];
int detailFetches(List<String> paths) =>
paths.where((path) => path.startsWith('/library/metadata/ep-') && !path.endsWith('/children')).length;
test('stops at the first sibling it cannot resolve instead of fanning out', () async {
final plex = plexClient(
(path) async => switch (path) {
'/library/metadata/ep-2' => container([episodeMetadata(id: 'ep-2', index: 2, file: sharedFile)]),
'/library/metadata/season-1/children' => container(pathlessSeason(60)),
// Every sibling detail refuses; only the first may ever be asked.
_ => http.Response('boom', 500),
},
);
final impact = await resolveDeleteImpact(item: thinEpisode, client: plex.client);
expect(impact.scope, DeleteImpactScope.unverified);
expect(impact.reason, DeleteImpactUnverifiedReason.probeFailed);
expect(
detailFetches(plex.paths),
2,
reason: 'the target plus exactly one sibling — not one request per path-less episode',
);
});
test('asks nothing extra of a season whose rows already carry paths', () async {
final plex = plexClient(
(path) async => switch (path) {
'/library/metadata/ep-2' => container([episodeMetadata(id: 'ep-2', index: 2, file: sharedFile)]),
'/library/metadata/season-1/children' => container([
episodeMetadata(id: 'ep-2', index: 2, file: sharedFile),
for (var index = 3; index < 40; index++)
episodeMetadata(id: 'ep-$index', index: index, file: '/tv/bb/S01E$index.mkv'),
]),
_ => http.Response('should not be fetched', 500),
},
);
final impact = await resolveDeleteImpact(item: thinEpisode, client: plex.client);
expect(impact.scope, DeleteImpactScope.exclusive);
expect(detailFetches(plex.paths), 1, reason: 'only the thin target itself needs a detail request');
});
});
// The Plex cases above cannot express a deadline: their cache is a real
// database, so they need a real clock. Cancellation is backend-neutral, so it
// is pinned here against a narrow fake under `fakeAsync` — deterministic,
// and no wall-clock waiting.
group('cancellation on timeout', () {
const timeout = Duration(seconds: 10);
MediaItem pathlessEpisode(String id, int index) => testMediaItem(
id: id,
kind: MediaKind.episode,
title: 'Episode $index',
parentId: 'season-1',
parentIndex: 1,
index: index,
grandparentId: 'show-1',
serverId: 'srv-1',
mediaVersions: const [
MediaVersion(
id: 'v',
parts: [MediaPart(id: 'p')],
),
],
);
test('no further sibling is fetched once the deadline has returned', () {
final target = testMediaItem(
id: 'ep-2',
kind: MediaKind.episode,
title: 'Cat in the Bag',
parentId: 'season-1',
parentIndex: 1,
index: 2,
grandparentId: 'show-1',
serverId: 'srv-1',
mediaVersions: const [
MediaVersion(
id: 'v-2',
parts: [MediaPart(id: 'p-2', file: '/tv/bb/S01E02.mkv')],
),
],
);
// Every sibling row lacks a path, so each one costs a detail lookup.
final client = _GatedClient(
children: [target, for (var index = 3; index < 30; index++) pathlessEpisode('ep-$index', index)],
);
DeleteImpact? impact;
fakeAsync((async) {
resolveDeleteImpact(item: target, client: client, timeout: timeout).then((value) => impact = value);
// Let the children lookup land and the first sibling fetch begin.
async.flushMicrotasks();
expect(client.itemFetches, 1, reason: 'one sibling in flight');
expect(impact, isNull);
async.elapse(timeout + const Duration(seconds: 1));
expect(impact?.scope, DeleteImpactScope.unverified);
expect(impact?.reason, DeleteImpactUnverifiedReason.probeFailed);
expect(client.itemFetches, 1, reason: 'the deadline must not have started another');
// The abandoned request finally answers. The loop resumes here — and
// must stop instead of walking the remaining 26 siblings.
client.releaseFetch();
async.flushMicrotasks();
async.elapse(const Duration(minutes: 1));
expect(
client.itemFetches,
1,
reason: 'a completed in-flight fetch must not restart the walk after the probe gave up',
);
});
});
test('an uninterrupted probe still visits every sibling', () {
final target = testMediaItem(
id: 'ep-2',
kind: MediaKind.episode,
parentId: 'season-1',
index: 2,
grandparentId: 'show-1',
serverId: 'srv-1',
mediaVersions: const [
MediaVersion(
id: 'v-2',
parts: [MediaPart(id: 'p-2', file: '/tv/bb/S01E02.mkv')],
),
],
);
final client = _GatedClient(children: [target, pathlessEpisode('ep-3', 3), pathlessEpisode('ep-4', 4)])
..autoRelease = true
..detailFile = '/tv/bb/S01E02.mkv';
DeleteImpact? impact;
fakeAsync((async) {
resolveDeleteImpact(item: target, client: client, timeout: timeout).then((value) => impact = value);
async.flushMicrotasks();
expect(client.itemFetches, 2, reason: 'both path-less siblings resolved');
expect(impact?.scope, DeleteImpactScope.broad);
expect(impact?.sharedWith.map((item) => item.id), ['ep-3', 'ep-4']);
});
});
test('the season is never listed when the deadline expires on the item itself', () {
// The target's own row carries no path, so its lookup is the first thing
// outstanding. Resuming past it would ask the server for the children of
// a season nobody is waiting on any more.
final target = pathlessEpisode('ep-2', 2);
final client = _GatedClient(children: [target, pathlessEpisode('ep-3', 3)]);
DeleteImpact? impact;
fakeAsync((async) {
resolveDeleteImpact(item: target, client: client, timeout: timeout).then((value) => impact = value);
async.flushMicrotasks();
expect(client.itemFetches, 1);
expect(client.childrenFetches, 0);
async.elapse(timeout + const Duration(seconds: 1));
expect(impact?.reason, DeleteImpactUnverifiedReason.probeFailed);
client.releaseFetch();
async.flushMicrotasks();
async.elapse(const Duration(minutes: 1));
expect(client.childrenFetches, 0, reason: 'an abandoned probe must not go on to list the season');
expect(client.itemFetches, 1);
});
});
});
}
/// Neutral client whose item lookups can be held open, so a test can place the
/// probe deadline in the middle of one.
class _GatedClient implements MediaServerClient {
_GatedClient({required this.children});
final List<MediaItem> children;
/// Answer item lookups immediately instead of gating them.
bool autoRelease = false;
/// Path reported by a released lookup.
String detailFile = '/tv/bb/other.mkv';
int itemFetches = 0;
int childrenFetches = 0;
final List<Completer<MediaItem?>> _pending = [];
void releaseFetch() {
for (final completer in _pending) {
if (!completer.isCompleted) completer.complete(_detail());
}
_pending.clear();
}
MediaItem _detail() => testMediaItem(
id: 'resolved',
kind: MediaKind.episode,
serverId: 'srv-1',
mediaVersions: [
MediaVersion(
id: 'v',
parts: [MediaPart(id: 'p', file: detailFile)],
),
],
);
@override
Future<List<MediaItem>> fetchChildren(String parentId) async {
childrenFetches++;
return children;
}
@override
Future<MediaItem?> fetchItem(String id) {
itemFetches++;
if (autoRelease) return Future.value(_detail());
final completer = Completer<MediaItem?>();
_pending.add(completer);
return completer.future;
}
@override
dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation);
}
+502 -22
View File
@@ -19,8 +19,10 @@ import 'package:plezy/media/media_backend.dart';
import 'package:plezy/media/media_file_info.dart'; import 'package:plezy/media/media_file_info.dart';
import 'package:plezy/media/media_item.dart'; import 'package:plezy/media/media_item.dart';
import 'package:plezy/media/media_kind.dart'; import 'package:plezy/media/media_kind.dart';
import 'package:plezy/media/media_part.dart';
import 'package:plezy/media/media_playlist.dart'; import 'package:plezy/media/media_playlist.dart';
import 'package:plezy/media/media_server_client.dart'; import 'package:plezy/media/media_server_client.dart';
import 'package:plezy/media/media_version.dart';
import 'package:plezy/media/server_capabilities.dart'; import 'package:plezy/media/server_capabilities.dart';
import 'package:plezy/metadata_edit/metadata_edit_adapters.dart'; import 'package:plezy/metadata_edit/metadata_edit_adapters.dart';
import 'package:plezy/models/plex/plex_home_user.dart'; import 'package:plezy/models/plex/plex_home_user.dart';
@@ -41,6 +43,7 @@ import 'package:plezy/services/music/music_playback_service.dart';
import 'package:plezy/services/multi_server_manager.dart'; import 'package:plezy/services/multi_server_manager.dart';
import 'package:plezy/services/plex_api_cache.dart'; import 'package:plezy/services/plex_api_cache.dart';
import 'package:plezy/services/settings_service.dart'; import 'package:plezy/services/settings_service.dart';
import 'package:plezy/utils/deletion_notifier.dart';
import 'package:plezy/theme/mono_theme.dart'; import 'package:plezy/theme/mono_theme.dart';
import 'package:plezy/utils/media_server_http_client.dart'; import 'package:plezy/utils/media_server_http_client.dart';
import 'package:plezy/utils/media_server_timeouts.dart'; import 'package:plezy/utils/media_server_timeouts.dart';
@@ -193,7 +196,7 @@ void main() {
await _openMenu(tester, menuKey); await _openMenu(tester, menuKey);
expect(find.text(t.mediaMenu.deleteFromServer), findsNothing); expect(find.text(t.mediaMenu.deleteMovieFromServer), findsNothing);
}); });
testWidgets('shows delete for a non-admin the server permits', (tester) async { testWidgets('shows delete for a non-admin the server permits', (tester) async {
@@ -207,7 +210,7 @@ void main() {
await _openMenu(tester, menuKey); await _openMenu(tester, menuKey);
expect(find.text(t.mediaMenu.deleteFromServer), findsOneWidget); expect(find.text(t.mediaMenu.deleteMovieFromServer), findsOneWidget);
final probes = requests.where((uri) => uri.queryParameters['Fields'] == 'CanDelete').toList(); final probes = requests.where((uri) => uri.queryParameters['Fields'] == 'CanDelete').toList();
expect(probes, hasLength(1)); expect(probes, hasLength(1));
expect(probes.single.queryParameters['ids'], 'movie-1'); expect(probes.single.queryParameters['ids'], 'movie-1');
@@ -223,7 +226,7 @@ void main() {
await _openMenu(tester, menuKey); await _openMenu(tester, menuKey);
expect(find.text(t.mediaMenu.deleteFromServer), findsOneWidget); expect(find.text(t.mediaMenu.deleteMovieFromServer), findsOneWidget);
}); });
testWidgets('hides delete for a library-granted user outside the grant', (tester) async { testWidgets('hides delete for a library-granted user outside the grant', (tester) async {
@@ -236,7 +239,7 @@ void main() {
await _openMenu(tester, menuKey); await _openMenu(tester, menuKey);
expect(find.text(t.mediaMenu.deleteFromServer), findsNothing); expect(find.text(t.mediaMenu.deleteMovieFromServer), findsNothing);
}); });
testWidgets('hides delete when the permission probe fails', (tester) async { testWidgets('hides delete when the permission probe fails', (tester) async {
@@ -248,7 +251,7 @@ void main() {
await _openMenu(tester, menuKey); await _openMenu(tester, menuKey);
expect(find.text(t.mediaMenu.deleteFromServer), findsNothing); expect(find.text(t.mediaMenu.deleteMovieFromServer), findsNothing);
expect(tester.takeException(), isNull); expect(tester.takeException(), isNull);
// The menu itself still opened; only the destructive entry is missing. // The menu itself still opened; only the destructive entry is missing.
expect(find.text(t.mediaMenu.fileInfo), findsOneWidget); expect(find.text(t.mediaMenu.fileInfo), findsOneWidget);
@@ -266,7 +269,7 @@ void main() {
await _openMenu(tester, menuKey); await _openMenu(tester, menuKey);
expect(find.text(t.mediaMenu.deleteFromServer), findsNothing); expect(find.text(t.mediaMenu.deleteMovieFromServer), findsNothing);
expect(requests.where((uri) => uri.queryParameters['Fields'] == 'CanDelete'), isEmpty); expect(requests.where((uri) => uri.queryParameters['Fields'] == 'CanDelete'), isEmpty);
}); });
@@ -285,7 +288,374 @@ void main() {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(find.text(t.mediaMenu.fileInfo), findsOneWidget); expect(find.text(t.mediaMenu.fileInfo), findsOneWidget);
expect(find.text(t.mediaMenu.deleteFromServer), findsNothing); expect(find.text(t.mediaMenu.deleteMovieFromServer), findsNothing);
});
});
group('MediaContextMenu delete confirmation', () {
testWidgets('names the episode, never the show, at every step (issue #1781)', (tester) async {
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
item: _episode(id: 'ep-2', index: 2, file: '/tv/bb/S01E02.mkv'),
handler: _deleteFlowHandler(
episodes: [_episodeJson(id: 'ep-2', index: 2, path: '/tv/bb/S01E02.mkv')],
),
);
await _openMenu(tester, menuKey);
expect(find.text(t.mediaMenu.deleteEpisodeFromServer), findsOneWidget);
expect(find.text(t.mediaMenu.deleteShowFromServer), findsNothing);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeFromServer));
await tester.pumpAndSettle();
final dialog = find.byType(AlertDialog);
expect(dialog, findsOneWidget);
expect(find.descendant(of: dialog, matching: find.text(t.mediaMenu.deleteEpisodeTitle)), findsOneWidget);
expect(
find.descendant(
of: dialog,
matching: find.text(t.mediaMenu.confirmDeleteTarget(title: 'Breaking Bad · S1 E2 · Cat in the Bag')),
),
findsOneWidget,
reason: 'the confirmation must name the episode; displayTitle would render only "Breaking Bad"',
);
expect(find.descendant(of: dialog, matching: find.text(t.mediaMenu.deleteEpisodeConfirm)), findsOneWidget);
expect(find.text(t.mediaMenu.deleteMultipleWarning), findsNothing);
});
testWidgets('a show delete says so and states the episode count', (tester) async {
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
item: testMediaItem(
id: 'show-1',
backend: MediaBackend.jellyfin,
kind: MediaKind.show,
title: 'Breaking Bad',
serverId: 'srv-1',
leafCount: 62,
),
handler: _deleteFlowHandler(episodes: const []),
);
await _openMenu(tester, menuKey);
expect(find.text(t.mediaMenu.deleteShowFromServer), findsOneWidget);
expect(find.text(t.mediaMenu.deleteEpisodeFromServer), findsNothing);
await tester.tap(find.text(t.mediaMenu.deleteShowFromServer));
await tester.pumpAndSettle();
final dialog = find.byType(AlertDialog);
expect(find.descendant(of: dialog, matching: find.text(t.mediaMenu.deleteShowTitle)), findsOneWidget);
expect(
find.descendant(of: dialog, matching: find.text(t.mediaMenu.deleteEpisodeCountWarning(n: 62))),
findsOneWidget,
);
expect(find.descendant(of: dialog, matching: find.text(t.mediaMenu.deleteShowConfirm)), findsOneWidget);
});
testWidgets('cancelling issues no delete request', (tester) async {
final requests = <Uri>[];
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
requests: requests,
item: _episode(id: 'ep-2', index: 2, file: '/tv/bb/S01E02.mkv'),
handler: _deleteFlowHandler(
episodes: [_episodeJson(id: 'ep-2', index: 2, path: '/tv/bb/S01E02.mkv')],
),
);
await _openMenu(tester, menuKey);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeFromServer));
await tester.pumpAndSettle();
await tester.tap(find.text(t.common.cancel));
await tester.pumpAndSettle();
expect(requests.where((uri) => uri.path.contains('/Items/ep-2')), isEmpty);
});
testWidgets('confirming deletes exactly the focused episode', (tester) async {
final deleted = <String>[];
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
item: _episode(id: 'ep-2', index: 2, file: '/tv/bb/S01E02.mkv'),
handler: _deleteFlowHandler(
episodes: [
_episodeJson(id: 'ep-1', index: 1, path: '/tv/bb/S01E01.mkv'),
_episodeJson(id: 'ep-2', index: 2, path: '/tv/bb/S01E02.mkv'),
_episodeJson(id: 'ep-3', index: 3, path: '/tv/bb/S01E03.mkv'),
],
onDelete: deleted.add,
),
);
await _confirmDelete(tester, menuKey, t.mediaMenu.deleteEpisodeConfirm);
expect(deleted, ['ep-2'], reason: 'siblings in the same folder must be untouched');
});
testWidgets('warns about episodes sharing the file and reports them deleted', (tester) async {
const sharedFile = '/tv/bb/S01E02-E03.mkv';
final events = <String>[];
final subscription = DeletionNotifier().stream.listen((event) => events.add(event.itemId));
addTearDown(subscription.cancel);
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
item: _episode(id: 'ep-2', index: 2, file: sharedFile),
handler: _deleteFlowHandler(
episodes: [
_episodeJson(id: 'ep-1', index: 1, path: '/tv/bb/S01E01.mkv'),
_episodeJson(id: 'ep-2', index: 2, path: sharedFile),
_episodeJson(id: 'ep-3', index: 3, path: sharedFile),
],
),
);
await _openMenu(tester, menuKey);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeFromServer));
await tester.pumpAndSettle();
expect(find.textContaining(t.mediaMenu.deleteSharedFileHeading(n: 1)), findsOneWidget);
expect(find.textContaining('S1 E3'), findsOneWidget);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeConfirm));
await tester.pumpAndSettle();
expect(events, containsAll(<String>['ep-2', 'ep-3']));
expect(events, isNot(contains('ep-1')));
});
testWidgets('a long shared-file list scrolls instead of overflowing a phone dialog', (tester) async {
const sharedFile = '/tv/bb/S01E01-E08.mkv';
tester.view.physicalSize = const Size(360, 640);
tester.view.devicePixelRatio = 1.0;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
item: _episode(id: 'ep-1', index: 1, file: sharedFile),
handler: _deleteFlowHandler(
episodes: [for (var i = 1; i <= 8; i++) _episodeJson(id: 'ep-$i', index: i, path: sharedFile)],
),
);
await _openMenu(tester, menuKey);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeFromServer));
await tester.pumpAndSettle();
expect(find.textContaining(t.mediaMenu.deleteSharedFileHeading(n: 7)), findsOneWidget);
expect(tester.takeException(), isNull, reason: 'the warning block must scroll, not overflow');
});
testWidgets('system back during the impact probe keeps the spinner and the screen', (tester) async {
// The spinner is documented non-dismissible, but barrierDismissible
// alone does not stop system back. If back popped it, the probe's
// cleanup would pop the screen underneath instead.
final gate = Completer<void>();
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
onPushedRoute: true,
item: _episode(id: 'ep-2', index: 2, file: '/tv/bb/S01E02.mkv'),
handler: (request) async {
if (request.url.queryParameters.containsKey('ParentId')) {
await gate.future;
return jsonResponse({
'Items': [_episodeJson(id: 'ep-2', index: 2, path: '/tv/bb/S01E02.mkv')],
'TotalRecordCount': 1,
});
}
if (request.url.queryParameters['Fields'] == 'CanDelete') {
return _canDeleteResponse(request.url.queryParameters['ids'] ?? '', true);
}
if (request.url.path.contains('/Seasons')) return jsonResponse({'Items': <Object?>[]});
return jsonResponse({'Items': <Object?>[]});
},
);
await _openMenu(tester, menuKey);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeFromServer));
await tester.pump();
expect(find.byType(CircularProgressIndicator), findsOneWidget);
await tester.binding.handlePopRoute();
// Bounded pumps only: pumpAndSettle would run the fake clock past
// MediaServerTimeouts.deleteImpactProbe and close the spinner by
// timeout, hiding whatever back actually did.
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.byType(CircularProgressIndicator), findsOneWidget, reason: 'the spinner is non-dismissible');
expect(find.text('home screen'), findsNothing, reason: 'the screen under the spinner must survive back');
gate.complete();
await tester.pumpAndSettle();
expect(find.byType(AlertDialog), findsOneWidget);
expect(find.byType(CircularProgressIndicator), findsNothing);
expect(find.text('home screen'), findsNothing);
expect(
find.descendant(of: find.byType(AlertDialog), matching: find.text(t.mediaMenu.deleteEpisodeConfirm)),
findsOneWidget,
);
});
testWidgets('warns when the episode is split across several files', (tester) async {
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
item: _episode(
id: 'ep-2',
index: 2,
versions: [
MediaVersion(
id: 'v-1',
parts: const [
MediaPart(id: 'p-1', file: '/tv/bb/S01E02-cd1.mkv'),
MediaPart(id: 'p-2', file: '/tv/bb/S01E02-cd2.mkv'),
],
),
],
),
handler: _deleteFlowHandler(
episodes: [_episodeJson(id: 'ep-2', index: 2, path: '/tv/bb/S01E02-cd1.mkv')],
),
);
await _openMenu(tester, menuKey);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeFromServer));
await tester.pumpAndSettle();
expect(find.textContaining(t.mediaMenu.deleteMultiPartWarning(n: 2)), findsOneWidget);
});
testWidgets('refuses to imply single-file scope when the server withholds paths', (tester) async {
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
item: _episode(
id: 'ep-2',
index: 2,
versions: const [
MediaVersion(
id: 'v-1',
parts: [MediaPart(id: 'p-1')],
),
],
),
handler: _deleteFlowHandler(
episodes: [_episodeJson(id: 'ep-2', index: 2, path: null)],
details: {'ep-2': _episodeJson(id: 'ep-2', index: 2, path: null)},
),
);
await _openMenu(tester, menuKey);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeFromServer));
await tester.pumpAndSettle();
final dialog = find.byType(AlertDialog);
expect(
find.descendant(of: dialog, matching: find.text(t.mediaMenu.deleteScopeUnverifiedNoFileInfo)),
findsOneWidget,
);
expect(
find.descendant(of: dialog, matching: find.text(t.mediaMenu.deleteAnyway)),
findsOneWidget,
reason: 'an unverified scope must not offer the confident per-kind confirm label',
);
expect(find.text(t.mediaMenu.deleteEpisodeConfirm), findsNothing);
});
testWidgets('recovers scope from the detail endpoint when the browse row omitted paths', (tester) async {
// PlexMappers synthesizes a single file-less part for a thin row, and
// Jellyfin resume rows can arrive without MediaSources. Treating that as
// "server withholds paths" would make the danger warning the default
// even though the detail endpoint answers.
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
item: _episode(
id: 'ep-2',
index: 2,
versions: const [
MediaVersion(
id: 'v-1',
parts: [MediaPart(id: 'p-1')],
),
],
),
handler: _deleteFlowHandler(
episodes: [
_episodeJson(id: 'ep-1', index: 1, path: '/tv/bb/S01E01.mkv'),
_episodeJson(id: 'ep-2', index: 2, path: '/tv/bb/S01E02.mkv'),
],
details: {'ep-2': _episodeJson(id: 'ep-2', index: 2, path: '/tv/bb/S01E02.mkv')},
),
);
await _openMenu(tester, menuKey);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeFromServer));
await tester.pumpAndSettle();
final dialog = find.byType(AlertDialog);
expect(find.descendant(of: dialog, matching: find.text(t.mediaMenu.deleteEpisodeConfirm)), findsOneWidget);
expect(find.text(t.mediaMenu.deleteAnyway), findsNothing);
expect(find.text(t.mediaMenu.deleteScopeUnverifiedNoFileInfo), findsNothing);
expect(find.text(t.mediaMenu.deleteScopeUnverifiedProbeFailed), findsNothing);
});
testWidgets('resolves a path-less sibling row before ruling out a shared file', (tester) async {
// The sibling browse row carries no path. Trusting it would report the
// delete as exclusive while the server destroys ep-3 along with it.
const sharedFile = '/tv/bb/S01E02-E03.mkv';
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
item: _episode(id: 'ep-2', index: 2, file: sharedFile),
handler: _deleteFlowHandler(
episodes: [
_episodeJson(id: 'ep-2', index: 2, path: sharedFile),
_episodeJson(id: 'ep-3', index: 3, path: null),
],
details: {'ep-3': _episodeJson(id: 'ep-3', index: 3, path: sharedFile)},
),
);
await _openMenu(tester, menuKey);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeFromServer));
await tester.pumpAndSettle();
expect(find.textContaining(t.mediaMenu.deleteSharedFileHeading(n: 1)), findsOneWidget);
expect(find.textContaining('S1 E3'), findsOneWidget);
});
testWidgets('flags an unverified scope when the sibling lookup fails, and still deletes', (tester) async {
final deleted = <String>[];
final menuKey = await _pumpJellyfinItemMenu(
tester,
isAdministrator: true,
item: _episode(id: 'ep-2', index: 2, file: '/tv/bb/S01E02.mkv'),
handler: _deleteFlowHandler(episodes: null, onDelete: deleted.add),
);
await _openMenu(tester, menuKey);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeFromServer));
await tester.pumpAndSettle();
expect(find.text(t.mediaMenu.deleteScopeUnverifiedProbeFailed), findsOneWidget);
await tester.tap(find.text(t.mediaMenu.deleteAnyway));
await tester.pumpAndSettle();
expect(deleted, ['ep-2']);
}); });
}); });
@@ -928,6 +1298,88 @@ Future<http.Response> _libraryGrantedToOneMovie(http.Request request) async {
return _canDeleteResponse(id, id == 'movie-in-grant'); return _canDeleteResponse(id, id == 'movie-in-grant');
} }
/// Jellyfin episode fixture. [file] populates the inline media source so the
/// impact probe resolves without a detail refetch; pass [versions] instead to
/// model a split or path-less item.
MediaItem _episode({required String id, required int index, String? file, List<MediaVersion>? versions}) {
return testMediaItem(
id: id,
backend: MediaBackend.jellyfin,
kind: MediaKind.episode,
title: 'Cat in the Bag',
parentId: 'season-1',
parentIndex: 1,
index: index,
grandparentId: 'show-1',
grandparentTitle: 'Breaking Bad',
serverId: 'srv-1',
mediaVersions:
versions ??
[
MediaVersion(
id: 'v-$id',
parts: [MediaPart(id: 'p-$id', file: file)],
),
],
);
}
Map<String, Object?> _episodeJson({required String id, required int index, required String? path}) => {
'Id': id,
'Name': 'Episode $index',
'Type': 'Episode',
'IndexNumber': index,
'ParentIndexNumber': 1,
'SeriesId': 'show-1',
'SeriesName': 'Breaking Bad',
'SeasonId': 'season-1',
'MediaSources': [
{'Id': 'ms-$id', 'Path': ?path},
],
};
/// Answers every request the delete flow makes: the CanDelete probe, the
/// season-children lookup, per-item detail refetches, and the DELETE itself.
///
/// A null [episodes] makes the children lookup fail, which is the
/// probe-failure path. [details] is keyed by item id and models the detail
/// endpoint carrying file paths that the browse row omitted.
Future<http.Response> Function(http.Request) _deleteFlowHandler({
required List<Map<String, Object?>>? episodes,
Map<String, Map<String, Object?>> details = const {},
void Function(String id)? onDelete,
}) {
return (request) async {
final path = request.url.path;
final lastSegment = path.split('/').last;
if (request.method == 'DELETE') {
onDelete?.call(lastSegment);
return http.Response('', 204);
}
if (request.url.queryParameters['Fields'] == 'CanDelete') {
return _canDeleteResponse(request.url.queryParameters['ids'] ?? '', true);
}
// fetchChildren probes /Shows/{id}/Seasons first and falls through to the
// generic ParentId query when it comes back empty.
if (path.contains('/Seasons')) return jsonResponse({'Items': <Object?>[]});
if (request.url.queryParameters.containsKey('ParentId')) {
if (episodes == null) return http.Response('boom', 500);
return jsonResponse({'Items': episodes, 'TotalRecordCount': episodes.length});
}
final detail = details[lastSegment];
if (detail != null) return jsonResponse(detail);
return jsonResponse({'Items': <Object?>[]});
};
}
Future<void> _confirmDelete(WidgetTester tester, GlobalKey<MediaContextMenuState> menuKey, String confirmLabel) async {
await _openMenu(tester, menuKey);
await tester.tap(find.text(t.mediaMenu.deleteEpisodeFromServer));
await tester.pumpAndSettle();
await tester.tap(find.text(confirmLabel));
await tester.pumpAndSettle();
}
Future<GlobalKey<MediaContextMenuState>> _pumpJellyfinMovieMenu( Future<GlobalKey<MediaContextMenuState>> _pumpJellyfinMovieMenu(
WidgetTester tester, { WidgetTester tester, {
required bool isAdministrator, required bool isAdministrator,
@@ -935,6 +1387,34 @@ Future<GlobalKey<MediaContextMenuState>> _pumpJellyfinMovieMenu(
String itemId = 'movie-1', String itemId = 'movie-1',
bool serverOnline = true, bool serverOnline = true,
List<Uri>? requests, List<Uri>? requests,
}) {
return _pumpJellyfinItemMenu(
tester,
isAdministrator: isAdministrator,
handler: handler,
serverOnline: serverOnline,
requests: requests,
item: testMediaItem(
id: itemId,
backend: MediaBackend.jellyfin,
kind: MediaKind.movie,
title: 'Movie',
serverId: 'srv-1',
),
);
}
Future<GlobalKey<MediaContextMenuState>> _pumpJellyfinItemMenu(
WidgetTester tester, {
required bool isAdministrator,
required Future<http.Response> Function(http.Request request) handler,
required MediaItem item,
bool serverOnline = true,
List<Uri>? requests,
/// Push the menu onto a route above a home screen, so the navigator has
/// something below the menu that a stray pop could reach.
bool onPushedRoute = false,
}) async { }) async {
LocaleSettings.setLocaleSync(AppLocale.en); LocaleSettings.setLocaleSync(AppLocale.en);
TvDetectionService.debugSetAppleTVOverride(true); TvDetectionService.debugSetAppleTVOverride(true);
@@ -959,12 +1439,15 @@ Future<GlobalKey<MediaContextMenuState>> _pumpJellyfinMovieMenu(
}); });
final menuKey = GlobalKey<MediaContextMenuState>(); final menuKey = GlobalKey<MediaContextMenuState>();
final item = testMediaItem( final navigatorKey = GlobalKey<NavigatorState>();
id: itemId, final menuScreen = Scaffold(
backend: MediaBackend.jellyfin, body: Center(
kind: MediaKind.movie, child: MediaContextMenu(
title: 'Movie', key: menuKey,
serverId: 'srv-1', item: item,
child: const SizedBox(width: 120, height: 80, child: Text('delete target')),
),
),
); );
await tester.pumpWidget( await tester.pumpWidget(
TranslationProvider( TranslationProvider(
@@ -975,20 +1458,17 @@ Future<GlobalKey<MediaContextMenuState>> _pumpJellyfinMovieMenu(
ChangeNotifierProvider<OfflineModeProvider>.value(value: offlineMode), ChangeNotifierProvider<OfflineModeProvider>.value(value: offlineMode),
], ],
child: MaterialApp( child: MaterialApp(
navigatorKey: navigatorKey,
theme: monoTheme(dark: true), theme: monoTheme(dark: true),
home: Scaffold( home: onPushedRoute ? const Scaffold(body: Center(child: Text('home screen'))) : menuScreen,
body: Center(
child: MediaContextMenu(
key: menuKey,
item: item,
child: const SizedBox(width: 120, height: 80, child: Text('delete target')),
),
),
),
), ),
), ),
), ),
); );
if (onPushedRoute) {
unawaited(navigatorKey.currentState!.push(MaterialPageRoute<void>(builder: (_) => menuScreen)));
await tester.pumpAndSettle();
}
return menuKey; return menuKey;
} }