From ddb157809ad824041815b7aa0a00280a10cabcf5 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Mon, 30 Mar 2026 08:03:39 +0200 Subject: [PATCH] feat: play version context menu option --- lib/i18n/da.i18n.json | 3 +- lib/i18n/de.i18n.json | 3 +- lib/i18n/en.i18n.json | 3 +- lib/i18n/es.i18n.json | 3 +- lib/i18n/fr.i18n.json | 3 +- lib/i18n/it.i18n.json | 3 +- lib/i18n/ja.i18n.json | 3 +- lib/i18n/ko.i18n.json | 3 +- lib/i18n/nb.i18n.json | 3 +- lib/i18n/nl.i18n.json | 3 +- lib/i18n/pl.i18n.json | 3 +- lib/i18n/pt.i18n.json | 3 +- lib/i18n/ru.i18n.json | 3 +- lib/i18n/strings.g.dart | 4 +-- lib/i18n/strings_da.g.dart | 4 ++- lib/i18n/strings_de.g.dart | 4 ++- lib/i18n/strings_en.g.dart | 6 +++- lib/i18n/strings_es.g.dart | 4 ++- lib/i18n/strings_fr.g.dart | 4 ++- lib/i18n/strings_it.g.dart | 4 ++- lib/i18n/strings_ja.g.dart | 4 ++- lib/i18n/strings_ko.g.dart | 4 ++- lib/i18n/strings_nb.g.dart | 4 ++- lib/i18n/strings_nl.g.dart | 4 ++- lib/i18n/strings_pl.g.dart | 4 ++- lib/i18n/strings_pt.g.dart | 4 ++- lib/i18n/strings_ru.g.dart | 4 ++- lib/i18n/strings_sv.g.dart | 4 ++- lib/i18n/strings_zh.g.dart | 4 ++- lib/i18n/sv.i18n.json | 3 +- lib/i18n/zh.i18n.json | 3 +- lib/models/plex_metadata.dart | 6 ++++ lib/models/plex_metadata.g.dart | 3 ++ lib/widgets/media_context_menu.dart | 47 +++++++++++++++++++++++++++++ 34 files changed, 135 insertions(+), 32 deletions(-) diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index ea234757..3046891a 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Medieelement slettet", "mediaFailedToDelete": "Kunne ikke slette medieelement", "rate": "Bedøm", - "playFromBeginning": "Afspil fra begyndelsen" + "playFromBeginning": "Afspil fra begyndelsen", + "playVersion": "Afspil version..." }, "accessibility": { "mediaCardMovie": "${title}, film", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index afb3bbb1..1b8f11b6 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Medienelement erfolgreich gelöscht", "mediaFailedToDelete": "Löschen des Medienelements fehlgeschlagen", "rate": "Bewerten", - "playFromBeginning": "Von Anfang an abspielen" + "playFromBeginning": "Von Anfang an abspielen", + "playVersion": "Version abspielen..." }, "accessibility": { "mediaCardMovie": "${title}, Film", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 63b1ebb5..f806e5a3 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Media item deleted successfully", "mediaFailedToDelete": "Failed to delete media item", "rate": "Rate", - "playFromBeginning": "Play from Beginning" + "playFromBeginning": "Play from Beginning", + "playVersion": "Play Version..." }, "accessibility": { "mediaCardMovie": "${title}, movie", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index b08174b3..ab3a3e1e 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Elemento multimedia eliminado con éxito", "mediaFailedToDelete": "Error al eliminar el elemento multimedia", "rate": "Calificar", - "playFromBeginning": "Reproducir desde el inicio" + "playFromBeginning": "Reproducir desde el inicio", + "playVersion": "Reproducir versión..." }, "accessibility": { "mediaCardMovie": "${title}, película", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index f06b017f..25be32e1 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Élément média supprimé avec succès", "mediaFailedToDelete": "Échec de la suppression de l'élément média", "rate": "Noter", - "playFromBeginning": "Lire depuis le début" + "playFromBeginning": "Lire depuis le début", + "playVersion": "Lire la version..." }, "accessibility": { "mediaCardMovie": "${title}, film", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 3181abf9..c3d32a65 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Elemento multimediale eliminato con successo", "mediaFailedToDelete": "Impossibile eliminare l'elemento multimediale", "rate": "Valuta", - "playFromBeginning": "Riproduci dall'inizio" + "playFromBeginning": "Riproduci dall'inizio", + "playVersion": "Riproduci versione..." }, "accessibility": { "mediaCardMovie": "${title}, film", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index 7b3cd37f..009747a8 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "メディアアイテムを正常に削除しました", "mediaFailedToDelete": "メディアアイテムの削除に失敗しました", "rate": "評価", - "playFromBeginning": "最初から再生" + "playFromBeginning": "最初から再生", + "playVersion": "バージョンを再生..." }, "accessibility": { "mediaCardMovie": "${title}、映画", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 9ef71301..46e88712 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "미디어 항목이 성공적으로 삭제되었습니다", "mediaFailedToDelete": "미디어 항목 삭제 실패", "rate": "평가", - "playFromBeginning": "처음부터 재생" + "playFromBeginning": "처음부터 재생", + "playVersion": "버전 재생..." }, "accessibility": { "mediaCardMovie": "${title}, 영화", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index 670d23ce..eb6832a4 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Medieelement slettet", "mediaFailedToDelete": "Kunne ikke slette medieelement", "rate": "Vurder", - "playFromBeginning": "Spill fra begynnelsen" + "playFromBeginning": "Spill fra begynnelsen", + "playVersion": "Spill av versjon..." }, "accessibility": { "mediaCardMovie": "${title}, film", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 856f55a8..da6c8d23 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Media-item succesvol verwijderd", "mediaFailedToDelete": "Verwijderen van media-item mislukt", "rate": "Beoordelen", - "playFromBeginning": "Afspelen vanaf het begin" + "playFromBeginning": "Afspelen vanaf het begin", + "playVersion": "Versie afspelen..." }, "accessibility": { "mediaCardMovie": "${title}, film", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index 0cd31819..9d0e2b7b 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Element multimedialny usunięty pomyślnie", "mediaFailedToDelete": "Nie udało się usunąć elementu multimedialnego", "rate": "Oceń", - "playFromBeginning": "Odtwórz od początku" + "playFromBeginning": "Odtwórz od początku", + "playVersion": "Odtwórz wersję..." }, "accessibility": { "mediaCardMovie": "${title}, film", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index 4d1c3b9d..46881201 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Item de mídia excluído com sucesso", "mediaFailedToDelete": "Falha ao excluir item de mídia", "rate": "Avaliar", - "playFromBeginning": "Reproduzir do início" + "playFromBeginning": "Reproduzir do início", + "playVersion": "Reproduzir versão..." }, "accessibility": { "mediaCardMovie": "${title}, filme", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index c0fd9a24..68d178af 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Медиаэлемент успешно удалён", "mediaFailedToDelete": "Не удалось удалить медиаэлемент", "rate": "Оценить", - "playFromBeginning": "Воспроизвести сначала" + "playFromBeginning": "Воспроизвести сначала", + "playVersion": "Воспроизвести версию..." }, "accessibility": { "mediaCardMovie": "${title}, фильм", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 438d0791..c537969d 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 15 -/// Strings: 12105 (807 per locale) +/// Strings: 12120 (808 per locale) /// -/// Built on 2026-03-28 at 13:35 UTC +/// Built on 2026-03-29 at 18:23 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_da.g.dart b/lib/i18n/strings_da.g.dart index 14b1fc00..d56450e9 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuDa implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'Kunne ikke slette medieelement'; @override String get rate => 'Bedøm'; @override String get playFromBeginning => 'Afspil fra begyndelsen'; + @override String get playVersion => 'Afspil version...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsDa { 'mediaMenu.mediaFailedToDelete' => 'Kunne ikke slette medieelement', 'mediaMenu.rate' => 'Bedøm', 'mediaMenu.playFromBeginning' => 'Afspil fra begyndelsen', + 'mediaMenu.playVersion' => 'Afspil version...', '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}', @@ -1815,9 +1817,9 @@ extension on TranslationsDa { 'liveTv.now' => 'Nu', 'liveTv.today' => 'I dag', 'liveTv.midnight' => 'Midnat', - 'liveTv.overnight' => 'Nat', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Nat', 'liveTv.morning' => 'Morgen', 'liveTv.daytime' => 'Dagtid', 'liveTv.evening' => 'Aften', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 2ad5b2f0..1f851f21 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuDe implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'Löschen des Medienelements fehlgeschlagen'; @override String get rate => 'Bewerten'; @override String get playFromBeginning => 'Von Anfang an abspielen'; + @override String get playVersion => 'Version abspielen...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsDe { 'mediaMenu.mediaFailedToDelete' => 'Löschen des Medienelements fehlgeschlagen', 'mediaMenu.rate' => 'Bewerten', 'mediaMenu.playFromBeginning' => 'Von Anfang an abspielen', + 'mediaMenu.playVersion' => 'Version abspielen...', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, Film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, Serie', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1815,9 +1817,9 @@ extension on TranslationsDe { 'liveTv.now' => 'Jetzt', 'liveTv.today' => 'Heute', 'liveTv.midnight' => 'Mitternacht', - 'liveTv.overnight' => 'Nacht', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Nacht', 'liveTv.morning' => 'Morgen', 'liveTv.daytime' => 'Tagsüber', 'liveTv.evening' => 'Abend', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 615385b0..c5aa7ea6 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -932,6 +932,9 @@ class TranslationsMediaMenuEn { /// en: 'Play from Beginning' String get playFromBeginning => 'Play from Beginning'; + + /// en: 'Play Version...' + String get playVersion => 'Play Version...'; } // Path: accessibility @@ -3198,6 +3201,7 @@ extension on Translations { 'mediaMenu.mediaFailedToDelete' => 'Failed to delete media item', 'mediaMenu.rate' => 'Rate', 'mediaMenu.playFromBeginning' => 'Play from Beginning', + 'mediaMenu.playVersion' => 'Play Version...', '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}', @@ -3436,9 +3440,9 @@ extension on Translations { 'liveTv.now' => 'Now', 'liveTv.today' => 'Today', 'liveTv.midnight' => 'Midnight', - 'liveTv.overnight' => 'Overnight', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Overnight', 'liveTv.morning' => 'Morning', 'liveTv.daytime' => 'Daytime', 'liveTv.evening' => 'Evening', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 4714e772..62f1e894 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuEs implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'Error al eliminar el elemento multimedia'; @override String get rate => 'Calificar'; @override String get playFromBeginning => 'Reproducir desde el inicio'; + @override String get playVersion => 'Reproducir versión...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsEs { 'mediaMenu.mediaFailedToDelete' => 'Error al eliminar el elemento multimedia', 'mediaMenu.rate' => 'Calificar', 'mediaMenu.playFromBeginning' => 'Reproducir desde el inicio', + 'mediaMenu.playVersion' => 'Reproducir versión...', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, película', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, serie de TV', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1815,9 +1817,9 @@ extension on TranslationsEs { 'liveTv.now' => 'Ahora', 'liveTv.today' => 'Hoy', 'liveTv.midnight' => 'Medianoche', - 'liveTv.overnight' => 'Madrugada', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Madrugada', 'liveTv.morning' => 'Mañana', 'liveTv.daytime' => 'Día', 'liveTv.evening' => 'Noche', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 0ea61ab3..7577029b 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuFr implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'Échec de la suppression de l\'élément média'; @override String get rate => 'Noter'; @override String get playFromBeginning => 'Lire depuis le début'; + @override String get playVersion => 'Lire la version...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsFr { 'mediaMenu.mediaFailedToDelete' => 'Échec de la suppression de l\'élément média', 'mediaMenu.rate' => 'Noter', 'mediaMenu.playFromBeginning' => 'Lire depuis le début', + 'mediaMenu.playVersion' => 'Lire la version...', '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}', @@ -1815,9 +1817,9 @@ extension on TranslationsFr { 'liveTv.now' => 'Maintenant', 'liveTv.today' => 'Aujourd\'hui', 'liveTv.midnight' => 'Minuit', - 'liveTv.overnight' => 'Nuit', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Nuit', 'liveTv.morning' => 'Matin', 'liveTv.daytime' => 'Journée', 'liveTv.evening' => 'Soirée', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index b2f265a6..400f1eab 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuIt implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'Impossibile eliminare l\'elemento multimediale'; @override String get rate => 'Valuta'; @override String get playFromBeginning => 'Riproduci dall\'inizio'; + @override String get playVersion => 'Riproduci versione...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsIt { 'mediaMenu.mediaFailedToDelete' => 'Impossibile eliminare l\'elemento multimediale', 'mediaMenu.rate' => 'Valuta', 'mediaMenu.playFromBeginning' => 'Riproduci dall\'inizio', + 'mediaMenu.playVersion' => 'Riproduci versione...', '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}', @@ -1815,9 +1817,9 @@ extension on TranslationsIt { 'liveTv.now' => 'Ora', 'liveTv.today' => 'Oggi', 'liveTv.midnight' => 'Mezzanotte', - 'liveTv.overnight' => 'Notte', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Notte', 'liveTv.morning' => 'Mattina', 'liveTv.daytime' => 'Giorno', 'liveTv.evening' => 'Sera', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index 20f57070..38d64a84 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuJa implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'メディアアイテムの削除に失敗しました'; @override String get rate => '評価'; @override String get playFromBeginning => '最初から再生'; + @override String get playVersion => 'バージョンを再生...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsJa { 'mediaMenu.mediaFailedToDelete' => 'メディアアイテムの削除に失敗しました', 'mediaMenu.rate' => '評価', 'mediaMenu.playFromBeginning' => '最初から再生', + 'mediaMenu.playVersion' => 'バージョンを再生...', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}、映画', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}、テレビ番組', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}、${episodeInfo}', @@ -1815,9 +1817,9 @@ extension on TranslationsJa { 'liveTv.now' => '現在', 'liveTv.today' => '今日', 'liveTv.midnight' => '深夜', - 'liveTv.overnight' => '深夜', _ => null, } ?? switch (path) { + 'liveTv.overnight' => '深夜', 'liveTv.morning' => '朝', 'liveTv.daytime' => '昼', 'liveTv.evening' => '夕方', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index dbaa4797..56b043f9 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuKo implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => '미디어 항목 삭제 실패'; @override String get rate => '평가'; @override String get playFromBeginning => '처음부터 재생'; + @override String get playVersion => '버전 재생...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsKo { 'mediaMenu.mediaFailedToDelete' => '미디어 항목 삭제 실패', 'mediaMenu.rate' => '평가', 'mediaMenu.playFromBeginning' => '처음부터 재생', + 'mediaMenu.playVersion' => '버전 재생...', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, 영화', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, TV 프로그램', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1815,9 +1817,9 @@ extension on TranslationsKo { 'liveTv.now' => '지금', 'liveTv.today' => '오늘', 'liveTv.midnight' => '자정', - 'liveTv.overnight' => '심야', _ => null, } ?? switch (path) { + 'liveTv.overnight' => '심야', 'liveTv.morning' => '아침', 'liveTv.daytime' => '낮', 'liveTv.evening' => '저녁', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index 199d27e1..fcb267f6 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuNb implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'Kunne ikke slette medieelement'; @override String get rate => 'Vurder'; @override String get playFromBeginning => 'Spill fra begynnelsen'; + @override String get playVersion => 'Spill av versjon...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsNb { 'mediaMenu.mediaFailedToDelete' => 'Kunne ikke slette medieelement', 'mediaMenu.rate' => 'Vurder', 'mediaMenu.playFromBeginning' => 'Spill fra begynnelsen', + 'mediaMenu.playVersion' => 'Spill av versjon...', '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}', @@ -1815,9 +1817,9 @@ extension on TranslationsNb { 'liveTv.now' => 'Nå', 'liveTv.today' => 'I dag', 'liveTv.midnight' => 'Midnatt', - 'liveTv.overnight' => 'Natt', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Natt', 'liveTv.morning' => 'Morgen', 'liveTv.daytime' => 'Dagtid', 'liveTv.evening' => 'Kveld', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 87e2cf23..d244b68f 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuNl implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'Verwijderen van media-item mislukt'; @override String get rate => 'Beoordelen'; @override String get playFromBeginning => 'Afspelen vanaf het begin'; + @override String get playVersion => 'Versie afspelen...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsNl { 'mediaMenu.mediaFailedToDelete' => 'Verwijderen van media-item mislukt', 'mediaMenu.rate' => 'Beoordelen', 'mediaMenu.playFromBeginning' => 'Afspelen vanaf het begin', + 'mediaMenu.playVersion' => 'Versie afspelen...', '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}', @@ -1815,9 +1817,9 @@ extension on TranslationsNl { 'liveTv.now' => 'Nu', 'liveTv.today' => 'Vandaag', 'liveTv.midnight' => 'Middernacht', - 'liveTv.overnight' => 'Nacht', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Nacht', 'liveTv.morning' => 'Ochtend', 'liveTv.daytime' => 'Overdag', 'liveTv.evening' => 'Avond', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index 4e632a1d..dcd39b1f 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuPl implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'Nie udało się usunąć elementu multimedialnego'; @override String get rate => 'Oceń'; @override String get playFromBeginning => 'Odtwórz od początku'; + @override String get playVersion => 'Odtwórz wersję...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsPl { 'mediaMenu.mediaFailedToDelete' => 'Nie udało się usunąć elementu multimedialnego', 'mediaMenu.rate' => 'Oceń', 'mediaMenu.playFromBeginning' => 'Odtwórz od początku', + 'mediaMenu.playVersion' => 'Odtwórz wersję...', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, film', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, serial TV', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1815,9 +1817,9 @@ extension on TranslationsPl { 'liveTv.now' => 'Teraz', 'liveTv.today' => 'Dzisiaj', 'liveTv.midnight' => 'Północ', - 'liveTv.overnight' => 'Nocą', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Nocą', 'liveTv.morning' => 'Rano', 'liveTv.daytime' => 'W ciągu dnia', 'liveTv.evening' => 'Wieczorem', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index b9a3ea1e..15426532 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuPt implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'Falha ao excluir item de mídia'; @override String get rate => 'Avaliar'; @override String get playFromBeginning => 'Reproduzir do início'; + @override String get playVersion => 'Reproduzir versão...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsPt { 'mediaMenu.mediaFailedToDelete' => 'Falha ao excluir item de mídia', 'mediaMenu.rate' => 'Avaliar', 'mediaMenu.playFromBeginning' => 'Reproduzir do início', + 'mediaMenu.playVersion' => 'Reproduzir versão...', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, filme', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, série de TV', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1815,9 +1817,9 @@ extension on TranslationsPt { 'liveTv.now' => 'Agora', 'liveTv.today' => 'Hoje', 'liveTv.midnight' => 'Meia-noite', - 'liveTv.overnight' => 'Madrugada', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Madrugada', 'liveTv.morning' => 'Manhã', 'liveTv.daytime' => 'Dia', 'liveTv.evening' => 'Noite', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index 5f79c0c1..325a2b8b 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuRu implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'Не удалось удалить медиаэлемент'; @override String get rate => 'Оценить'; @override String get playFromBeginning => 'Воспроизвести сначала'; + @override String get playVersion => 'Воспроизвести версию...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsRu { 'mediaMenu.mediaFailedToDelete' => 'Не удалось удалить медиаэлемент', 'mediaMenu.rate' => 'Оценить', 'mediaMenu.playFromBeginning' => 'Воспроизвести сначала', + 'mediaMenu.playVersion' => 'Воспроизвести версию...', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, фильм', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, сериал', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1815,9 +1817,9 @@ extension on TranslationsRu { 'liveTv.now' => 'Сейчас', 'liveTv.today' => 'Сегодня', 'liveTv.midnight' => 'Полночь', - 'liveTv.overnight' => 'Ночь', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Ночь', 'liveTv.morning' => 'Утро', 'liveTv.daytime' => 'День', 'liveTv.evening' => 'Вечер', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 1331437b..18d63d5c 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuSv implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => 'Kunde inte ta bort mediaobjekt'; @override String get rate => 'Betygsätt'; @override String get playFromBeginning => 'Spela från början'; + @override String get playVersion => 'Spela version...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsSv { 'mediaMenu.mediaFailedToDelete' => 'Kunde inte ta bort mediaobjekt', 'mediaMenu.rate' => 'Betygsätt', 'mediaMenu.playFromBeginning' => 'Spela från början', + 'mediaMenu.playVersion' => 'Spela version...', '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}', @@ -1815,9 +1817,9 @@ extension on TranslationsSv { 'liveTv.now' => 'Nu', 'liveTv.today' => 'Idag', 'liveTv.midnight' => 'Midnatt', - 'liveTv.overnight' => 'Natt', _ => null, } ?? switch (path) { + 'liveTv.overnight' => 'Natt', 'liveTv.morning' => 'Morgon', 'liveTv.daytime' => 'Dagtid', 'liveTv.evening' => 'Kväll', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index 9b012d0b..3a791e30 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -420,6 +420,7 @@ class _TranslationsMediaMenuZh implements TranslationsMediaMenuEn { @override String get mediaFailedToDelete => '删除媒体项失败'; @override String get rate => '评分'; @override String get playFromBeginning => '从头播放'; + @override String get playVersion => '播放版本...'; } // Path: accessibility @@ -1577,6 +1578,7 @@ extension on TranslationsZh { 'mediaMenu.mediaFailedToDelete' => '删除媒体项失败', 'mediaMenu.rate' => '评分', 'mediaMenu.playFromBeginning' => '从头播放', + 'mediaMenu.playVersion' => '播放版本...', 'accessibility.mediaCardMovie' => ({required Object title}) => '${title}, 电影', 'accessibility.mediaCardShow' => ({required Object title}) => '${title}, 电视剧', 'accessibility.mediaCardEpisode' => ({required Object title, required Object episodeInfo}) => '${title}, ${episodeInfo}', @@ -1815,9 +1817,9 @@ extension on TranslationsZh { 'liveTv.now' => '现在', 'liveTv.today' => '今天', 'liveTv.midnight' => '午夜', - 'liveTv.overnight' => '凌晨', _ => null, } ?? switch (path) { + 'liveTv.overnight' => '凌晨', 'liveTv.morning' => '上午', 'liveTv.daytime' => '白天', 'liveTv.evening' => '晚上', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index 9b5451a4..80d18b6e 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "Mediaobjekt borttaget", "mediaFailedToDelete": "Kunde inte ta bort mediaobjekt", "rate": "Betygsätt", - "playFromBeginning": "Spela från början" + "playFromBeginning": "Spela från början", + "playVersion": "Spela version..." }, "accessibility": { "mediaCardMovie": "${title}, film", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index c230b152..4f72d4eb 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -292,7 +292,8 @@ "mediaDeletedSuccessfully": "媒体项已成功删除", "mediaFailedToDelete": "删除媒体项失败", "rate": "评分", - "playFromBeginning": "从头播放" + "playFromBeginning": "从头播放", + "playVersion": "播放版本..." }, "accessibility": { "mediaCardMovie": "${title}, 电影", diff --git a/lib/models/plex_metadata.dart b/lib/models/plex_metadata.dart index 0735b4c1..f69d0346 100644 --- a/lib/models/plex_metadata.dart +++ b/lib/models/plex_metadata.dart @@ -4,6 +4,7 @@ import 'package:sentry_flutter/sentry_flutter.dart'; import '../services/settings_service.dart' show EpisodePosterMode; import '../widgets/plex_optimized_image.dart' show kBlurArtwork, obfuscateText; import 'mixins/multi_server_fields.dart'; +import 'plex_media_version.dart'; import 'plex_role.dart'; import '../utils/global_key_utils.dart'; @@ -99,6 +100,8 @@ class PlexMetadata with MultiServerFields { final int? childCount; // Number of items in a collection or playlist @JsonKey(name: 'Role') final List? role; // Cast members + @JsonKey(name: 'Media', includeToJson: false) + final List? mediaVersions; // Available media versions/editions final String? audioLanguage; // Per-media preferred audio language final String? subtitleLanguage; // Per-media preferred subtitle language @JsonKey(fromJson: _flexibleInt) @@ -201,6 +204,7 @@ class PlexMetadata with MultiServerFields { this.viewedLeafCount, this.childCount, this.role, + this.mediaVersions, this.audioLanguage, this.subtitleLanguage, this.subtitleMode, @@ -260,6 +264,7 @@ class PlexMetadata with MultiServerFields { int? viewedLeafCount, int? childCount, List? role, + List? mediaVersions, String? audioLanguage, String? subtitleLanguage, int? subtitleMode, @@ -317,6 +322,7 @@ class PlexMetadata with MultiServerFields { viewedLeafCount: viewedLeafCount ?? this.viewedLeafCount, childCount: childCount ?? this.childCount, role: role ?? this.role, + mediaVersions: mediaVersions ?? this.mediaVersions, audioLanguage: audioLanguage ?? this.audioLanguage, subtitleLanguage: subtitleLanguage ?? this.subtitleLanguage, subtitleMode: subtitleMode ?? this.subtitleMode, diff --git a/lib/models/plex_metadata.g.dart b/lib/models/plex_metadata.g.dart index 8bfe8dcf..22cd4c1d 100644 --- a/lib/models/plex_metadata.g.dart +++ b/lib/models/plex_metadata.g.dart @@ -45,6 +45,9 @@ PlexMetadata _$PlexMetadataFromJson(Map json) => PlexMetadata( role: (json['Role'] as List?) ?.map((e) => PlexRole.fromJson(e as Map)) .toList(), + mediaVersions: (json['Media'] as List?) + ?.map((e) => PlexMediaVersion.fromJson(e as Map)) + .toList(), audioLanguage: json['audioLanguage'] as String?, subtitleLanguage: json['subtitleLanguage'] as String?, subtitleMode: _flexibleInt(json['subtitleMode']), diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 4b8aa634..3ce06b15 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -261,6 +261,15 @@ class MediaContextMenuState extends State { ); } + // Play Version (for episodes and movies with multiple versions) + if ((mediaType == PlexMediaType.episode || mediaType == PlexMediaType.movie) && + metadata.mediaVersions != null && + metadata.mediaVersions!.length > 1) { + menuActions.add( + _MenuAction(value: 'play_version', icon: Symbols.video_file_rounded, label: t.mediaMenu.playVersion), + ); + } + // File Info (for episodes and movies) if (mediaType == PlexMediaType.episode || mediaType == PlexMediaType.movie) { menuActions.add(_MenuAction(value: 'fileinfo', icon: Symbols.info_rounded, label: t.mediaMenu.fileInfo)); @@ -490,6 +499,10 @@ class MediaContextMenuState extends State { ); break; + case 'play_version': + didNavigate = await _handlePlayVersion(context); + break; + case 'fileinfo': await _showFileInfo(context); break; @@ -629,6 +642,40 @@ class MediaContextMenuState extends State { } } + /// Handle play version selection + Future _handlePlayVersion(BuildContext context) async { + final metadata = widget.item as PlexMetadata; + final versions = metadata.mediaVersions!; + + final selectedIndex = await showDialog( + context: context, + builder: (dialogContext) => SimpleDialog( + title: Text(t.mediaMenu.playVersion), + contentPadding: const EdgeInsets.symmetric(vertical: 8), + children: List.generate(versions.length, (index) { + final version = versions[index]; + return SimpleDialogOption( + onPressed: () => Navigator.pop(dialogContext, index), + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16), + child: Row( + children: [ + AppIcon(Symbols.video_file_rounded, fill: 1, size: 24), + const SizedBox(width: 16), + Text(version.displayLabel, style: Theme.of(dialogContext).textTheme.bodyLarge), + ], + ), + ); + }), + ), + ); + + if (selectedIndex != null && context.mounted) { + await navigateToVideoPlayer(context, metadata: metadata, selectedMediaIndex: selectedIndex); + return true; + } + return false; + } + /// Handle shuffle play using play queues Future _handleShufflePlayWithQueue(BuildContext context) async { final client = _getClientForItem();