From 41bf2f4ede9d9f79a0a90cc3cf33ede06f30854f Mon Sep 17 00:00:00 2001 From: Micah Morrison Date: Sun, 1 Feb 2026 22:56:41 -0500 Subject: [PATCH 1/2] Add the ability to delete media items --- lib/i18n/de.i18n.json | 6 +- lib/i18n/en.i18n.json | 6 +- lib/i18n/fr.i18n.json | 6 +- lib/i18n/it.i18n.json | 6 +- lib/i18n/ko.i18n.json | 6 +- lib/i18n/nl.i18n.json | 6 +- lib/i18n/strings.g.dart | 4 +- lib/i18n/strings_de.g.dart | 12 +++- lib/i18n/strings_en.g.dart | 20 +++++- lib/i18n/strings_fr.g.dart | 12 +++- lib/i18n/strings_it.g.dart | 12 +++- lib/i18n/strings_ko.g.dart | 12 +++- lib/i18n/strings_nl.g.dart | 12 +++- lib/i18n/strings_sv.g.dart | 12 +++- lib/i18n/strings_zh.g.dart | 12 +++- lib/i18n/sv.i18n.json | 6 +- lib/i18n/zh.i18n.json | 6 +- .../libraries/tabs/library_browse_tab.dart | 1 + lib/screens/media_detail_screen.dart | 23 +++++-- lib/screens/season_detail_screen.dart | 10 +-- lib/services/plex_client.dart | 7 +++ lib/widgets/focusable_list_tile.dart | 5 ++ lib/widgets/media_context_menu.dart | 62 ++++++++++++++++++- 23 files changed, 227 insertions(+), 37 deletions(-) diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 977d76f4..788f53b3 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -241,7 +241,11 @@ "goToSeries": "Zur Serie", "goToSeason": "Zur Staffel", "shufflePlay": "Zufallswiedergabe", - "fileInfo": "Dateiinfo" + "fileInfo": "Dateiinfo", + "confirmDelete": "Sind Sie sicher, dass Sie dieses Element aus Ihrem Dateisystem löschen möchten?", + "deleteMultipleWarning": "Mehrere Elemente können gelöscht werden.", + "mediaDeletedSuccessfully": "Medienelement erfolgreich gelöscht", + "mediaFailedToDelete": "Löschen des Medienelements fehlgeschlagen" }, "accessibility": { "mediaCardMovie": "${title}, Film", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 1d84fccb..c2ac4fac 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -240,7 +240,11 @@ "goToSeries": "Go to series", "goToSeason": "Go to season", "shufflePlay": "Shuffle Play", - "fileInfo": "File Info" + "fileInfo": "File Info", + "confirmDelete": "Are you sure you want to delete this item from your filesystem?", + "deleteMultipleWarning": "Multiple items may be deleted.", + "mediaDeletedSuccessfully": "Media item deleted successfully", + "mediaFailedToDelete": "Failed to delete media item" }, "accessibility": { "mediaCardMovie": "${title}, movie", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 28d14dff..a4c80009 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -240,7 +240,11 @@ "goToSeries": "Aller à la série", "goToSeason": "Aller à la saison", "shufflePlay": "Lecture aléatoire", - "fileInfo": "Informations sur le fichier" + "fileInfo": "Informations sur le fichier", + "confirmDelete": "Êtes-vous sûr de vouloir supprimer cet élément de votre système de fichiers?", + "deleteMultipleWarning": "Plusieurs éléments peuvent être supprimés.", + "mediaDeletedSuccessfully": "Élément média supprimé avec succès", + "mediaFailedToDelete": "Échec de la suppression de l'élément média" }, "accessibility": { "mediaCardMovie": "${title}, film", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 11173aea..21be8373 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -241,7 +241,11 @@ "goToSeries": "Vai alle serie", "goToSeason": "Vai alla stagione", "shufflePlay": "Riproduzione casuale", - "fileInfo": "Info sul file" + "fileInfo": "Info sul file", + "confirmDelete": "Sei sicuro di voler eliminare questo elemento dal tuo filesystem?", + "deleteMultipleWarning": "Potrebbero essere eliminati più elementi.", + "mediaDeletedSuccessfully": "Elemento multimediale eliminato con successo", + "mediaFailedToDelete": "Impossibile eliminare l'elemento multimediale" }, "accessibility": { "mediaCardMovie": "${title}, film", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 4b0080fa..a0e5be3a 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -241,7 +241,11 @@ "goToSeries": "시리즈로 이동", "goToSeason": "시즌으로 이동", "shufflePlay": "무작위 재생", - "fileInfo": "파일 정보" + "fileInfo": "파일 정보", + "confirmDelete": "파일 시스템에서 이 항목을 삭제하시겠습니까?", + "deleteMultipleWarning": "여러 항목이 삭제될 수 있습니다.", + "mediaDeletedSuccessfully": "미디어 항목이 성공적으로 삭제되었습니다", + "mediaFailedToDelete": "미디어 항목 삭제 실패" }, "accessibility": { "mediaCardMovie": "${title}, 영화", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 6fe55e91..48ed20dd 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -241,7 +241,11 @@ "goToSeries": "Ga naar serie", "goToSeason": "Ga naar seizoen", "shufflePlay": "Willekeurig afspelen", - "fileInfo": "Bestand info" + "fileInfo": "Bestand info", + "confirmDelete": "Weet je zeker dat je dit item van je bestandssysteem wilt verwijderen?", + "deleteMultipleWarning": "Meerdere items kunnen worden verwijderd.", + "mediaDeletedSuccessfully": "Media-item succesvol verwijderd", + "mediaFailedToDelete": "Verwijderen van media-item mislukt" }, "accessibility": { "mediaCardMovie": "${title}, film", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 10880c80..2be60713 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 8 -/// Strings: 4548 (568 per locale) +/// Strings: 4580 (572 per locale) /// -/// Built on 2026-02-01 at 12:48 UTC +/// Built on 2026-02-02 at 01:38 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 98aafdcf..2de1bbbc 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -372,6 +372,10 @@ class _TranslationsMediaMenuDe implements TranslationsMediaMenuEn { @override String get goToSeason => 'Zur Staffel'; @override String get shufflePlay => 'Zufallswiedergabe'; @override String get fileInfo => 'Dateiinfo'; + @override String get confirmDelete => 'Sind Sie sicher, dass Sie dieses Element aus Ihrem Dateisystem löschen möchten?'; + @override String get deleteMultipleWarning => 'Mehrere Elemente können gelöscht werden.'; + @override String get mediaDeletedSuccessfully => 'Medienelement erfolgreich gelöscht'; + @override String get mediaFailedToDelete => 'Löschen des Medienelements fehlgeschlagen'; } // Path: accessibility @@ -1206,6 +1210,10 @@ extension on TranslationsDe { 'mediaMenu.goToSeason' => 'Zur Staffel', 'mediaMenu.shufflePlay' => 'Zufallswiedergabe', 'mediaMenu.fileInfo' => 'Dateiinfo', + 'mediaMenu.confirmDelete' => 'Sind Sie sicher, dass Sie dieses Element aus Ihrem Dateisystem löschen möchten?', + 'mediaMenu.deleteMultipleWarning' => 'Mehrere Elemente können gelöscht werden.', + 'mediaMenu.mediaDeletedSuccessfully' => 'Medienelement erfolgreich gelöscht', + 'mediaMenu.mediaFailedToDelete' => 'Löschen des Medienelements fehlgeschlagen', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, Film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, Serie', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1494,12 +1502,12 @@ extension on TranslationsDe { 'collections.selectCollection' => 'Sammlung auswählen', 'collections.createNewCollection' => 'Neue Sammlung erstellen', 'collections.collectionName' => 'Sammlungsname', + _ => null, + } ?? switch (path) { 'collections.enterCollectionName' => 'Sammlungsnamen eingeben', 'collections.addedToCollection' => 'Zur Sammlung hinzugefügt', 'collections.errorAddingToCollection' => 'Fehler beim Hinzufügen zur Sammlung', 'collections.created' => 'Sammlung erstellt', - _ => null, - } ?? switch (path) { 'collections.removeFromCollection' => 'Aus Sammlung entfernen', 'collections.removeFromCollectionConfirm' => ({required Object title}) => '"${title}" aus dieser Sammlung entfernen?', 'collections.removedFromCollection' => 'Aus Sammlung entfernt', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index a892c0c3..e6126eac 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -777,6 +777,18 @@ class TranslationsMediaMenuEn { /// en: 'File Info' String get fileInfo => 'File Info'; + + /// en: 'Are you sure you want to delete this item from your filesystem?' + String get confirmDelete => 'Are you sure you want to delete this item from your filesystem?'; + + /// en: 'Multiple items may be deleted.' + String get deleteMultipleWarning => 'Multiple items may be deleted.'; + + /// en: 'Media item deleted successfully' + String get mediaDeletedSuccessfully => 'Media item deleted successfully'; + + /// en: 'Failed to delete media item' + String get mediaFailedToDelete => 'Failed to delete media item'; } // Path: accessibility @@ -2343,6 +2355,10 @@ extension on Translations { 'mediaMenu.goToSeason' => 'Go to season', 'mediaMenu.shufflePlay' => 'Shuffle Play', 'mediaMenu.fileInfo' => 'File Info', + 'mediaMenu.confirmDelete' => 'Are you sure you want to delete this item from your filesystem?', + 'mediaMenu.deleteMultipleWarning' => 'Multiple items may be deleted.', + 'mediaMenu.mediaDeletedSuccessfully' => 'Media item deleted successfully', + 'mediaMenu.mediaFailedToDelete' => 'Failed to delete media item', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, movie', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV show', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -2632,12 +2648,12 @@ extension on Translations { 'watchTogether.joinSession' => 'Join Session', 'watchTogether.joining' => 'Joining...', 'watchTogether.controlMode' => 'Control Mode', + _ => null, + } ?? switch (path) { 'watchTogether.controlModeQuestion' => 'Who can control playback?', 'watchTogether.hostOnly' => 'Host Only', 'watchTogether.anyone' => 'Anyone', 'watchTogether.hostingSession' => 'Hosting Session', - _ => null, - } ?? switch (path) { 'watchTogether.inSession' => 'In Session', 'watchTogether.sessionCode' => 'Session Code', 'watchTogether.hostControlsPlayback' => 'Host controls playback', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 68939f66..59f7c299 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -371,6 +371,10 @@ class _TranslationsMediaMenuFr implements TranslationsMediaMenuEn { @override String get goToSeason => 'Aller à la saison'; @override String get shufflePlay => 'Lecture aléatoire'; @override String get fileInfo => 'Informations sur le fichier'; + @override String get confirmDelete => 'Êtes-vous sûr de vouloir supprimer cet élément de votre système de fichiers?'; + @override String get deleteMultipleWarning => 'Plusieurs éléments peuvent être supprimé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'; } // Path: accessibility @@ -1203,6 +1207,10 @@ extension on TranslationsFr { 'mediaMenu.goToSeason' => 'Aller à la saison', 'mediaMenu.shufflePlay' => 'Lecture aléatoire', 'mediaMenu.fileInfo' => 'Informations sur le fichier', + 'mediaMenu.confirmDelete' => 'Êtes-vous sûr de vouloir supprimer cet élément de votre système de fichiers?', + 'mediaMenu.deleteMultipleWarning' => 'Plusieurs éléments peuvent être supprimés.', + 'mediaMenu.mediaDeletedSuccessfully' => 'Élément média supprimé avec succès', + 'mediaMenu.mediaFailedToDelete' => 'Échec de la suppression de l\'élément média', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, show TV', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1492,12 +1500,12 @@ extension on TranslationsFr { 'watchTogether.joinSession' => 'Rejoindre la session', 'watchTogether.joining' => 'Rejoindre...', 'watchTogether.controlMode' => 'Mode de contrôle', + _ => null, + } ?? switch (path) { 'watchTogether.controlModeQuestion' => 'Qui peut contrôler la lecture ?', 'watchTogether.hostOnly' => 'Hôte uniquement', 'watchTogether.anyone' => 'N\'importe qui', 'watchTogether.hostingSession' => 'Session d\'hébergement', - _ => null, - } ?? switch (path) { 'watchTogether.inSession' => 'En session', 'watchTogether.sessionCode' => 'Code de session', 'watchTogether.hostControlsPlayback' => 'L\'hôte contrôle la lecture', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index b0b91174..04123adc 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -372,6 +372,10 @@ class _TranslationsMediaMenuIt implements TranslationsMediaMenuEn { @override String get goToSeason => 'Vai alla stagione'; @override String get shufflePlay => 'Riproduzione casuale'; @override String get fileInfo => 'Info sul file'; + @override String get confirmDelete => 'Sei sicuro di voler eliminare questo elemento dal tuo filesystem?'; + @override String get deleteMultipleWarning => 'Potrebbero essere eliminati più elementi.'; + @override String get mediaDeletedSuccessfully => 'Elemento multimediale eliminato con successo'; + @override String get mediaFailedToDelete => 'Impossibile eliminare l\'elemento multimediale'; } // Path: accessibility @@ -1206,6 +1210,10 @@ extension on TranslationsIt { 'mediaMenu.goToSeason' => 'Vai alla stagione', 'mediaMenu.shufflePlay' => 'Riproduzione casuale', 'mediaMenu.fileInfo' => 'Info sul file', + 'mediaMenu.confirmDelete' => 'Sei sicuro di voler eliminare questo elemento dal tuo filesystem?', + 'mediaMenu.deleteMultipleWarning' => 'Potrebbero essere eliminati più elementi.', + 'mediaMenu.mediaDeletedSuccessfully' => 'Elemento multimediale eliminato con successo', + 'mediaMenu.mediaFailedToDelete' => 'Impossibile eliminare l\'elemento multimediale', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, serie TV', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1494,12 +1502,12 @@ extension on TranslationsIt { 'collections.selectCollection' => 'Seleziona raccolta', 'collections.createNewCollection' => 'Crea nuova raccolta', 'collections.collectionName' => 'Nome raccolta', + _ => null, + } ?? switch (path) { 'collections.enterCollectionName' => 'Inserisci nome raccolta', 'collections.addedToCollection' => 'Aggiunto alla raccolta', 'collections.errorAddingToCollection' => 'Errore nell\'aggiunta alla raccolta', 'collections.created' => 'Raccolta creata', - _ => null, - } ?? switch (path) { 'collections.removeFromCollection' => 'Rimuovi dalla raccolta', 'collections.removeFromCollectionConfirm' => ({required Object title}) => 'Rimuovere "${title}" da questa raccolta?', 'collections.removedFromCollection' => 'Rimosso dalla raccolta', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 826d6373..51f1d14e 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -372,6 +372,10 @@ class _TranslationsMediaMenuKo implements TranslationsMediaMenuEn { @override String get goToSeason => '시즌으로 이동'; @override String get shufflePlay => '무작위 재생'; @override String get fileInfo => '파일 정보'; + @override String get confirmDelete => '파일 시스템에서 이 항목을 삭제하시겠습니까?'; + @override String get deleteMultipleWarning => '여러 항목이 삭제될 수 있습니다.'; + @override String get mediaDeletedSuccessfully => '미디어 항목이 성공적으로 삭제되었습니다'; + @override String get mediaFailedToDelete => '미디어 항목 삭제 실패'; } // Path: accessibility @@ -1206,6 +1210,10 @@ extension on TranslationsKo { 'mediaMenu.goToSeason' => '시즌으로 이동', 'mediaMenu.shufflePlay' => '무작위 재생', 'mediaMenu.fileInfo' => '파일 정보', + 'mediaMenu.confirmDelete' => '파일 시스템에서 이 항목을 삭제하시겠습니까?', + 'mediaMenu.deleteMultipleWarning' => '여러 항목이 삭제될 수 있습니다.', + 'mediaMenu.mediaDeletedSuccessfully' => '미디어 항목이 성공적으로 삭제되었습니다', + 'mediaMenu.mediaFailedToDelete' => '미디어 항목 삭제 실패', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, 영화', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV 프로그램', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1494,12 +1502,12 @@ extension on TranslationsKo { 'watchTogether.createSession' => '세션 생성', 'watchTogether.creating' => '생성 중...', 'watchTogether.joinSession' => '세션 참여', + _ => null, + } ?? switch (path) { 'watchTogether.joining' => '참가 중...', 'watchTogether.controlMode' => '제어 모드', 'watchTogether.controlModeQuestion' => '누가 재생을 제어할 수 있나요?', 'watchTogether.hostOnly' => '호스트만', - _ => null, - } ?? switch (path) { 'watchTogether.anyone' => '누구나', 'watchTogether.hostingSession' => '세션 호스팅', 'watchTogether.inSession' => '세션 중', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 6b6d8530..b1d0658e 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -372,6 +372,10 @@ class _TranslationsMediaMenuNl implements TranslationsMediaMenuEn { @override String get goToSeason => 'Ga naar seizoen'; @override String get shufflePlay => 'Willekeurig afspelen'; @override String get fileInfo => 'Bestand info'; + @override String get confirmDelete => 'Weet je zeker dat je dit item van je bestandssysteem wilt verwijderen?'; + @override String get deleteMultipleWarning => 'Meerdere items kunnen worden verwijderd.'; + @override String get mediaDeletedSuccessfully => 'Media-item succesvol verwijderd'; + @override String get mediaFailedToDelete => 'Verwijderen van media-item mislukt'; } // Path: accessibility @@ -1206,6 +1210,10 @@ extension on TranslationsNl { 'mediaMenu.goToSeason' => 'Ga naar seizoen', 'mediaMenu.shufflePlay' => 'Willekeurig afspelen', 'mediaMenu.fileInfo' => 'Bestand info', + 'mediaMenu.confirmDelete' => 'Weet je zeker dat je dit item van je bestandssysteem wilt verwijderen?', + 'mediaMenu.deleteMultipleWarning' => 'Meerdere items kunnen worden verwijderd.', + 'mediaMenu.mediaDeletedSuccessfully' => 'Media-item succesvol verwijderd', + 'mediaMenu.mediaFailedToDelete' => 'Verwijderen van media-item mislukt', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV-serie', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1494,12 +1502,12 @@ extension on TranslationsNl { 'collections.selectCollection' => 'Selecteer collectie', 'collections.createNewCollection' => 'Nieuwe collectie maken', 'collections.collectionName' => 'Collectienaam', + _ => null, + } ?? switch (path) { 'collections.enterCollectionName' => 'Voer collectienaam in', 'collections.addedToCollection' => 'Toegevoegd aan collectie', 'collections.errorAddingToCollection' => 'Fout bij toevoegen aan collectie', 'collections.created' => 'Collectie gemaakt', - _ => null, - } ?? switch (path) { 'collections.removeFromCollection' => 'Verwijderen uit collectie', 'collections.removeFromCollectionConfirm' => ({required Object title}) => '"${title}" uit deze collectie verwijderen?', 'collections.removedFromCollection' => 'Uit collectie verwijderd', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 878ccaca..55b8f3f3 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -372,6 +372,10 @@ class _TranslationsMediaMenuSv implements TranslationsMediaMenuEn { @override String get goToSeason => 'Gå till säsong'; @override String get shufflePlay => 'Blanda uppspelning'; @override String get fileInfo => 'Filinformation'; + @override String get confirmDelete => 'Är du säker på att du vill ta bort detta objekt från ditt filsystem?'; + @override String get deleteMultipleWarning => 'Flera objekt kan komma att tas bort.'; + @override String get mediaDeletedSuccessfully => 'Mediaobjekt borttaget'; + @override String get mediaFailedToDelete => 'Kunde inte ta bort mediaobjekt'; } // Path: accessibility @@ -1206,6 +1210,10 @@ extension on TranslationsSv { 'mediaMenu.goToSeason' => 'Gå till säsong', 'mediaMenu.shufflePlay' => 'Blanda uppspelning', 'mediaMenu.fileInfo' => 'Filinformation', + 'mediaMenu.confirmDelete' => 'Är du säker på att du vill ta bort detta objekt från ditt filsystem?', + 'mediaMenu.deleteMultipleWarning' => 'Flera objekt kan komma att tas bort.', + 'mediaMenu.mediaDeletedSuccessfully' => 'Mediaobjekt borttaget', + 'mediaMenu.mediaFailedToDelete' => 'Kunde inte ta bort mediaobjekt', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV-serie', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1494,12 +1502,12 @@ extension on TranslationsSv { 'collections.selectCollection' => 'Välj samling', 'collections.createNewCollection' => 'Skapa ny samling', 'collections.collectionName' => 'Samlingsnamn', + _ => null, + } ?? switch (path) { 'collections.enterCollectionName' => 'Ange samlingsnamn', 'collections.addedToCollection' => 'Tillagd i samling', 'collections.errorAddingToCollection' => 'Fel vid tillägg i samling', 'collections.created' => 'Samling skapad', - _ => null, - } ?? switch (path) { 'collections.removeFromCollection' => 'Ta bort från samling', 'collections.removeFromCollectionConfirm' => ({required Object title}) => 'Ta bort "${title}" från denna samling?', 'collections.removedFromCollection' => 'Borttagen från samling', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 7cc08eaf..7d13299d 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -372,6 +372,10 @@ class _TranslationsMediaMenuZh implements TranslationsMediaMenuEn { @override String get goToSeason => '转到季'; @override String get shufflePlay => '随机播放'; @override String get fileInfo => '文件信息'; + @override String get confirmDelete => '确定要从文件系统中删除此项吗?'; + @override String get deleteMultipleWarning => '可能会删除多个项目。'; + @override String get mediaDeletedSuccessfully => '媒体项已成功删除'; + @override String get mediaFailedToDelete => '删除媒体项失败'; } // Path: accessibility @@ -1206,6 +1210,10 @@ extension on TranslationsZh { 'mediaMenu.goToSeason' => '转到季', 'mediaMenu.shufflePlay' => '随机播放', 'mediaMenu.fileInfo' => '文件信息', + 'mediaMenu.confirmDelete' => '确定要从文件系统中删除此项吗?', + 'mediaMenu.deleteMultipleWarning' => '可能会删除多个项目。', + 'mediaMenu.mediaDeletedSuccessfully' => '媒体项已成功删除', + 'mediaMenu.mediaFailedToDelete' => '删除媒体项失败', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, 电影', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, 电视剧', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1494,12 +1502,12 @@ extension on TranslationsZh { 'collections.selectCollection' => '选择合集', 'collections.createNewCollection' => '创建新合集', 'collections.collectionName' => '合集名称', + _ => null, + } ?? switch (path) { 'collections.enterCollectionName' => '输入合集名称', 'collections.addedToCollection' => '已添加到合集', 'collections.errorAddingToCollection' => '添加到合集失败', 'collections.created' => '已创建合集', - _ => null, - } ?? switch (path) { 'collections.removeFromCollection' => '从合集移除', 'collections.removeFromCollectionConfirm' => ({required Object title}) => '将“${title}”从此合集移除?', 'collections.removedFromCollection' => '已从合集移除', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 02e01794..ceac5b88 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -241,7 +241,11 @@ "goToSeries": "Gå till serie", "goToSeason": "Gå till säsong", "shufflePlay": "Blanda uppspelning", - "fileInfo": "Filinformation" + "fileInfo": "Filinformation", + "confirmDelete": "Är du säker på att du vill ta bort detta objekt från ditt filsystem?", + "deleteMultipleWarning": "Flera objekt kan komma att tas bort.", + "mediaDeletedSuccessfully": "Mediaobjekt borttaget", + "mediaFailedToDelete": "Kunde inte ta bort mediaobjekt" }, "accessibility": { "mediaCardMovie": "${title}, film", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index fcb46b24..fd0ac85f 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -241,7 +241,11 @@ "goToSeries": "转到系列", "goToSeason": "转到季", "shufflePlay": "随机播放", - "fileInfo": "文件信息" + "fileInfo": "文件信息", + "confirmDelete": "确定要从文件系统中删除此项吗?", + "deleteMultipleWarning": "可能会删除多个项目。", + "mediaDeletedSuccessfully": "媒体项已成功删除", + "mediaFailedToDelete": "删除媒体项失败" }, "accessibility": { "mediaCardMovie": "${title}, 电影", diff --git a/lib/screens/libraries/tabs/library_browse_tab.dart b/lib/screens/libraries/tabs/library_browse_tab.dart index 5b64e77a..2bf18e40 100644 --- a/lib/screens/libraries/tabs/library_browse_tab.dart +++ b/lib/screens/libraries/tabs/library_browse_tab.dart @@ -774,6 +774,7 @@ class _LibraryBrowseTabState extends BaseLibraryTabState with WatchStateAw _watchStateChanged = true; _updateWatchState(); }, + onListRefresh: () { + if (widget.isOffline) { + _loadSeasonsFromDownloads(); + } else { + _loadSeasons(); + } + }, ), ), ); @@ -969,6 +976,13 @@ class _MediaDetailScreenState extends State with WatchStateAw _watchStateChanged = true; _updateWatchState(); }, + onListRefresh: () { + if (widget.isOffline) { + _loadSeasonsFromDownloads(); + } else { + _loadSeasons(); + } + }, ); }, ); @@ -1589,11 +1603,7 @@ class _MediaDetailScreenState extends State with WatchStateAw ), ), ), - SliverPadding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom, - ), - ), + SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom)), ], ), // Sticky top bar with fading background @@ -1717,6 +1727,7 @@ class _SeasonCard extends StatefulWidget { final PlexClient? client; final VoidCallback onTap; final VoidCallback onRefresh; + final VoidCallback? onListRefresh; final bool isOffline; final String? localPosterPath; @@ -1725,6 +1736,7 @@ class _SeasonCard extends StatefulWidget { this.client, required this.onTap, required this.onRefresh, + this.onListRefresh, this.isOffline = false, this.localPosterPath, }); @@ -1753,6 +1765,7 @@ class _SeasonCardState extends State<_SeasonCard> { key: _contextMenuKey, item: widget.season, onRefresh: (ratingKey) => widget.onRefresh(), + onListRefresh: widget.onListRefresh, onTap: widget.onTap, child: Semantics( label: "media-season-${widget.season.ratingKey}", diff --git a/lib/screens/season_detail_screen.dart b/lib/screens/season_detail_screen.dart index a405d450..8dd1f566 100644 --- a/lib/screens/season_detail_screen.dart +++ b/lib/screens/season_detail_screen.dart @@ -234,14 +234,11 @@ class _SeasonDetailScreenState extends State with ItemUpdata ); }, onRefresh: widget.isOffline ? null : updateItem, + onListRefresh: widget.isOffline ? null : _loadEpisodes, ); }, childCount: _episodes.length), ), - SliverPadding( - padding: EdgeInsets.only( - bottom: MediaQuery.of(context).padding.bottom, - ), - ), + SliverPadding(padding: EdgeInsets.only(bottom: MediaQuery.of(context).padding.bottom)), ], ), ), @@ -266,6 +263,7 @@ class _EpisodeCard extends StatefulWidget { final PlexClient? client; final VoidCallback onTap; final Future Function(String)? onRefresh; + final Future Function()? onListRefresh; final bool autofocus; final bool isOffline; final String? localPosterPath; @@ -275,6 +273,7 @@ class _EpisodeCard extends StatefulWidget { this.client, required this.onTap, this.onRefresh, + this.onListRefresh, this.autofocus = false, this.isOffline = false, this.localPosterPath, @@ -338,6 +337,7 @@ class _EpisodeCardState extends State<_EpisodeCard> { key: _contextMenuKey, item: widget.episode, onRefresh: widget.onRefresh, + onListRefresh: widget.onListRefresh, onTap: widget.onTap, child: InkWell( key: Key(widget.episode.ratingKey), diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 95ff0930..72e2281b 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -1095,6 +1095,13 @@ class PlexClient { await _dio.put('/actions/removeFromContinueWatching', queryParameters: {'ratingKey': ratingKey}); } + /// Delete a media item from the library + /// This permanently removes the item and its associated files from the server + /// Returns true if deletion was successful, false otherwise + Future deleteMediaItem(String ratingKey) async { + return _wrapBoolApiCall(() => _dio.delete('/library/metadata/$ratingKey'), 'Failed to delete media item'); + } + /// Get server preferences Future> getServerPreferences() async { final response = await _dio.get('/:/prefs'); diff --git a/lib/widgets/focusable_list_tile.dart b/lib/widgets/focusable_list_tile.dart index c3699c9f..62b2d3f0 100644 --- a/lib/widgets/focusable_list_tile.dart +++ b/lib/widgets/focusable_list_tile.dart @@ -45,6 +45,9 @@ class FocusableListTile extends StatefulWidget { /// If true, consumes the first select key event to avoid accidental activation. final bool suppressInitialSelect; + /// An optional color to display behind the menu item when being hovered. + final Color? hoverColor; + const FocusableListTile({ super.key, this.title, @@ -60,6 +63,7 @@ class FocusableListTile extends StatefulWidget { this.autofocus = false, this.contentPadding, this.suppressInitialSelect = false, + this.hoverColor, }); @override @@ -84,6 +88,7 @@ class _FocusableListTileState extends State { contentPadding: widget.contentPadding, focusNode: widget.suppressInitialSelect ? null : widget.focusNode, autofocus: widget.suppressInitialSelect ? false : widget.autofocus, + hoverColor: widget.hoverColor, ); if (!widget.suppressInitialSelect) { diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 999ba2bf..b7831bf7 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -31,8 +31,10 @@ class _MenuAction { final String value; final IconData icon; final String label; + final PlexMediaType? mediaType; + final Color? hoverColor; - _MenuAction({required this.value, required this.icon, required this.label}); + _MenuAction({required this.value, required this.icon, required this.label, this.mediaType, this.hoverColor}); } /// A reusable wrapper widget that adds a context menu (long press / right click) @@ -244,6 +246,22 @@ class MediaContextMenuState extends State { mediaType == PlexMediaType.season) { menuActions.add(_MenuAction(value: 'add_to', icon: Symbols.add_rounded, label: t.common.addTo)); } + + // Delete media item (for episodes, movies, shows, and seasons) + if (mediaType == PlexMediaType.episode || + mediaType == PlexMediaType.movie || + mediaType == PlexMediaType.show || + mediaType == PlexMediaType.season) { + menuActions.add( + _MenuAction( + value: 'delete_media', + icon: Symbols.delete_rounded, + label: t.common.delete, + mediaType: mediaType, + hoverColor: Colors.red, + ), + ); + } } // End of regular menu items else block String? selected; @@ -411,6 +429,10 @@ class MediaContextMenuState extends State { case 'delete_download': await _handleDeleteDownload(context); break; + + case 'delete_media': + await _handleDeleteMediaItem(context, mediaType); + break; } } finally { _isContextMenuOpen = false; @@ -1028,6 +1050,42 @@ class MediaContextMenuState extends State { } } + /// Handle delete media item action + /// This permanently removes the media item and its associated files from the server + Future _handleDeleteMediaItem(BuildContext context, PlexMediaType? mediaType) async { + final client = _getClientForItem(); + final metadata = widget.item as PlexMetadata; + final isMultipleMediaItems = mediaType == PlexMediaType.show || mediaType == PlexMediaType.season; + + // Show confirmation dialog + final confirmed = await showDeleteConfirmation( + context, + title: t.common.delete, + message: "${t.mediaMenu.confirmDelete}${isMultipleMediaItems ? " ${t.mediaMenu.deleteMultipleWarning}" : ""}", + ); + + if (!confirmed || !context.mounted) return; + + try { + final success = await client.deleteMediaItem(metadata.ratingKey); + + if (context.mounted) { + if (success) { + showSuccessSnackBar(context, t.mediaMenu.mediaDeletedSuccessfully); + // Trigger list refresh to remove the item from the view + widget.onListRefresh?.call(); + } else { + showErrorSnackBar(context, t.mediaMenu.mediaFailedToDelete); + } + } + } catch (e) { + appLogger.e(t.mediaMenu.mediaFailedToDelete, error: e); + if (context.mounted) { + showErrorSnackBar(context, t.messages.errorLoading(error: e.toString())); + } + } + } + @override Widget build(BuildContext context) { return GestureDetector( @@ -1187,6 +1245,7 @@ class _FocusableContextMenuSheetState extends State<_FocusableContextMenuSheet> leading: AppIcon(action.icon, fill: 1), title: Text(action.label), onTap: () => Navigator.pop(context, action.value), + hoverColor: action.hoverColor, ); }), ], @@ -1293,6 +1352,7 @@ class _FocusablePopupMenuState extends State<_FocusablePopupMenu> { leading: AppIcon(action.icon, fill: 1, size: 20), title: Text(action.label), onTap: () => Navigator.pop(context, action.value), + hoverColor: action.hoverColor, ); }).toList(), ), From 63c1832c34983d946fc65a9aadcae413fe153c2b Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 2 Feb 2026 13:02:47 +0100 Subject: [PATCH 2/2] fix: address delete media PR review comments --- lib/widgets/media_context_menu.dart | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index b7831bf7..d774f797 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -31,10 +31,9 @@ class _MenuAction { final String value; final IconData icon; final String label; - final PlexMediaType? mediaType; final Color? hoverColor; - _MenuAction({required this.value, required this.icon, required this.label, this.mediaType, this.hoverColor}); + _MenuAction({required this.value, required this.icon, required this.label, this.hoverColor}); } /// A reusable wrapper widget that adds a context menu (long press / right click) @@ -257,7 +256,6 @@ class MediaContextMenuState extends State { value: 'delete_media', icon: Symbols.delete_rounded, label: t.common.delete, - mediaType: mediaType, hoverColor: Colors.red, ), ); @@ -1053,7 +1051,6 @@ class MediaContextMenuState extends State { /// Handle delete media item action /// This permanently removes the media item and its associated files from the server Future _handleDeleteMediaItem(BuildContext context, PlexMediaType? mediaType) async { - final client = _getClientForItem(); final metadata = widget.item as PlexMetadata; final isMultipleMediaItems = mediaType == PlexMediaType.show || mediaType == PlexMediaType.season; @@ -1061,12 +1058,13 @@ class MediaContextMenuState extends State { final confirmed = await showDeleteConfirmation( context, title: t.common.delete, - message: "${t.mediaMenu.confirmDelete}${isMultipleMediaItems ? " ${t.mediaMenu.deleteMultipleWarning}" : ""}", + message: "${t.mediaMenu.confirmDelete}${isMultipleMediaItems ? "\n${t.mediaMenu.deleteMultipleWarning}" : ""}", ); if (!confirmed || !context.mounted) return; try { + final client = _getClientForItem(); final success = await client.deleteMediaItem(metadata.ratingKey); if (context.mounted) { @@ -1081,7 +1079,7 @@ class MediaContextMenuState extends State { } catch (e) { appLogger.e(t.mediaMenu.mediaFailedToDelete, error: e); if (context.mounted) { - showErrorSnackBar(context, t.messages.errorLoading(error: e.toString())); + showErrorSnackBar(context, t.mediaMenu.mediaFailedToDelete); } } }