From da8b58328690cbd2706ea8fd3be3944811cc54e5 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Tue, 31 Mar 2026 14:10:17 +0200 Subject: [PATCH] feat: tag editing in metadata editor --- lib/i18n/da.i18n.json | 13 ++- lib/i18n/de.i18n.json | 13 ++- lib/i18n/en.i18n.json | 13 ++- lib/i18n/es.i18n.json | 13 ++- lib/i18n/fr.i18n.json | 13 ++- lib/i18n/it.i18n.json | 13 ++- lib/i18n/ja.i18n.json | 13 ++- lib/i18n/ko.i18n.json | 13 ++- lib/i18n/nb.i18n.json | 13 ++- lib/i18n/nl.i18n.json | 13 ++- lib/i18n/pl.i18n.json | 13 ++- lib/i18n/pt.i18n.json | 13 ++- lib/i18n/ru.i18n.json | 13 ++- lib/i18n/strings.g.dart | 4 +- lib/i18n/strings_da.g.dart | 22 +++++ lib/i18n/strings_de.g.dart | 22 +++++ lib/i18n/strings_en.g.dart | 44 +++++++++ lib/i18n/strings_es.g.dart | 22 +++++ lib/i18n/strings_fr.g.dart | 22 +++++ lib/i18n/strings_it.g.dart | 22 +++++ lib/i18n/strings_ja.g.dart | 22 +++++ lib/i18n/strings_ko.g.dart | 22 +++++ lib/i18n/strings_nb.g.dart | 22 +++++ lib/i18n/strings_nl.g.dart | 22 +++++ lib/i18n/strings_pl.g.dart | 22 +++++ lib/i18n/strings_pt.g.dart | 22 +++++ lib/i18n/strings_ru.g.dart | 22 +++++ lib/i18n/strings_sv.g.dart | 22 +++++ lib/i18n/strings_zh.g.dart | 22 +++++ lib/i18n/sv.i18n.json | 13 ++- lib/i18n/zh.i18n.json | 13 ++- lib/models/plex_metadata.dart | 48 ++++++++++ lib/models/plex_metadata.g.dart | 9 ++ lib/screens/metadata_edit_screen.dart | 119 +++++++++++++++++++++++- lib/services/plex_client.dart | 18 ++++ lib/widgets/tag_edit_dialog.dart | 125 ++++++++++++++++++++++++++ 36 files changed, 852 insertions(+), 18 deletions(-) create mode 100644 lib/widgets/tag_edit_dialog.dart diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index 47dd0ad0..70ee4b53 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Auto-vælg underteksttilstand", "manuallySelected": "Manuelt valgt", "shownWithForeignAudio": "Vist med fremmedsproget lyd", - "alwaysEnabled": "Altid aktiveret" + "alwaysEnabled": "Altid aktiveret", + "tags": "Tags", + "addTag": "Tilføj tag", + "genre": "Genre", + "director": "Instruktør", + "writer": "Forfatter", + "producer": "Producer", + "country": "Land", + "collection": "Samling", + "label": "Etiket", + "style": "Stil", + "mood": "Stemning" }, "serverTasks": { "title": "Serveropgaver", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index db7c3dd2..1767db76 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Automatische Untertitelauswahl", "manuallySelected": "Manuell ausgewählt", "shownWithForeignAudio": "Bei fremdsprachigem Audio anzeigen", - "alwaysEnabled": "Immer aktiviert" + "alwaysEnabled": "Immer aktiviert", + "tags": "Tags", + "addTag": "Tag hinzufügen", + "genre": "Genre", + "director": "Regisseur", + "writer": "Autor", + "producer": "Produzent", + "country": "Land", + "collection": "Sammlung", + "label": "Label", + "style": "Stil", + "mood": "Stimmung" }, "serverTasks": { "title": "Serveraufgaben", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 5d2cd05a..006865dc 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Auto-Select Subtitle Mode", "manuallySelected": "Manually selected", "shownWithForeignAudio": "Shown with foreign audio", - "alwaysEnabled": "Always enabled" + "alwaysEnabled": "Always enabled", + "tags": "Tags", + "addTag": "Add tag", + "genre": "Genre", + "director": "Director", + "writer": "Writer", + "producer": "Producer", + "country": "Country", + "collection": "Collection", + "label": "Label", + "style": "Style", + "mood": "Mood" }, "serverTasks": { "title": "Server Tasks", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 4f13c784..a69ca9ec 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Selección automática de subtítulos", "manuallySelected": "Seleccionado manualmente", "shownWithForeignAudio": "Mostrar con audio extranjero", - "alwaysEnabled": "Siempre activado" + "alwaysEnabled": "Siempre activado", + "tags": "Etiquetas", + "addTag": "Añadir etiqueta", + "genre": "Género", + "director": "Director", + "writer": "Guionista", + "producer": "Productor", + "country": "País", + "collection": "Colección", + "label": "Etiqueta", + "style": "Estilo", + "mood": "Estado de ánimo" }, "serverTasks": { "title": "Tareas del servidor", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index dc415658..ea251f60 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Sélection automatique des sous-titres", "manuallySelected": "Sélectionné manuellement", "shownWithForeignAudio": "Affichés avec audio étranger", - "alwaysEnabled": "Toujours activé" + "alwaysEnabled": "Toujours activé", + "tags": "Tags", + "addTag": "Ajouter un tag", + "genre": "Genre", + "director": "Réalisateur", + "writer": "Scénariste", + "producer": "Producteur", + "country": "Pays", + "collection": "Collection", + "label": "Label", + "style": "Style", + "mood": "Ambiance" }, "serverTasks": { "title": "Tâches du serveur", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 36785965..57dbb91a 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Selezione automatica sottotitoli", "manuallySelected": "Selezionato manualmente", "shownWithForeignAudio": "Mostrati con audio straniero", - "alwaysEnabled": "Sempre attivo" + "alwaysEnabled": "Sempre attivo", + "tags": "Tag", + "addTag": "Aggiungi tag", + "genre": "Genere", + "director": "Regista", + "writer": "Sceneggiatore", + "producer": "Produttore", + "country": "Paese", + "collection": "Collezione", + "label": "Etichetta", + "style": "Stile", + "mood": "Atmosfera" }, "serverTasks": { "title": "Attività del server", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index 96b1db25..b00a74be 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "字幕自動選択モード", "manuallySelected": "手動選択", "shownWithForeignAudio": "外国語音声時に表示", - "alwaysEnabled": "常に有効" + "alwaysEnabled": "常に有効", + "tags": "タグ", + "addTag": "タグを追加", + "genre": "ジャンル", + "director": "監督", + "writer": "脚本", + "producer": "プロデューサー", + "country": "国", + "collection": "コレクション", + "label": "ラベル", + "style": "スタイル", + "mood": "ムード" }, "serverTasks": { "title": "サーバータスク", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index 6998c9f4..ffa6907d 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "자막 자동 선택 모드", "manuallySelected": "수동 선택", "shownWithForeignAudio": "외국어 오디오 시 표시", - "alwaysEnabled": "항상 활성화" + "alwaysEnabled": "항상 활성화", + "tags": "태그", + "addTag": "태그 추가", + "genre": "장르", + "director": "감독", + "writer": "작가", + "producer": "프로듀서", + "country": "국가", + "collection": "컬렉션", + "label": "라벨", + "style": "스타일", + "mood": "분위기" }, "serverTasks": { "title": "서버 작업", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index 269d919d..eab3dc4b 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Automatisk valg av undertekstmodus", "manuallySelected": "Manuelt valgt", "shownWithForeignAudio": "Vist med fremmedspråklig lyd", - "alwaysEnabled": "Alltid aktivert" + "alwaysEnabled": "Alltid aktivert", + "tags": "Tagger", + "addTag": "Legg til tagg", + "genre": "Sjanger", + "director": "Regissør", + "writer": "Forfatter", + "producer": "Produsent", + "country": "Land", + "collection": "Samling", + "label": "Etikett", + "style": "Stil", + "mood": "Stemning" }, "serverTasks": { "title": "Serveroppgaver", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index a549a61e..4eca6910 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Automatische ondertitelselectie", "manuallySelected": "Handmatig geselecteerd", "shownWithForeignAudio": "Weergeven bij anderstalig geluid", - "alwaysEnabled": "Altijd ingeschakeld" + "alwaysEnabled": "Altijd ingeschakeld", + "tags": "Tags", + "addTag": "Tag toevoegen", + "genre": "Genre", + "director": "Regisseur", + "writer": "Schrijver", + "producer": "Producent", + "country": "Land", + "collection": "Collectie", + "label": "Label", + "style": "Stijl", + "mood": "Stemming" }, "serverTasks": { "title": "Servertaken", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index cf126307..f0dfd004 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Tryb automatycznego wyboru napisów", "manuallySelected": "Wybrany ręcznie", "shownWithForeignAudio": "Wyświetlane przy obcojęzycznym audio", - "alwaysEnabled": "Zawsze włączone" + "alwaysEnabled": "Zawsze włączone", + "tags": "Tagi", + "addTag": "Dodaj tag", + "genre": "Gatunek", + "director": "Reżyser", + "writer": "Scenarzysta", + "producer": "Producent", + "country": "Kraj", + "collection": "Kolekcja", + "label": "Etykieta", + "style": "Styl", + "mood": "Nastrój" }, "serverTasks": { "title": "Zadania serwera", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index 4bd96703..d72e5c66 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Modo de Seleção Automática de Legendas", "manuallySelected": "Seleção manual", "shownWithForeignAudio": "Exibir com áudio estrangeiro", - "alwaysEnabled": "Sempre ativado" + "alwaysEnabled": "Sempre ativado", + "tags": "Tags", + "addTag": "Adicionar tag", + "genre": "Gênero", + "director": "Diretor", + "writer": "Roteirista", + "producer": "Produtor", + "country": "País", + "collection": "Coleção", + "label": "Rótulo", + "style": "Estilo", + "mood": "Humor" }, "serverTasks": { "title": "Tarefas do servidor", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index 1919668a..2d765abb 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Автовыбор субтитров", "manuallySelected": "Выбор вручную", "shownWithForeignAudio": "Показывать при иноязычном аудио", - "alwaysEnabled": "Всегда включены" + "alwaysEnabled": "Всегда включены", + "tags": "Теги", + "addTag": "Добавить тег", + "genre": "Жанр", + "director": "Режиссёр", + "writer": "Сценарист", + "producer": "Продюсер", + "country": "Страна", + "collection": "Коллекция", + "label": "Метка", + "style": "Стиль", + "mood": "Настроение" }, "serverTasks": { "title": "Задачи сервера", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 1602ea9c..db840703 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: 12135 (809 per locale) +/// Strings: 12300 (820 per locale) /// -/// Built on 2026-03-31 at 02:43 UTC +/// Built on 2026-03-31 at 11:48 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 a2c632fd..5ffa78a0 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditDa implements TranslationsMetadataEditEn { @override String get manuallySelected => 'Manuelt valgt'; @override String get shownWithForeignAudio => 'Vist med fremmedsproget lyd'; @override String get alwaysEnabled => 'Altid aktiveret'; + @override String get tags => 'Tags'; + @override String get addTag => 'Tilføj tag'; + @override String get genre => 'Genre'; + @override String get director => 'Instruktør'; + @override String get writer => 'Forfatter'; + @override String get producer => 'Producer'; + @override String get country => 'Land'; + @override String get collection => 'Samling'; + @override String get label => 'Etiket'; + @override String get style => 'Stil'; + @override String get mood => 'Stemning'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsDa { 'metadataEdit.manuallySelected' => 'Manuelt valgt', 'metadataEdit.shownWithForeignAudio' => 'Vist med fremmedsproget lyd', 'metadataEdit.alwaysEnabled' => 'Altid aktiveret', + 'metadataEdit.tags' => 'Tags', + 'metadataEdit.addTag' => 'Tilføj tag', + 'metadataEdit.genre' => 'Genre', + 'metadataEdit.director' => 'Instruktør', + 'metadataEdit.writer' => 'Forfatter', + 'metadataEdit.producer' => 'Producer', + 'metadataEdit.country' => 'Land', + 'metadataEdit.collection' => 'Samling', + 'metadataEdit.label' => 'Etiket', + 'metadataEdit.style' => 'Stil', + 'metadataEdit.mood' => 'Stemning', 'serverTasks.title' => 'Serveropgaver', 'serverTasks.failedToLoad' => 'Kunne ikke indlæse opgaver', 'serverTasks.noTasks' => 'Ingen opgaver kører', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index 8a705208..edd0acd5 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditDe implements TranslationsMetadataEditEn { @override String get manuallySelected => 'Manuell ausgewählt'; @override String get shownWithForeignAudio => 'Bei fremdsprachigem Audio anzeigen'; @override String get alwaysEnabled => 'Immer aktiviert'; + @override String get tags => 'Tags'; + @override String get addTag => 'Tag hinzufügen'; + @override String get genre => 'Genre'; + @override String get director => 'Regisseur'; + @override String get writer => 'Autor'; + @override String get producer => 'Produzent'; + @override String get country => 'Land'; + @override String get collection => 'Sammlung'; + @override String get label => 'Label'; + @override String get style => 'Stil'; + @override String get mood => 'Stimmung'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsDe { 'metadataEdit.manuallySelected' => 'Manuell ausgewählt', 'metadataEdit.shownWithForeignAudio' => 'Bei fremdsprachigem Audio anzeigen', 'metadataEdit.alwaysEnabled' => 'Immer aktiviert', + 'metadataEdit.tags' => 'Tags', + 'metadataEdit.addTag' => 'Tag hinzufügen', + 'metadataEdit.genre' => 'Genre', + 'metadataEdit.director' => 'Regisseur', + 'metadataEdit.writer' => 'Autor', + 'metadataEdit.producer' => 'Produzent', + 'metadataEdit.country' => 'Land', + 'metadataEdit.collection' => 'Sammlung', + 'metadataEdit.label' => 'Label', + 'metadataEdit.style' => 'Stil', + 'metadataEdit.mood' => 'Stimmung', 'serverTasks.title' => 'Serveraufgaben', 'serverTasks.failedToLoad' => 'Aufgaben konnten nicht geladen werden', 'serverTasks.noTasks' => 'Keine laufenden Aufgaben', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index f81da097..9ca358b6 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -2522,6 +2522,39 @@ class TranslationsMetadataEditEn { /// en: 'Always enabled' String get alwaysEnabled => 'Always enabled'; + + /// en: 'Tags' + String get tags => 'Tags'; + + /// en: 'Add tag' + String get addTag => 'Add tag'; + + /// en: 'Genre' + String get genre => 'Genre'; + + /// en: 'Director' + String get director => 'Director'; + + /// en: 'Writer' + String get writer => 'Writer'; + + /// en: 'Producer' + String get producer => 'Producer'; + + /// en: 'Country' + String get country => 'Country'; + + /// en: 'Collection' + String get collection => 'Collection'; + + /// en: 'Label' + String get label => 'Label'; + + /// en: 'Style' + String get style => 'Style'; + + /// en: 'Mood' + String get mood => 'Mood'; } // Path: serverTasks @@ -3739,6 +3772,17 @@ extension on Translations { 'metadataEdit.manuallySelected' => 'Manually selected', 'metadataEdit.shownWithForeignAudio' => 'Shown with foreign audio', 'metadataEdit.alwaysEnabled' => 'Always enabled', + 'metadataEdit.tags' => 'Tags', + 'metadataEdit.addTag' => 'Add tag', + 'metadataEdit.genre' => 'Genre', + 'metadataEdit.director' => 'Director', + 'metadataEdit.writer' => 'Writer', + 'metadataEdit.producer' => 'Producer', + 'metadataEdit.country' => 'Country', + 'metadataEdit.collection' => 'Collection', + 'metadataEdit.label' => 'Label', + 'metadataEdit.style' => 'Style', + 'metadataEdit.mood' => 'Mood', 'serverTasks.title' => 'Server Tasks', 'serverTasks.failedToLoad' => 'Failed to load tasks', 'serverTasks.noTasks' => 'No tasks running', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index cc023d52..b16948f0 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditEs implements TranslationsMetadataEditEn { @override String get manuallySelected => 'Seleccionado manualmente'; @override String get shownWithForeignAudio => 'Mostrar con audio extranjero'; @override String get alwaysEnabled => 'Siempre activado'; + @override String get tags => 'Etiquetas'; + @override String get addTag => 'Añadir etiqueta'; + @override String get genre => 'Género'; + @override String get director => 'Director'; + @override String get writer => 'Guionista'; + @override String get producer => 'Productor'; + @override String get country => 'País'; + @override String get collection => 'Colección'; + @override String get label => 'Etiqueta'; + @override String get style => 'Estilo'; + @override String get mood => 'Estado de ánimo'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsEs { 'metadataEdit.manuallySelected' => 'Seleccionado manualmente', 'metadataEdit.shownWithForeignAudio' => 'Mostrar con audio extranjero', 'metadataEdit.alwaysEnabled' => 'Siempre activado', + 'metadataEdit.tags' => 'Etiquetas', + 'metadataEdit.addTag' => 'Añadir etiqueta', + 'metadataEdit.genre' => 'Género', + 'metadataEdit.director' => 'Director', + 'metadataEdit.writer' => 'Guionista', + 'metadataEdit.producer' => 'Productor', + 'metadataEdit.country' => 'País', + 'metadataEdit.collection' => 'Colección', + 'metadataEdit.label' => 'Etiqueta', + 'metadataEdit.style' => 'Estilo', + 'metadataEdit.mood' => 'Estado de ánimo', 'serverTasks.title' => 'Tareas del servidor', 'serverTasks.failedToLoad' => 'Error al cargar tareas', 'serverTasks.noTasks' => 'No hay tareas en ejecución', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index f2722005..88d910f1 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditFr implements TranslationsMetadataEditEn { @override String get manuallySelected => 'Sélectionné manuellement'; @override String get shownWithForeignAudio => 'Affichés avec audio étranger'; @override String get alwaysEnabled => 'Toujours activé'; + @override String get tags => 'Tags'; + @override String get addTag => 'Ajouter un tag'; + @override String get genre => 'Genre'; + @override String get director => 'Réalisateur'; + @override String get writer => 'Scénariste'; + @override String get producer => 'Producteur'; + @override String get country => 'Pays'; + @override String get collection => 'Collection'; + @override String get label => 'Label'; + @override String get style => 'Style'; + @override String get mood => 'Ambiance'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsFr { 'metadataEdit.manuallySelected' => 'Sélectionné manuellement', 'metadataEdit.shownWithForeignAudio' => 'Affichés avec audio étranger', 'metadataEdit.alwaysEnabled' => 'Toujours activé', + 'metadataEdit.tags' => 'Tags', + 'metadataEdit.addTag' => 'Ajouter un tag', + 'metadataEdit.genre' => 'Genre', + 'metadataEdit.director' => 'Réalisateur', + 'metadataEdit.writer' => 'Scénariste', + 'metadataEdit.producer' => 'Producteur', + 'metadataEdit.country' => 'Pays', + 'metadataEdit.collection' => 'Collection', + 'metadataEdit.label' => 'Label', + 'metadataEdit.style' => 'Style', + 'metadataEdit.mood' => 'Ambiance', 'serverTasks.title' => 'Tâches du serveur', 'serverTasks.failedToLoad' => 'Échec du chargement des tâches', 'serverTasks.noTasks' => 'Aucune tâche en cours', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index ed7399cf..77da9716 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditIt implements TranslationsMetadataEditEn { @override String get manuallySelected => 'Selezionato manualmente'; @override String get shownWithForeignAudio => 'Mostrati con audio straniero'; @override String get alwaysEnabled => 'Sempre attivo'; + @override String get tags => 'Tag'; + @override String get addTag => 'Aggiungi tag'; + @override String get genre => 'Genere'; + @override String get director => 'Regista'; + @override String get writer => 'Sceneggiatore'; + @override String get producer => 'Produttore'; + @override String get country => 'Paese'; + @override String get collection => 'Collezione'; + @override String get label => 'Etichetta'; + @override String get style => 'Stile'; + @override String get mood => 'Atmosfera'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsIt { 'metadataEdit.manuallySelected' => 'Selezionato manualmente', 'metadataEdit.shownWithForeignAudio' => 'Mostrati con audio straniero', 'metadataEdit.alwaysEnabled' => 'Sempre attivo', + 'metadataEdit.tags' => 'Tag', + 'metadataEdit.addTag' => 'Aggiungi tag', + 'metadataEdit.genre' => 'Genere', + 'metadataEdit.director' => 'Regista', + 'metadataEdit.writer' => 'Sceneggiatore', + 'metadataEdit.producer' => 'Produttore', + 'metadataEdit.country' => 'Paese', + 'metadataEdit.collection' => 'Collezione', + 'metadataEdit.label' => 'Etichetta', + 'metadataEdit.style' => 'Stile', + 'metadataEdit.mood' => 'Atmosfera', 'serverTasks.title' => 'Attività del server', 'serverTasks.failedToLoad' => 'Impossibile caricare le attività', 'serverTasks.noTasks' => 'Nessuna attività in corso', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index 21d2ea35..95254e0a 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditJa implements TranslationsMetadataEditEn { @override String get manuallySelected => '手動選択'; @override String get shownWithForeignAudio => '外国語音声時に表示'; @override String get alwaysEnabled => '常に有効'; + @override String get tags => 'タグ'; + @override String get addTag => 'タグを追加'; + @override String get genre => 'ジャンル'; + @override String get director => '監督'; + @override String get writer => '脚本'; + @override String get producer => 'プロデューサー'; + @override String get country => '国'; + @override String get collection => 'コレクション'; + @override String get label => 'ラベル'; + @override String get style => 'スタイル'; + @override String get mood => 'ムード'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsJa { 'metadataEdit.manuallySelected' => '手動選択', 'metadataEdit.shownWithForeignAudio' => '外国語音声時に表示', 'metadataEdit.alwaysEnabled' => '常に有効', + 'metadataEdit.tags' => 'タグ', + 'metadataEdit.addTag' => 'タグを追加', + 'metadataEdit.genre' => 'ジャンル', + 'metadataEdit.director' => '監督', + 'metadataEdit.writer' => '脚本', + 'metadataEdit.producer' => 'プロデューサー', + 'metadataEdit.country' => '国', + 'metadataEdit.collection' => 'コレクション', + 'metadataEdit.label' => 'ラベル', + 'metadataEdit.style' => 'スタイル', + 'metadataEdit.mood' => 'ムード', 'serverTasks.title' => 'サーバータスク', 'serverTasks.failedToLoad' => 'タスクの読み込みに失敗しました', 'serverTasks.noTasks' => '実行中のタスクはありません', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index a17cba13..37282f77 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditKo implements TranslationsMetadataEditEn { @override String get manuallySelected => '수동 선택'; @override String get shownWithForeignAudio => '외국어 오디오 시 표시'; @override String get alwaysEnabled => '항상 활성화'; + @override String get tags => '태그'; + @override String get addTag => '태그 추가'; + @override String get genre => '장르'; + @override String get director => '감독'; + @override String get writer => '작가'; + @override String get producer => '프로듀서'; + @override String get country => '국가'; + @override String get collection => '컬렉션'; + @override String get label => '라벨'; + @override String get style => '스타일'; + @override String get mood => '분위기'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsKo { 'metadataEdit.manuallySelected' => '수동 선택', 'metadataEdit.shownWithForeignAudio' => '외국어 오디오 시 표시', 'metadataEdit.alwaysEnabled' => '항상 활성화', + 'metadataEdit.tags' => '태그', + 'metadataEdit.addTag' => '태그 추가', + 'metadataEdit.genre' => '장르', + 'metadataEdit.director' => '감독', + 'metadataEdit.writer' => '작가', + 'metadataEdit.producer' => '프로듀서', + 'metadataEdit.country' => '국가', + 'metadataEdit.collection' => '컬렉션', + 'metadataEdit.label' => '라벨', + 'metadataEdit.style' => '스타일', + 'metadataEdit.mood' => '분위기', 'serverTasks.title' => '서버 작업', 'serverTasks.failedToLoad' => '작업을 불러올 수 없습니다', 'serverTasks.noTasks' => '실행 중인 작업 없음', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index 4cdab733..77c2af7e 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditNb implements TranslationsMetadataEditEn { @override String get manuallySelected => 'Manuelt valgt'; @override String get shownWithForeignAudio => 'Vist med fremmedspråklig lyd'; @override String get alwaysEnabled => 'Alltid aktivert'; + @override String get tags => 'Tagger'; + @override String get addTag => 'Legg til tagg'; + @override String get genre => 'Sjanger'; + @override String get director => 'Regissør'; + @override String get writer => 'Forfatter'; + @override String get producer => 'Produsent'; + @override String get country => 'Land'; + @override String get collection => 'Samling'; + @override String get label => 'Etikett'; + @override String get style => 'Stil'; + @override String get mood => 'Stemning'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsNb { 'metadataEdit.manuallySelected' => 'Manuelt valgt', 'metadataEdit.shownWithForeignAudio' => 'Vist med fremmedspråklig lyd', 'metadataEdit.alwaysEnabled' => 'Alltid aktivert', + 'metadataEdit.tags' => 'Tagger', + 'metadataEdit.addTag' => 'Legg til tagg', + 'metadataEdit.genre' => 'Sjanger', + 'metadataEdit.director' => 'Regissør', + 'metadataEdit.writer' => 'Forfatter', + 'metadataEdit.producer' => 'Produsent', + 'metadataEdit.country' => 'Land', + 'metadataEdit.collection' => 'Samling', + 'metadataEdit.label' => 'Etikett', + 'metadataEdit.style' => 'Stil', + 'metadataEdit.mood' => 'Stemning', 'serverTasks.title' => 'Serveroppgaver', 'serverTasks.failedToLoad' => 'Kunne ikke laste oppgaver', 'serverTasks.noTasks' => 'Ingen oppgaver kjører', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index fd1aacc3..9bc44588 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditNl implements TranslationsMetadataEditEn { @override String get manuallySelected => 'Handmatig geselecteerd'; @override String get shownWithForeignAudio => 'Weergeven bij anderstalig geluid'; @override String get alwaysEnabled => 'Altijd ingeschakeld'; + @override String get tags => 'Tags'; + @override String get addTag => 'Tag toevoegen'; + @override String get genre => 'Genre'; + @override String get director => 'Regisseur'; + @override String get writer => 'Schrijver'; + @override String get producer => 'Producent'; + @override String get country => 'Land'; + @override String get collection => 'Collectie'; + @override String get label => 'Label'; + @override String get style => 'Stijl'; + @override String get mood => 'Stemming'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsNl { 'metadataEdit.manuallySelected' => 'Handmatig geselecteerd', 'metadataEdit.shownWithForeignAudio' => 'Weergeven bij anderstalig geluid', 'metadataEdit.alwaysEnabled' => 'Altijd ingeschakeld', + 'metadataEdit.tags' => 'Tags', + 'metadataEdit.addTag' => 'Tag toevoegen', + 'metadataEdit.genre' => 'Genre', + 'metadataEdit.director' => 'Regisseur', + 'metadataEdit.writer' => 'Schrijver', + 'metadataEdit.producer' => 'Producent', + 'metadataEdit.country' => 'Land', + 'metadataEdit.collection' => 'Collectie', + 'metadataEdit.label' => 'Label', + 'metadataEdit.style' => 'Stijl', + 'metadataEdit.mood' => 'Stemming', 'serverTasks.title' => 'Servertaken', 'serverTasks.failedToLoad' => 'Taken konden niet worden geladen', 'serverTasks.noTasks' => 'Geen actieve taken', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index d7b72fc4..dd936718 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditPl implements TranslationsMetadataEditEn { @override String get manuallySelected => 'Wybrany ręcznie'; @override String get shownWithForeignAudio => 'Wyświetlane przy obcojęzycznym audio'; @override String get alwaysEnabled => 'Zawsze włączone'; + @override String get tags => 'Tagi'; + @override String get addTag => 'Dodaj tag'; + @override String get genre => 'Gatunek'; + @override String get director => 'Reżyser'; + @override String get writer => 'Scenarzysta'; + @override String get producer => 'Producent'; + @override String get country => 'Kraj'; + @override String get collection => 'Kolekcja'; + @override String get label => 'Etykieta'; + @override String get style => 'Styl'; + @override String get mood => 'Nastrój'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsPl { 'metadataEdit.manuallySelected' => 'Wybrany ręcznie', 'metadataEdit.shownWithForeignAudio' => 'Wyświetlane przy obcojęzycznym audio', 'metadataEdit.alwaysEnabled' => 'Zawsze włączone', + 'metadataEdit.tags' => 'Tagi', + 'metadataEdit.addTag' => 'Dodaj tag', + 'metadataEdit.genre' => 'Gatunek', + 'metadataEdit.director' => 'Reżyser', + 'metadataEdit.writer' => 'Scenarzysta', + 'metadataEdit.producer' => 'Producent', + 'metadataEdit.country' => 'Kraj', + 'metadataEdit.collection' => 'Kolekcja', + 'metadataEdit.label' => 'Etykieta', + 'metadataEdit.style' => 'Styl', + 'metadataEdit.mood' => 'Nastrój', 'serverTasks.title' => 'Zadania serwera', 'serverTasks.failedToLoad' => 'Nie udało się załadować zadań', 'serverTasks.noTasks' => 'Brak uruchomionych zadań', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index e01bfa87..4e73104f 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditPt implements TranslationsMetadataEditEn { @override String get manuallySelected => 'Seleção manual'; @override String get shownWithForeignAudio => 'Exibir com áudio estrangeiro'; @override String get alwaysEnabled => 'Sempre ativado'; + @override String get tags => 'Tags'; + @override String get addTag => 'Adicionar tag'; + @override String get genre => 'Gênero'; + @override String get director => 'Diretor'; + @override String get writer => 'Roteirista'; + @override String get producer => 'Produtor'; + @override String get country => 'País'; + @override String get collection => 'Coleção'; + @override String get label => 'Rótulo'; + @override String get style => 'Estilo'; + @override String get mood => 'Humor'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsPt { 'metadataEdit.manuallySelected' => 'Seleção manual', 'metadataEdit.shownWithForeignAudio' => 'Exibir com áudio estrangeiro', 'metadataEdit.alwaysEnabled' => 'Sempre ativado', + 'metadataEdit.tags' => 'Tags', + 'metadataEdit.addTag' => 'Adicionar tag', + 'metadataEdit.genre' => 'Gênero', + 'metadataEdit.director' => 'Diretor', + 'metadataEdit.writer' => 'Roteirista', + 'metadataEdit.producer' => 'Produtor', + 'metadataEdit.country' => 'País', + 'metadataEdit.collection' => 'Coleção', + 'metadataEdit.label' => 'Rótulo', + 'metadataEdit.style' => 'Estilo', + 'metadataEdit.mood' => 'Humor', 'serverTasks.title' => 'Tarefas do servidor', 'serverTasks.failedToLoad' => 'Falha ao carregar tarefas', 'serverTasks.noTasks' => 'Nenhuma tarefa em execução', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index eeeb11a7..569a4bef 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditRu implements TranslationsMetadataEditEn { @override String get manuallySelected => 'Выбор вручную'; @override String get shownWithForeignAudio => 'Показывать при иноязычном аудио'; @override String get alwaysEnabled => 'Всегда включены'; + @override String get tags => 'Теги'; + @override String get addTag => 'Добавить тег'; + @override String get genre => 'Жанр'; + @override String get director => 'Режиссёр'; + @override String get writer => 'Сценарист'; + @override String get producer => 'Продюсер'; + @override String get country => 'Страна'; + @override String get collection => 'Коллекция'; + @override String get label => 'Метка'; + @override String get style => 'Стиль'; + @override String get mood => 'Настроение'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsRu { 'metadataEdit.manuallySelected' => 'Выбор вручную', 'metadataEdit.shownWithForeignAudio' => 'Показывать при иноязычном аудио', 'metadataEdit.alwaysEnabled' => 'Всегда включены', + 'metadataEdit.tags' => 'Теги', + 'metadataEdit.addTag' => 'Добавить тег', + 'metadataEdit.genre' => 'Жанр', + 'metadataEdit.director' => 'Режиссёр', + 'metadataEdit.writer' => 'Сценарист', + 'metadataEdit.producer' => 'Продюсер', + 'metadataEdit.country' => 'Страна', + 'metadataEdit.collection' => 'Коллекция', + 'metadataEdit.label' => 'Метка', + 'metadataEdit.style' => 'Стиль', + 'metadataEdit.mood' => 'Настроение', 'serverTasks.title' => 'Задачи сервера', 'serverTasks.failedToLoad' => 'Не удалось загрузить задачи', 'serverTasks.noTasks' => 'Нет выполняемых задач', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index badcb211..e8e2a728 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditSv implements TranslationsMetadataEditEn { @override String get manuallySelected => 'Manuellt vald'; @override String get shownWithForeignAudio => 'Visas vid främmande ljud'; @override String get alwaysEnabled => 'Alltid aktiverad'; + @override String get tags => 'Taggar'; + @override String get addTag => 'Lägg till tagg'; + @override String get genre => 'Genre'; + @override String get director => 'Regissör'; + @override String get writer => 'Författare'; + @override String get producer => 'Producent'; + @override String get country => 'Land'; + @override String get collection => 'Samling'; + @override String get label => 'Etikett'; + @override String get style => 'Stil'; + @override String get mood => 'Stämning'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsSv { 'metadataEdit.manuallySelected' => 'Manuellt vald', 'metadataEdit.shownWithForeignAudio' => 'Visas vid främmande ljud', 'metadataEdit.alwaysEnabled' => 'Alltid aktiverad', + 'metadataEdit.tags' => 'Taggar', + 'metadataEdit.addTag' => 'Lägg till tagg', + 'metadataEdit.genre' => 'Genre', + 'metadataEdit.director' => 'Regissör', + 'metadataEdit.writer' => 'Författare', + 'metadataEdit.producer' => 'Producent', + 'metadataEdit.country' => 'Land', + 'metadataEdit.collection' => 'Samling', + 'metadataEdit.label' => 'Etikett', + 'metadataEdit.style' => 'Stil', + 'metadataEdit.mood' => 'Stämning', 'serverTasks.title' => 'Serveruppgifter', 'serverTasks.failedToLoad' => 'Kunde inte ladda uppgifter', 'serverTasks.noTasks' => 'Inga pågående uppgifter', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index a6f582a1..6ba92cfb 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -1115,6 +1115,17 @@ class _TranslationsMetadataEditZh implements TranslationsMetadataEditEn { @override String get manuallySelected => '手动选择'; @override String get shownWithForeignAudio => '外语音频时显示'; @override String get alwaysEnabled => '始终启用'; + @override String get tags => '标签'; + @override String get addTag => '添加标签'; + @override String get genre => '类型'; + @override String get director => '导演'; + @override String get writer => '编剧'; + @override String get producer => '制片人'; + @override String get country => '国家'; + @override String get collection => '合集'; + @override String get label => '标记'; + @override String get style => '风格'; + @override String get mood => '氛围'; } // Path: serverTasks @@ -2114,6 +2125,17 @@ extension on TranslationsZh { 'metadataEdit.manuallySelected' => '手动选择', 'metadataEdit.shownWithForeignAudio' => '外语音频时显示', 'metadataEdit.alwaysEnabled' => '始终启用', + 'metadataEdit.tags' => '标签', + 'metadataEdit.addTag' => '添加标签', + 'metadataEdit.genre' => '类型', + 'metadataEdit.director' => '导演', + 'metadataEdit.writer' => '编剧', + 'metadataEdit.producer' => '制片人', + 'metadataEdit.country' => '国家', + 'metadataEdit.collection' => '合集', + 'metadataEdit.label' => '标记', + 'metadataEdit.style' => '风格', + 'metadataEdit.mood' => '氛围', 'serverTasks.title' => '服务器任务', 'serverTasks.failedToLoad' => '加载任务失败', 'serverTasks.noTasks' => '没有正在运行的任务', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index e8afab67..6e43f6cb 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "Automatiskt val av undertexter", "manuallySelected": "Manuellt vald", "shownWithForeignAudio": "Visas vid främmande ljud", - "alwaysEnabled": "Alltid aktiverad" + "alwaysEnabled": "Alltid aktiverad", + "tags": "Taggar", + "addTag": "Lägg till tagg", + "genre": "Genre", + "director": "Regissör", + "writer": "Författare", + "producer": "Producent", + "country": "Land", + "collection": "Samling", + "label": "Etikett", + "style": "Stil", + "mood": "Stämning" }, "serverTasks": { "title": "Serveruppgifter", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 01af4fa0..80b5c381 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -891,7 +891,18 @@ "subtitleMode": "自动选择字幕模式", "manuallySelected": "手动选择", "shownWithForeignAudio": "外语音频时显示", - "alwaysEnabled": "始终启用" + "alwaysEnabled": "始终启用", + "tags": "标签", + "addTag": "添加标签", + "genre": "类型", + "director": "导演", + "writer": "编剧", + "producer": "制片人", + "country": "国家", + "collection": "合集", + "label": "标记", + "style": "风格", + "mood": "氛围" }, "serverTasks": { "title": "服务器任务", diff --git a/lib/models/plex_metadata.dart b/lib/models/plex_metadata.dart index f69d0346..94ff8038 100644 --- a/lib/models/plex_metadata.dart +++ b/lib/models/plex_metadata.dart @@ -13,6 +13,9 @@ part 'plex_metadata.g.dart'; Object? _readRatingKey(Map json, String key) => json['ratingKey'] ?? json['key'] ?? ''; +List? _tagsFromJson(List? json) => + json?.cast>().map((e) => e['tag'] as String).toList(); + int? _flexibleInt(Object? v) => switch (v) { num n => n.toInt(), String s => int.tryParse(s), @@ -102,6 +105,24 @@ class PlexMetadata with MultiServerFields { final List? role; // Cast members @JsonKey(name: 'Media', includeToJson: false) final List? mediaVersions; // Available media versions/editions + @JsonKey(name: 'Genre', fromJson: _tagsFromJson, includeToJson: false) + final List? genre; + @JsonKey(name: 'Director', fromJson: _tagsFromJson, includeToJson: false) + final List? director; + @JsonKey(name: 'Writer', fromJson: _tagsFromJson, includeToJson: false) + final List? writer; + @JsonKey(name: 'Producer', fromJson: _tagsFromJson, includeToJson: false) + final List? producer; + @JsonKey(name: 'Country', fromJson: _tagsFromJson, includeToJson: false) + final List? country; + @JsonKey(name: 'Collection', fromJson: _tagsFromJson, includeToJson: false) + final List? collection; + @JsonKey(name: 'Label', fromJson: _tagsFromJson, includeToJson: false) + final List? label; + @JsonKey(name: 'Style', fromJson: _tagsFromJson, includeToJson: false) + final List? style; + @JsonKey(name: 'Mood', fromJson: _tagsFromJson, includeToJson: false) + final List? mood; final String? audioLanguage; // Per-media preferred audio language final String? subtitleLanguage; // Per-media preferred subtitle language @JsonKey(fromJson: _flexibleInt) @@ -205,6 +226,15 @@ class PlexMetadata with MultiServerFields { this.childCount, this.role, this.mediaVersions, + this.genre, + this.director, + this.writer, + this.producer, + this.country, + this.collection, + this.label, + this.style, + this.mood, this.audioLanguage, this.subtitleLanguage, this.subtitleMode, @@ -265,6 +295,15 @@ class PlexMetadata with MultiServerFields { int? childCount, List? role, List? mediaVersions, + List? genre, + List? director, + List? writer, + List? producer, + List? country, + List? collection, + List? label, + List? style, + List? mood, String? audioLanguage, String? subtitleLanguage, int? subtitleMode, @@ -323,6 +362,15 @@ class PlexMetadata with MultiServerFields { childCount: childCount ?? this.childCount, role: role ?? this.role, mediaVersions: mediaVersions ?? this.mediaVersions, + genre: genre ?? this.genre, + director: director ?? this.director, + writer: writer ?? this.writer, + producer: producer ?? this.producer, + country: country ?? this.country, + collection: collection ?? this.collection, + label: label ?? this.label, + style: style ?? this.style, + mood: mood ?? this.mood, 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 22cd4c1d..9f16a540 100644 --- a/lib/models/plex_metadata.g.dart +++ b/lib/models/plex_metadata.g.dart @@ -48,6 +48,15 @@ PlexMetadata _$PlexMetadataFromJson(Map json) => PlexMetadata( mediaVersions: (json['Media'] as List?) ?.map((e) => PlexMediaVersion.fromJson(e as Map)) .toList(), + genre: _tagsFromJson(json['Genre'] as List?), + director: _tagsFromJson(json['Director'] as List?), + writer: _tagsFromJson(json['Writer'] as List?), + producer: _tagsFromJson(json['Producer'] as List?), + country: _tagsFromJson(json['Country'] as List?), + collection: _tagsFromJson(json['Collection'] as List?), + label: _tagsFromJson(json['Label'] as List?), + style: _tagsFromJson(json['Style'] as List?), + mood: _tagsFromJson(json['Mood'] as List?), audioLanguage: json['audioLanguage'] as String?, subtitleLanguage: json['subtitleLanguage'] as String?, subtitleMode: _flexibleInt(json['subtitleMode']), diff --git a/lib/screens/metadata_edit_screen.dart b/lib/screens/metadata_edit_screen.dart index b5dfbbb4..a00db25a 100644 --- a/lib/screens/metadata_edit_screen.dart +++ b/lib/screens/metadata_edit_screen.dart @@ -13,6 +13,7 @@ import '../widgets/artwork_picker_dialog.dart'; import '../widgets/focusable_list_tile.dart'; import '../widgets/focused_scroll_scaffold.dart'; import '../widgets/plex_optimized_image.dart'; +import '../widgets/tag_edit_dialog.dart'; class MetadataEditScreen extends StatefulWidget { final PlexMetadata metadata; @@ -49,9 +50,19 @@ class _MetadataEditScreenState extends State { String? _origTagline; String? _origSummary; + // Tag field values + final Map> _tags = {}; + final Map> _origTags = {}; + // Advanced prefs (loaded from metadata JSON) final Map _currentPrefs = {}; + static bool _tagsEqual(List a, List b) => + a.length == b.length && a.every((e) => b.contains(e)); + + bool get _hasTagChanges => _tags.keys.any( + (k) => !_tagsEqual(_tags[k] ?? [], _origTags[k] ?? [])); + bool get _hasChanges => _title != _origTitle || _titleSort != _origTitleSort || @@ -60,7 +71,8 @@ class _MetadataEditScreenState extends State { _contentRating != _origContentRating || _studio != _origStudio || _tagline != _origTagline || - _summary != _origSummary; + _summary != _origSummary || + _hasTagChanges; PlexMediaType get _mediaType => widget.metadata.mediaType; @@ -117,6 +129,21 @@ class _MetadataEditScreenState extends State { _origStudio = _studio; _origTagline = _tagline; _origSummary = _summary; + + void initTag(String key, List? values) { + _tags[key] = List.of(values ?? []); + _origTags[key] = List.of(values ?? []); + } + + initTag('genre', meta.genre); + initTag('director', meta.director); + initTag('writer', meta.writer); + initTag('producer', meta.producer); + initTag('country', meta.country); + initTag('collection', meta.collection); + initTag('label', meta.label); + initTag('style', meta.style); + initTag('mood', meta.mood); } Future _save() async { @@ -130,6 +157,16 @@ class _MetadataEditScreenState extends State { setState(() => _isSaving = true); + Map current, List original})>? tagChanges; + for (final key in _tags.keys) { + final current = _tags[key] ?? []; + final original = _origTags[key] ?? []; + if (!_tagsEqual(current, original)) { + tagChanges ??= {}; + tagChanges[key] = (current: current, original: original); + } + } + final success = await _client.updateMetadata( sectionId: sectionId, ratingKey: widget.metadata.ratingKey, @@ -142,6 +179,7 @@ class _MetadataEditScreenState extends State { studio: _studio != _origStudio ? _studio : null, tagline: _tagline != _origTagline ? _tagline : null, summary: _summary != _origSummary ? _summary : null, + tagChanges: tagChanges, ); if (!mounted) return; @@ -331,6 +369,57 @@ class _MetadataEditScreenState extends State { _mediaType == PlexMediaType.movie || _mediaType == PlexMediaType.show || _mediaType == PlexMediaType.episode; bool get _showAdvanced => _mediaType != PlexMediaType.episode; + List<({String key, String label})> get _tagFields { + switch (_mediaType) { + case PlexMediaType.movie: + case PlexMediaType.show: + return [ + (key: 'genre', label: t.metadataEdit.genre), + (key: 'director', label: t.metadataEdit.director), + (key: 'writer', label: t.metadataEdit.writer), + (key: 'producer', label: t.metadataEdit.producer), + (key: 'country', label: t.metadataEdit.country), + (key: 'collection', label: t.metadataEdit.collection), + (key: 'label', label: t.metadataEdit.label), + ]; + case PlexMediaType.episode: + return [ + (key: 'director', label: t.metadataEdit.director), + (key: 'writer', label: t.metadataEdit.writer), + ]; + case PlexMediaType.artist: + return [ + (key: 'genre', label: t.metadataEdit.genre), + (key: 'style', label: t.metadataEdit.style), + (key: 'mood', label: t.metadataEdit.mood), + (key: 'country', label: t.metadataEdit.country), + (key: 'collection', label: t.metadataEdit.collection), + ]; + case PlexMediaType.album: + return [ + (key: 'genre', label: t.metadataEdit.genre), + (key: 'style', label: t.metadataEdit.style), + (key: 'mood', label: t.metadataEdit.mood), + (key: 'collection', label: t.metadataEdit.collection), + ]; + default: + return []; + } + } + + Future _editTag(String key, String label) async { + final result = await showDialog>( + context: context, + builder: (context) => TagEditDialog( + title: label, + initialTags: _tags[key] ?? [], + ), + ); + if (result != null && mounted) { + setState(() => _tags[key] = result); + } + } + @override Widget build(BuildContext context) { if (_isLoading) { @@ -360,6 +449,10 @@ class _MetadataEditScreenState extends State { sliver: SliverList( delegate: SliverChildListDelegate([ _buildBasicInfoCard(), + if (_tagFields.isNotEmpty) ...[ + const SizedBox(height: 16), + _buildTagsCard(), + ], const SizedBox(height: 16), _buildArtworkCard(), if (_showAdvanced) ...[ @@ -489,6 +582,30 @@ class _MetadataEditScreenState extends State { ); } + Widget _buildTagsCard() { + final fields = _tagFields; + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + t.metadataEdit.tags, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + for (final field in fields) + _buildFieldTile( + label: field.label, + value: (_tags[field.key] ?? []).isEmpty ? null : (_tags[field.key]!).join(', '), + onTap: () => _editTag(field.key, field.label), + ), + ], + ), + ); + } + Widget _buildArtworkCard() { final meta = _fullMetadata ?? widget.metadata; diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 69e87e83..8ce7450c 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -1677,6 +1677,7 @@ class PlexClient { String? studio, String? tagline, String? summary, + Map current, List original})>? tagChanges, }) { final queryParams = {'type': typeNumber, 'id': ratingKey}; @@ -1696,6 +1697,23 @@ class PlexClient { addField('tagline', tagline); addField('summary', summary); + if (tagChanges != null) { + for (final entry in tagChanges.entries) { + final field = entry.key; + final current = entry.value.current; + final original = entry.value.original; + for (var i = 0; i < current.length; i++) { + queryParams['$field[$i].tag.tag'] = current[i]; + } + final removed = original.where((t) => !current.contains(t)).toList(); + if (removed.isNotEmpty) { + queryParams['$field[].tag.tag-'] = + removed.map(Uri.encodeComponent).join(','); + } + queryParams['$field.locked'] = '1'; + } + } + return _wrapBoolApiCall( () => _dio.put('/library/sections/$sectionId/all', queryParameters: queryParams), 'Failed to update metadata', diff --git a/lib/widgets/tag_edit_dialog.dart b/lib/widgets/tag_edit_dialog.dart new file mode 100644 index 00000000..7b917012 --- /dev/null +++ b/lib/widgets/tag_edit_dialog.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import '../focus/dpad_navigator.dart'; +import '../focus/focusable_button.dart'; +import '../i18n/strings.g.dart'; +import '../widgets/app_icon.dart'; +import '../widgets/focusable_list_tile.dart'; + +class TagEditDialog extends StatefulWidget { + final String title; + final List initialTags; + + const TagEditDialog({super.key, required this.title, required this.initialTags}); + + @override + State createState() => _TagEditDialogState(); +} + +class _TagEditDialogState extends State { + late final TextEditingController _controller; + late final FocusNode _textFieldFocusNode; + late final List _tags; + final _saveFocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + _controller = TextEditingController(); + _textFieldFocusNode = FocusNode( + onKeyEvent: (node, event) { + if (!event.isActionable) return KeyEventResult.ignored; + if (event.logicalKey.isDownKey) { + node.nextFocus(); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + ); + _tags = List.of(widget.initialTags); + } + + @override + void dispose() { + _controller.dispose(); + _textFieldFocusNode.dispose(); + _saveFocusNode.dispose(); + super.dispose(); + } + + void _addTag() { + final text = _controller.text.trim(); + if (text.isEmpty || _tags.contains(text)) return; + setState(() { + _tags.add(text); + _controller.clear(); + }); + _textFieldFocusNode.requestFocus(); + } + + void _removeTag(int index) { + setState(() => _tags.removeAt(index)); + _textFieldFocusNode.requestFocus(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(widget.title), + content: SizedBox( + width: 400, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: _controller, + focusNode: _textFieldFocusNode, + autofocus: true, + decoration: InputDecoration( + labelText: t.metadataEdit.addTag, + suffixIcon: FocusableButton( + onPressed: _addTag, + child: IconButton( + icon: const AppIcon(Symbols.add_rounded), + onPressed: _addTag, + ), + ), + ), + textInputAction: TextInputAction.done, + onSubmitted: (_) => _addTag(), + ), + if (_tags.isNotEmpty) ...[ + const SizedBox(height: 12), + ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: ListView.builder( + shrinkWrap: true, + itemCount: _tags.length, + itemBuilder: (context, index) => FocusableListTile( + title: Text(_tags[index]), + trailing: const AppIcon(Symbols.close_rounded), + onTap: () => _removeTag(index), + ), + ), + ), + ], + ], + ), + ), + actions: [ + FocusableButton( + onPressed: () => Navigator.pop(context), + child: TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), + ), + FocusableButton( + focusNode: _saveFocusNode, + onPressed: () => Navigator.pop(context, _tags), + child: TextButton( + onPressed: () => Navigator.pop(context, _tags), + child: Text(t.common.save), + ), + ), + ], + ); + } +}