From 4eb55885f7139e272db734b7d937f390b47f2d77 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Sun, 22 Feb 2026 17:56:13 +0100 Subject: [PATCH] feat: metadata editing close #338 --- lib/i18n/de.i18n.json | 63 ++- lib/i18n/en.i18n.json | 61 +++ lib/i18n/es.i18n.json | 63 ++- lib/i18n/fr.i18n.json | 63 ++- lib/i18n/it.i18n.json | 63 ++- lib/i18n/ko.i18n.json | 63 ++- lib/i18n/nl.i18n.json | 63 ++- lib/i18n/strings.g.dart | 4 +- lib/i18n/strings_de.g.dart | 128 +++++ lib/i18n/strings_en.g.dart | 246 +++++++++ lib/i18n/strings_es.g.dart | 128 +++++ lib/i18n/strings_fr.g.dart | 128 +++++ lib/i18n/strings_it.g.dart | 128 +++++ lib/i18n/strings_ko.g.dart | 128 +++++ lib/i18n/strings_nl.g.dart | 128 +++++ lib/i18n/strings_sv.g.dart | 128 +++++ lib/i18n/strings_zh.g.dart | 128 +++++ lib/i18n/sv.i18n.json | 63 ++- lib/i18n/zh.i18n.json | 63 ++- lib/models/plex_metadata.dart | 20 + lib/models/plex_metadata.g.dart | 4 + lib/screens/media_detail_screen.dart | 20 + lib/screens/metadata_edit_screen.dart | 717 +++++++++++++++++++++++++ lib/services/plex_client.dart | 88 +++ lib/utils/dialogs.dart | 75 +++ lib/utils/language_codes.dart | 32 ++ lib/widgets/artwork_picker_dialog.dart | 226 ++++++++ lib/widgets/media_context_menu.dart | 22 + 28 files changed, 3033 insertions(+), 10 deletions(-) create mode 100644 lib/screens/metadata_edit_screen.dart create mode 100644 lib/widgets/artwork_picker_dialog.dart diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index c0f15a36..93d7a148 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -1,7 +1,7 @@ { "app": { "title": "Plezy" -}, + }, "auth": { "signInWithPlex": "Mit Plex anmelden", "showQRCode": "QR-Code anzeigen", @@ -831,5 +831,66 @@ "launchFailed": "Externer Player konnte nicht geöffnet werden", "appNotInstalled": "${name} ist nicht installiert", "playInExternalPlayer": "In externem Player abspielen" + }, + "metadataEdit": { + "editMetadata": "Bearbeiten...", + "screenTitle": "Metadaten bearbeiten", + "basicInfo": "Grundlegende Infos", + "artwork": "Artwork", + "advancedSettings": "Erweiterte Einstellungen", + "title": "Titel", + "sortTitle": "Sortiertitel", + "originalTitle": "Originaltitel", + "releaseDate": "Erscheinungsdatum", + "contentRating": "Altersfreigabe", + "studio": "Studio", + "tagline": "Tagline", + "summary": "Zusammenfassung", + "poster": "Poster", + "background": "Hintergrund", + "selectPoster": "Poster auswählen", + "selectBackground": "Hintergrund auswählen", + "fromUrl": "Von URL", + "uploadFile": "Datei hochladen", + "enterImageUrl": "Bild-URL eingeben", + "imageUrl": "Bild-URL", + "metadataUpdated": "Metadaten aktualisiert", + "metadataUpdateFailed": "Metadaten konnten nicht aktualisiert werden", + "artworkUpdated": "Artwork aktualisiert", + "artworkUpdateFailed": "Artwork konnte nicht aktualisiert werden", + "noArtworkAvailable": "Kein Artwork verfügbar", + "notSet": "Nicht festgelegt", + "libraryDefault": "Bibliotheksstandard", + "accountDefault": "Kontostandard", + "seriesDefault": "Serienstandard", + "episodeSorting": "Episodensortierung", + "oldestFirst": "Älteste zuerst", + "newestFirst": "Neueste zuerst", + "keep": "Behalten", + "allEpisodes": "Alle Episoden", + "latestEpisodes": "${count} neueste Episoden", + "latestEpisode": "Neueste Episode", + "episodesAddedPastDays": "Episoden der letzten ${count} Tage", + "deleteAfterPlaying": "Episoden nach Wiedergabe löschen", + "never": "Nie", + "afterADay": "Nach einem Tag", + "afterAWeek": "Nach einer Woche", + "afterAMonth": "Nach einem Monat", + "onNextRefresh": "Bei nächster Aktualisierung", + "seasons": "Staffeln", + "show": "Anzeigen", + "hide": "Ausblenden", + "episodeOrdering": "Episodenreihenfolge", + "tmdbAiring": "The Movie Database (Ausgestrahlt)", + "tvdbAiring": "TheTVDB (Ausgestrahlt)", + "tvdbAbsolute": "TheTVDB (Absolut)", + "metadataLanguage": "Metadaten-Sprache", + "useOriginalTitle": "Originaltitel verwenden", + "preferredAudioLanguage": "Bevorzugte Audiosprache", + "preferredSubtitleLanguage": "Bevorzugte Untertitelsprache", + "subtitleMode": "Automatische Untertitelauswahl", + "manuallySelected": "Manuell ausgewählt", + "shownWithForeignAudio": "Bei fremdsprachigem Audio anzeigen", + "alwaysEnabled": "Immer aktiviert" } } diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index 7a47f554..4210513c 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -831,5 +831,66 @@ "launchFailed": "Failed to open external player", "appNotInstalled": "${name} is not installed", "playInExternalPlayer": "Play in External Player" + }, + "metadataEdit": { + "editMetadata": "Edit...", + "screenTitle": "Edit Metadata", + "basicInfo": "Basic Info", + "artwork": "Artwork", + "advancedSettings": "Advanced Settings", + "title": "Title", + "sortTitle": "Sort Title", + "originalTitle": "Original Title", + "releaseDate": "Release Date", + "contentRating": "Content Rating", + "studio": "Studio", + "tagline": "Tagline", + "summary": "Summary", + "poster": "Poster", + "background": "Background", + "selectPoster": "Select Poster", + "selectBackground": "Select Background", + "fromUrl": "From URL", + "uploadFile": "Upload File", + "enterImageUrl": "Enter image URL", + "imageUrl": "Image URL", + "metadataUpdated": "Metadata updated", + "metadataUpdateFailed": "Failed to update metadata", + "artworkUpdated": "Artwork updated", + "artworkUpdateFailed": "Failed to update artwork", + "noArtworkAvailable": "No artwork available", + "notSet": "Not set", + "libraryDefault": "Library default", + "accountDefault": "Account default", + "seriesDefault": "Series default", + "episodeSorting": "Episode Sorting", + "oldestFirst": "Oldest first", + "newestFirst": "Newest first", + "keep": "Keep", + "allEpisodes": "All episodes", + "latestEpisodes": "${count} latest episodes", + "latestEpisode": "Latest episode", + "episodesAddedPastDays": "Episodes added in the past ${count} days", + "deleteAfterPlaying": "Delete Episodes After Playing", + "never": "Never", + "afterADay": "After a day", + "afterAWeek": "After a week", + "afterAMonth": "After a month", + "onNextRefresh": "On next refresh", + "seasons": "Seasons", + "show": "Show", + "hide": "Hide", + "episodeOrdering": "Episode Ordering", + "tmdbAiring": "The Movie Database (Aired)", + "tvdbAiring": "TheTVDB (Aired)", + "tvdbAbsolute": "TheTVDB (Absolute)", + "metadataLanguage": "Metadata Language", + "useOriginalTitle": "Use Original Title", + "preferredAudioLanguage": "Preferred Audio Language", + "preferredSubtitleLanguage": "Preferred Subtitle Language", + "subtitleMode": "Auto-Select Subtitle Mode", + "manuallySelected": "Manually selected", + "shownWithForeignAudio": "Shown with foreign audio", + "alwaysEnabled": "Always enabled" } } diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index 63438767..25f3a4b9 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -1,7 +1,7 @@ { "app": { "title": "Plezy" -}, + }, "auth": { "signInWithPlex": "Inicia sesión con Plex", "showQRCode": "Mostrar código QR", @@ -831,5 +831,66 @@ "launchFailed": "No se pudo abrir el reproductor externo", "appNotInstalled": "${name} no está instalado", "playInExternalPlayer": "Reproducir en reproductor externo" + }, + "metadataEdit": { + "editMetadata": "Editar...", + "screenTitle": "Editar metadatos", + "basicInfo": "Información básica", + "artwork": "Artwork", + "advancedSettings": "Ajustes avanzados", + "title": "Título", + "sortTitle": "Título de ordenación", + "originalTitle": "Título original", + "releaseDate": "Fecha de estreno", + "contentRating": "Clasificación de contenido", + "studio": "Estudio", + "tagline": "Eslogan", + "summary": "Resumen", + "poster": "Póster", + "background": "Fondo", + "selectPoster": "Seleccionar póster", + "selectBackground": "Seleccionar fondo", + "fromUrl": "Desde URL", + "uploadFile": "Subir archivo", + "enterImageUrl": "Introducir URL de imagen", + "imageUrl": "URL de imagen", + "metadataUpdated": "Metadatos actualizados", + "metadataUpdateFailed": "Error al actualizar los metadatos", + "artworkUpdated": "Artwork actualizado", + "artworkUpdateFailed": "Error al actualizar el artwork", + "noArtworkAvailable": "No hay artwork disponible", + "notSet": "No establecido", + "libraryDefault": "Predeterminado de biblioteca", + "accountDefault": "Predeterminado de cuenta", + "seriesDefault": "Predeterminado de serie", + "episodeSorting": "Orden de episodios", + "oldestFirst": "Más antiguos primero", + "newestFirst": "Más recientes primero", + "keep": "Conservar", + "allEpisodes": "Todos los episodios", + "latestEpisodes": "${count} episodios más recientes", + "latestEpisode": "Episodio más reciente", + "episodesAddedPastDays": "Episodios añadidos en los últimos ${count} días", + "deleteAfterPlaying": "Eliminar episodios después de reproducir", + "never": "Nunca", + "afterADay": "Después de un día", + "afterAWeek": "Después de una semana", + "afterAMonth": "Después de un mes", + "onNextRefresh": "En la próxima actualización", + "seasons": "Temporadas", + "show": "Mostrar", + "hide": "Ocultar", + "episodeOrdering": "Orden de episodios", + "tmdbAiring": "The Movie Database (Emisión)", + "tvdbAiring": "TheTVDB (Emisión)", + "tvdbAbsolute": "TheTVDB (Absoluto)", + "metadataLanguage": "Idioma de metadatos", + "useOriginalTitle": "Usar título original", + "preferredAudioLanguage": "Idioma de audio preferido", + "preferredSubtitleLanguage": "Idioma de subtítulos preferido", + "subtitleMode": "Selección automática de subtítulos", + "manuallySelected": "Seleccionado manualmente", + "shownWithForeignAudio": "Mostrar con audio extranjero", + "alwaysEnabled": "Siempre activado" } } diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index c6201cab..afad6935 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -1,7 +1,7 @@ { "app": { "title": "Plezy" -}, + }, "auth": { "signInWithPlex": "S'inscrire avec Plex", "showQRCode": "Afficher le QR Code", @@ -831,5 +831,66 @@ "launchFailed": "Impossible d'ouvrir le lecteur externe", "appNotInstalled": "${name} n'est pas installé", "playInExternalPlayer": "Lire dans un lecteur externe" + }, + "metadataEdit": { + "editMetadata": "Modifier...", + "screenTitle": "Modifier les métadonnées", + "basicInfo": "Informations de base", + "artwork": "Artwork", + "advancedSettings": "Paramètres avancés", + "title": "Titre", + "sortTitle": "Titre de tri", + "originalTitle": "Titre original", + "releaseDate": "Date de sortie", + "contentRating": "Classification", + "studio": "Studio", + "tagline": "Slogan", + "summary": "Résumé", + "poster": "Affiche", + "background": "Arrière-plan", + "selectPoster": "Sélectionner l'affiche", + "selectBackground": "Sélectionner l'arrière-plan", + "fromUrl": "Depuis une URL", + "uploadFile": "Importer un fichier", + "enterImageUrl": "Entrer l'URL de l'image", + "imageUrl": "URL de l'image", + "metadataUpdated": "Métadonnées mises à jour", + "metadataUpdateFailed": "Échec de la mise à jour des métadonnées", + "artworkUpdated": "Artwork mis à jour", + "artworkUpdateFailed": "Échec de la mise à jour de l'artwork", + "noArtworkAvailable": "Aucun artwork disponible", + "notSet": "Non défini", + "libraryDefault": "Par défaut de la bibliothèque", + "accountDefault": "Par défaut du compte", + "seriesDefault": "Par défaut de la série", + "episodeSorting": "Tri des épisodes", + "oldestFirst": "Plus anciens en premier", + "newestFirst": "Plus récents en premier", + "keep": "Conserver", + "allEpisodes": "Tous les épisodes", + "latestEpisodes": "${count} derniers épisodes", + "latestEpisode": "Dernier épisode", + "episodesAddedPastDays": "Épisodes ajoutés ces ${count} derniers jours", + "deleteAfterPlaying": "Supprimer les épisodes après lecture", + "never": "Jamais", + "afterADay": "Après un jour", + "afterAWeek": "Après une semaine", + "afterAMonth": "Après un mois", + "onNextRefresh": "Au prochain rafraîchissement", + "seasons": "Saisons", + "show": "Afficher", + "hide": "Masquer", + "episodeOrdering": "Ordre des épisodes", + "tmdbAiring": "The Movie Database (Diffusion)", + "tvdbAiring": "TheTVDB (Diffusion)", + "tvdbAbsolute": "TheTVDB (Absolu)", + "metadataLanguage": "Langue des métadonnées", + "useOriginalTitle": "Utiliser le titre original", + "preferredAudioLanguage": "Langue audio préférée", + "preferredSubtitleLanguage": "Langue de sous-titres préférée", + "subtitleMode": "Sélection automatique des sous-titres", + "manuallySelected": "Sélectionné manuellement", + "shownWithForeignAudio": "Affichés avec audio étranger", + "alwaysEnabled": "Toujours activé" } } diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index 7322b034..ae151fbd 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -1,7 +1,7 @@ { "app": { "title": "Plezy" -}, + }, "auth": { "signInWithPlex": "Accedi con Plex", "showQRCode": "Mostra QR Code", @@ -831,5 +831,66 @@ "launchFailed": "Impossibile aprire il lettore esterno", "appNotInstalled": "${name} non è installato", "playInExternalPlayer": "Riproduci in lettore esterno" + }, + "metadataEdit": { + "editMetadata": "Modifica...", + "screenTitle": "Modifica metadati", + "basicInfo": "Informazioni di base", + "artwork": "Artwork", + "advancedSettings": "Impostazioni avanzate", + "title": "Titolo", + "sortTitle": "Titolo di ordinamento", + "originalTitle": "Titolo originale", + "releaseDate": "Data di uscita", + "contentRating": "Classificazione", + "studio": "Studio", + "tagline": "Tagline", + "summary": "Trama", + "poster": "Poster", + "background": "Sfondo", + "selectPoster": "Seleziona poster", + "selectBackground": "Seleziona sfondo", + "fromUrl": "Da URL", + "uploadFile": "Carica file", + "enterImageUrl": "Inserisci URL immagine", + "imageUrl": "URL immagine", + "metadataUpdated": "Metadati aggiornati", + "metadataUpdateFailed": "Aggiornamento metadati non riuscito", + "artworkUpdated": "Artwork aggiornato", + "artworkUpdateFailed": "Aggiornamento artwork non riuscito", + "noArtworkAvailable": "Nessun artwork disponibile", + "notSet": "Non impostato", + "libraryDefault": "Predefinito libreria", + "accountDefault": "Predefinito account", + "seriesDefault": "Predefinito serie", + "episodeSorting": "Ordinamento episodi", + "oldestFirst": "Più vecchi prima", + "newestFirst": "Più recenti prima", + "keep": "Conserva", + "allEpisodes": "Tutti gli episodi", + "latestEpisodes": "${count} episodi più recenti", + "latestEpisode": "Episodio più recente", + "episodesAddedPastDays": "Episodi aggiunti negli ultimi ${count} giorni", + "deleteAfterPlaying": "Elimina episodi dopo la riproduzione", + "never": "Mai", + "afterADay": "Dopo un giorno", + "afterAWeek": "Dopo una settimana", + "afterAMonth": "Dopo un mese", + "onNextRefresh": "Al prossimo aggiornamento", + "seasons": "Stagioni", + "show": "Mostra", + "hide": "Nascondi", + "episodeOrdering": "Ordine episodi", + "tmdbAiring": "The Movie Database (In onda)", + "tvdbAiring": "TheTVDB (In onda)", + "tvdbAbsolute": "TheTVDB (Assoluto)", + "metadataLanguage": "Lingua metadati", + "useOriginalTitle": "Usa titolo originale", + "preferredAudioLanguage": "Lingua audio preferita", + "preferredSubtitleLanguage": "Lingua sottotitoli preferita", + "subtitleMode": "Selezione automatica sottotitoli", + "manuallySelected": "Selezionato manualmente", + "shownWithForeignAudio": "Mostrati con audio straniero", + "alwaysEnabled": "Sempre attivo" } } diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index b3d6d170..856cd475 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -1,7 +1,7 @@ { "app": { "title": "Plezy" -}, + }, "auth": { "signInWithPlex": "Plex 계정으로 로그인", "showQRCode": "QR 코드", @@ -831,5 +831,66 @@ "launchFailed": "외부 플레이어를 열 수 없습니다", "appNotInstalled": "${name}이(가) 설치되어 있지 않습니다", "playInExternalPlayer": "외부 플레이어에서 재생" + }, + "metadataEdit": { + "editMetadata": "편집...", + "screenTitle": "메타데이터 편집", + "basicInfo": "기본 정보", + "artwork": "아트워크", + "advancedSettings": "고급 설정", + "title": "제목", + "sortTitle": "정렬 제목", + "originalTitle": "원제", + "releaseDate": "출시일", + "contentRating": "콘텐츠 등급", + "studio": "스튜디오", + "tagline": "태그라인", + "summary": "줄거리", + "poster": "포스터", + "background": "배경", + "selectPoster": "포스터 선택", + "selectBackground": "배경 선택", + "fromUrl": "URL에서", + "uploadFile": "파일 업로드", + "enterImageUrl": "이미지 URL 입력", + "imageUrl": "이미지 URL", + "metadataUpdated": "메타데이터가 업데이트되었습니다", + "metadataUpdateFailed": "메타데이터 업데이트 실패", + "artworkUpdated": "아트워크가 업데이트되었습니다", + "artworkUpdateFailed": "아트워크 업데이트 실패", + "noArtworkAvailable": "사용 가능한 아트워크 없음", + "notSet": "설정되지 않음", + "libraryDefault": "라이브러리 기본값", + "accountDefault": "계정 기본값", + "seriesDefault": "시리즈 기본값", + "episodeSorting": "에피소드 정렬", + "oldestFirst": "오래된 순", + "newestFirst": "최신 순", + "keep": "유지", + "allEpisodes": "모든 에피소드", + "latestEpisodes": "최신 에피소드 ${count}개", + "latestEpisode": "최신 에피소드", + "episodesAddedPastDays": "최근 ${count}일 내 추가된 에피소드", + "deleteAfterPlaying": "재생 후 에피소드 삭제", + "never": "안 함", + "afterADay": "하루 후", + "afterAWeek": "일주일 후", + "afterAMonth": "한 달 후", + "onNextRefresh": "다음 새로고침 시", + "seasons": "시즌", + "show": "표시", + "hide": "숨기기", + "episodeOrdering": "에피소드 순서", + "tmdbAiring": "The Movie Database (방영순)", + "tvdbAiring": "TheTVDB (방영순)", + "tvdbAbsolute": "TheTVDB (절대순)", + "metadataLanguage": "메타데이터 언어", + "useOriginalTitle": "원제 사용", + "preferredAudioLanguage": "선호 오디오 언어", + "preferredSubtitleLanguage": "선호 자막 언어", + "subtitleMode": "자막 자동 선택 모드", + "manuallySelected": "수동 선택", + "shownWithForeignAudio": "외국어 오디오 시 표시", + "alwaysEnabled": "항상 활성화" } } diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 978c927c..cc446181 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -1,7 +1,7 @@ { "app": { "title": "Plezy" -}, + }, "auth": { "signInWithPlex": "Inloggen met Plex", "showQRCode": "Toon QR-code", @@ -831,5 +831,66 @@ "launchFailed": "Kan externe speler niet openen", "appNotInstalled": "${name} is niet geïnstalleerd", "playInExternalPlayer": "Afspelen in externe speler" + }, + "metadataEdit": { + "editMetadata": "Bewerken...", + "screenTitle": "Metadata bewerken", + "basicInfo": "Basisinformatie", + "artwork": "Artwork", + "advancedSettings": "Geavanceerde instellingen", + "title": "Titel", + "sortTitle": "Sorteertitel", + "originalTitle": "Oorspronkelijke titel", + "releaseDate": "Releasedatum", + "contentRating": "Leeftijdsclassificatie", + "studio": "Studio", + "tagline": "Tagline", + "summary": "Samenvatting", + "poster": "Poster", + "background": "Achtergrond", + "selectPoster": "Poster selecteren", + "selectBackground": "Achtergrond selecteren", + "fromUrl": "Vanaf URL", + "uploadFile": "Bestand uploaden", + "enterImageUrl": "Voer afbeeldings-URL in", + "imageUrl": "Afbeeldings-URL", + "metadataUpdated": "Metadata bijgewerkt", + "metadataUpdateFailed": "Metadata bijwerken mislukt", + "artworkUpdated": "Artwork bijgewerkt", + "artworkUpdateFailed": "Artwork bijwerken mislukt", + "noArtworkAvailable": "Geen artwork beschikbaar", + "notSet": "Niet ingesteld", + "libraryDefault": "Bibliotheekstandaard", + "accountDefault": "Accountstandaard", + "seriesDefault": "Seriestandaard", + "episodeSorting": "Afleveringen sorteren", + "oldestFirst": "Oudste eerst", + "newestFirst": "Nieuwste eerst", + "keep": "Bewaren", + "allEpisodes": "Alle afleveringen", + "latestEpisodes": "${count} nieuwste afleveringen", + "latestEpisode": "Nieuwste aflevering", + "episodesAddedPastDays": "Afleveringen toegevoegd in de afgelopen ${count} dagen", + "deleteAfterPlaying": "Afleveringen verwijderen na afspelen", + "never": "Nooit", + "afterADay": "Na een dag", + "afterAWeek": "Na een week", + "afterAMonth": "Na een maand", + "onNextRefresh": "Bij volgende verversing", + "seasons": "Seizoenen", + "show": "Tonen", + "hide": "Verbergen", + "episodeOrdering": "Afleveringsvolgorde", + "tmdbAiring": "The Movie Database (Uitgezonden)", + "tvdbAiring": "TheTVDB (Uitgezonden)", + "tvdbAbsolute": "TheTVDB (Absoluut)", + "metadataLanguage": "Metadatataal", + "useOriginalTitle": "Oorspronkelijke titel gebruiken", + "preferredAudioLanguage": "Voorkeurstaal audio", + "preferredSubtitleLanguage": "Voorkeurstaal ondertiteling", + "subtitleMode": "Automatische ondertitelselectie", + "manuallySelected": "Handmatig geselecteerd", + "shownWithForeignAudio": "Weergeven bij anderstalig geluid", + "alwaysEnabled": "Altijd ingeschakeld" } } diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index e4b899ec..d1dcb9d2 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 9 -/// Strings: 6705 (745 per locale) +/// Strings: 7236 (804 per locale) /// -/// Built on 2026-02-20 at 23:08 UTC +/// Built on 2026-02-22 at 16:11 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 992b4993..b6b44128 100644 --- a/lib/i18n/strings_de.g.dart +++ b/lib/i18n/strings_de.g.dart @@ -74,6 +74,7 @@ class TranslationsDe with BaseTranslations implements T @override late final _TranslationsCompanionRemoteDe companionRemote = _TranslationsCompanionRemoteDe._(_root); @override late final _TranslationsVideoSettingsDe videoSettings = _TranslationsVideoSettingsDe._(_root); @override late final _TranslationsExternalPlayerDe externalPlayer = _TranslationsExternalPlayerDe._(_root); + @override late final _TranslationsMetadataEditDe metadataEdit = _TranslationsMetadataEditDe._(_root); } // Path: app @@ -1050,6 +1051,74 @@ class _TranslationsExternalPlayerDe implements TranslationsExternalPlayerEn { @override String get playInExternalPlayer => 'In externem Player abspielen'; } +// Path: metadataEdit +class _TranslationsMetadataEditDe implements TranslationsMetadataEditEn { + _TranslationsMetadataEditDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get editMetadata => 'Bearbeiten...'; + @override String get screenTitle => 'Metadaten bearbeiten'; + @override String get basicInfo => 'Grundlegende Infos'; + @override String get artwork => 'Artwork'; + @override String get advancedSettings => 'Erweiterte Einstellungen'; + @override String get title => 'Titel'; + @override String get sortTitle => 'Sortiertitel'; + @override String get originalTitle => 'Originaltitel'; + @override String get releaseDate => 'Erscheinungsdatum'; + @override String get contentRating => 'Altersfreigabe'; + @override String get studio => 'Studio'; + @override String get tagline => 'Tagline'; + @override String get summary => 'Zusammenfassung'; + @override String get poster => 'Poster'; + @override String get background => 'Hintergrund'; + @override String get selectPoster => 'Poster auswählen'; + @override String get selectBackground => 'Hintergrund auswählen'; + @override String get fromUrl => 'Von URL'; + @override String get uploadFile => 'Datei hochladen'; + @override String get enterImageUrl => 'Bild-URL eingeben'; + @override String get imageUrl => 'Bild-URL'; + @override String get metadataUpdated => 'Metadaten aktualisiert'; + @override String get metadataUpdateFailed => 'Metadaten konnten nicht aktualisiert werden'; + @override String get artworkUpdated => 'Artwork aktualisiert'; + @override String get artworkUpdateFailed => 'Artwork konnte nicht aktualisiert werden'; + @override String get noArtworkAvailable => 'Kein Artwork verfügbar'; + @override String get notSet => 'Nicht festgelegt'; + @override String get libraryDefault => 'Bibliotheksstandard'; + @override String get accountDefault => 'Kontostandard'; + @override String get seriesDefault => 'Serienstandard'; + @override String get episodeSorting => 'Episodensortierung'; + @override String get oldestFirst => 'Älteste zuerst'; + @override String get newestFirst => 'Neueste zuerst'; + @override String get keep => 'Behalten'; + @override String get allEpisodes => 'Alle Episoden'; + @override String latestEpisodes({required Object count}) => '${count} neueste Episoden'; + @override String get latestEpisode => 'Neueste Episode'; + @override String episodesAddedPastDays({required Object count}) => 'Episoden der letzten ${count} Tage'; + @override String get deleteAfterPlaying => 'Episoden nach Wiedergabe löschen'; + @override String get never => 'Nie'; + @override String get afterADay => 'Nach einem Tag'; + @override String get afterAWeek => 'Nach einer Woche'; + @override String get afterAMonth => 'Nach einem Monat'; + @override String get onNextRefresh => 'Bei nächster Aktualisierung'; + @override String get seasons => 'Staffeln'; + @override String get show => 'Anzeigen'; + @override String get hide => 'Ausblenden'; + @override String get episodeOrdering => 'Episodenreihenfolge'; + @override String get tmdbAiring => 'The Movie Database (Ausgestrahlt)'; + @override String get tvdbAiring => 'TheTVDB (Ausgestrahlt)'; + @override String get tvdbAbsolute => 'TheTVDB (Absolut)'; + @override String get metadataLanguage => 'Metadaten-Sprache'; + @override String get useOriginalTitle => 'Originaltitel verwenden'; + @override String get preferredAudioLanguage => 'Bevorzugte Audiosprache'; + @override String get preferredSubtitleLanguage => 'Bevorzugte Untertitelsprache'; + @override String get subtitleMode => 'Automatische Untertitelauswahl'; + @override String get manuallySelected => 'Manuell ausgewählt'; + @override String get shownWithForeignAudio => 'Bei fremdsprachigem Audio anzeigen'; + @override String get alwaysEnabled => 'Immer aktiviert'; +} + // Path: hotkeys.actions class _TranslationsHotkeysActionsDe implements TranslationsHotkeysActionsEn { _TranslationsHotkeysActionsDe._(this._root); @@ -1979,6 +2048,65 @@ extension on TranslationsDe { 'externalPlayer.launchFailed' => 'Externer Player konnte nicht geöffnet werden', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} ist nicht installiert', 'externalPlayer.playInExternalPlayer' => 'In externem Player abspielen', + 'metadataEdit.editMetadata' => 'Bearbeiten...', + 'metadataEdit.screenTitle' => 'Metadaten bearbeiten', + 'metadataEdit.basicInfo' => 'Grundlegende Infos', + 'metadataEdit.artwork' => 'Artwork', + 'metadataEdit.advancedSettings' => 'Erweiterte Einstellungen', + 'metadataEdit.title' => 'Titel', + 'metadataEdit.sortTitle' => 'Sortiertitel', + 'metadataEdit.originalTitle' => 'Originaltitel', + 'metadataEdit.releaseDate' => 'Erscheinungsdatum', + 'metadataEdit.contentRating' => 'Altersfreigabe', + 'metadataEdit.studio' => 'Studio', + 'metadataEdit.tagline' => 'Tagline', + 'metadataEdit.summary' => 'Zusammenfassung', + 'metadataEdit.poster' => 'Poster', + 'metadataEdit.background' => 'Hintergrund', + 'metadataEdit.selectPoster' => 'Poster auswählen', + 'metadataEdit.selectBackground' => 'Hintergrund auswählen', + 'metadataEdit.fromUrl' => 'Von URL', + 'metadataEdit.uploadFile' => 'Datei hochladen', + 'metadataEdit.enterImageUrl' => 'Bild-URL eingeben', + 'metadataEdit.imageUrl' => 'Bild-URL', + 'metadataEdit.metadataUpdated' => 'Metadaten aktualisiert', + 'metadataEdit.metadataUpdateFailed' => 'Metadaten konnten nicht aktualisiert werden', + 'metadataEdit.artworkUpdated' => 'Artwork aktualisiert', + 'metadataEdit.artworkUpdateFailed' => 'Artwork konnte nicht aktualisiert werden', + 'metadataEdit.noArtworkAvailable' => 'Kein Artwork verfügbar', + 'metadataEdit.notSet' => 'Nicht festgelegt', + 'metadataEdit.libraryDefault' => 'Bibliotheksstandard', + 'metadataEdit.accountDefault' => 'Kontostandard', + 'metadataEdit.seriesDefault' => 'Serienstandard', + 'metadataEdit.episodeSorting' => 'Episodensortierung', + 'metadataEdit.oldestFirst' => 'Älteste zuerst', + 'metadataEdit.newestFirst' => 'Neueste zuerst', + 'metadataEdit.keep' => 'Behalten', + 'metadataEdit.allEpisodes' => 'Alle Episoden', + 'metadataEdit.latestEpisodes' => ({required Object count}) => '${count} neueste Episoden', + 'metadataEdit.latestEpisode' => 'Neueste Episode', + 'metadataEdit.episodesAddedPastDays' => ({required Object count}) => 'Episoden der letzten ${count} Tage', + 'metadataEdit.deleteAfterPlaying' => 'Episoden nach Wiedergabe löschen', + 'metadataEdit.never' => 'Nie', + 'metadataEdit.afterADay' => 'Nach einem Tag', + 'metadataEdit.afterAWeek' => 'Nach einer Woche', + 'metadataEdit.afterAMonth' => 'Nach einem Monat', + 'metadataEdit.onNextRefresh' => 'Bei nächster Aktualisierung', + 'metadataEdit.seasons' => 'Staffeln', + 'metadataEdit.show' => 'Anzeigen', + 'metadataEdit.hide' => 'Ausblenden', + 'metadataEdit.episodeOrdering' => 'Episodenreihenfolge', + 'metadataEdit.tmdbAiring' => 'The Movie Database (Ausgestrahlt)', + 'metadataEdit.tvdbAiring' => 'TheTVDB (Ausgestrahlt)', + 'metadataEdit.tvdbAbsolute' => 'TheTVDB (Absolut)', + 'metadataEdit.metadataLanguage' => 'Metadaten-Sprache', + 'metadataEdit.useOriginalTitle' => 'Originaltitel verwenden', + 'metadataEdit.preferredAudioLanguage' => 'Bevorzugte Audiosprache', + 'metadataEdit.preferredSubtitleLanguage' => 'Bevorzugte Untertitelsprache', + 'metadataEdit.subtitleMode' => 'Automatische Untertitelauswahl', + 'metadataEdit.manuallySelected' => 'Manuell ausgewählt', + 'metadataEdit.shownWithForeignAudio' => 'Bei fremdsprachigem Audio anzeigen', + 'metadataEdit.alwaysEnabled' => 'Immer aktiviert', _ => null, }; } diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index e0b4e381..d6ca04bc 100644 --- a/lib/i18n/strings_en.g.dart +++ b/lib/i18n/strings_en.g.dart @@ -77,6 +77,7 @@ class Translations with BaseTranslations { late final TranslationsCompanionRemoteEn companionRemote = TranslationsCompanionRemoteEn._(_root); late final TranslationsVideoSettingsEn videoSettings = TranslationsVideoSettingsEn._(_root); late final TranslationsExternalPlayerEn externalPlayer = TranslationsExternalPlayerEn._(_root); + late final TranslationsMetadataEditEn metadataEdit = TranslationsMetadataEditEn._(_root); } // Path: app @@ -2325,6 +2326,192 @@ class TranslationsExternalPlayerEn { String get playInExternalPlayer => 'Play in External Player'; } +// Path: metadataEdit +class TranslationsMetadataEditEn { + TranslationsMetadataEditEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Edit...' + String get editMetadata => 'Edit...'; + + /// en: 'Edit Metadata' + String get screenTitle => 'Edit Metadata'; + + /// en: 'Basic Info' + String get basicInfo => 'Basic Info'; + + /// en: 'Artwork' + String get artwork => 'Artwork'; + + /// en: 'Advanced Settings' + String get advancedSettings => 'Advanced Settings'; + + /// en: 'Title' + String get title => 'Title'; + + /// en: 'Sort Title' + String get sortTitle => 'Sort Title'; + + /// en: 'Original Title' + String get originalTitle => 'Original Title'; + + /// en: 'Release Date' + String get releaseDate => 'Release Date'; + + /// en: 'Content Rating' + String get contentRating => 'Content Rating'; + + /// en: 'Studio' + String get studio => 'Studio'; + + /// en: 'Tagline' + String get tagline => 'Tagline'; + + /// en: 'Summary' + String get summary => 'Summary'; + + /// en: 'Poster' + String get poster => 'Poster'; + + /// en: 'Background' + String get background => 'Background'; + + /// en: 'Select Poster' + String get selectPoster => 'Select Poster'; + + /// en: 'Select Background' + String get selectBackground => 'Select Background'; + + /// en: 'From URL' + String get fromUrl => 'From URL'; + + /// en: 'Upload File' + String get uploadFile => 'Upload File'; + + /// en: 'Enter image URL' + String get enterImageUrl => 'Enter image URL'; + + /// en: 'Image URL' + String get imageUrl => 'Image URL'; + + /// en: 'Metadata updated' + String get metadataUpdated => 'Metadata updated'; + + /// en: 'Failed to update metadata' + String get metadataUpdateFailed => 'Failed to update metadata'; + + /// en: 'Artwork updated' + String get artworkUpdated => 'Artwork updated'; + + /// en: 'Failed to update artwork' + String get artworkUpdateFailed => 'Failed to update artwork'; + + /// en: 'No artwork available' + String get noArtworkAvailable => 'No artwork available'; + + /// en: 'Not set' + String get notSet => 'Not set'; + + /// en: 'Library default' + String get libraryDefault => 'Library default'; + + /// en: 'Account default' + String get accountDefault => 'Account default'; + + /// en: 'Series default' + String get seriesDefault => 'Series default'; + + /// en: 'Episode Sorting' + String get episodeSorting => 'Episode Sorting'; + + /// en: 'Oldest first' + String get oldestFirst => 'Oldest first'; + + /// en: 'Newest first' + String get newestFirst => 'Newest first'; + + /// en: 'Keep' + String get keep => 'Keep'; + + /// en: 'All episodes' + String get allEpisodes => 'All episodes'; + + /// en: '${count} latest episodes' + String latestEpisodes({required Object count}) => '${count} latest episodes'; + + /// en: 'Latest episode' + String get latestEpisode => 'Latest episode'; + + /// en: 'Episodes added in the past ${count} days' + String episodesAddedPastDays({required Object count}) => 'Episodes added in the past ${count} days'; + + /// en: 'Delete Episodes After Playing' + String get deleteAfterPlaying => 'Delete Episodes After Playing'; + + /// en: 'Never' + String get never => 'Never'; + + /// en: 'After a day' + String get afterADay => 'After a day'; + + /// en: 'After a week' + String get afterAWeek => 'After a week'; + + /// en: 'After a month' + String get afterAMonth => 'After a month'; + + /// en: 'On next refresh' + String get onNextRefresh => 'On next refresh'; + + /// en: 'Seasons' + String get seasons => 'Seasons'; + + /// en: 'Show' + String get show => 'Show'; + + /// en: 'Hide' + String get hide => 'Hide'; + + /// en: 'Episode Ordering' + String get episodeOrdering => 'Episode Ordering'; + + /// en: 'The Movie Database (Aired)' + String get tmdbAiring => 'The Movie Database (Aired)'; + + /// en: 'TheTVDB (Aired)' + String get tvdbAiring => 'TheTVDB (Aired)'; + + /// en: 'TheTVDB (Absolute)' + String get tvdbAbsolute => 'TheTVDB (Absolute)'; + + /// en: 'Metadata Language' + String get metadataLanguage => 'Metadata Language'; + + /// en: 'Use Original Title' + String get useOriginalTitle => 'Use Original Title'; + + /// en: 'Preferred Audio Language' + String get preferredAudioLanguage => 'Preferred Audio Language'; + + /// en: 'Preferred Subtitle Language' + String get preferredSubtitleLanguage => 'Preferred Subtitle Language'; + + /// en: 'Auto-Select Subtitle Mode' + String get subtitleMode => 'Auto-Select Subtitle Mode'; + + /// en: 'Manually selected' + String get manuallySelected => 'Manually selected'; + + /// en: 'Shown with foreign audio' + String get shownWithForeignAudio => 'Shown with foreign audio'; + + /// en: 'Always enabled' + String get alwaysEnabled => 'Always enabled'; +} + // Path: hotkeys.actions class TranslationsHotkeysActionsEn { TranslationsHotkeysActionsEn._(this._root); @@ -3476,6 +3663,65 @@ extension on Translations { 'externalPlayer.launchFailed' => 'Failed to open external player', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is not installed', 'externalPlayer.playInExternalPlayer' => 'Play in External Player', + 'metadataEdit.editMetadata' => 'Edit...', + 'metadataEdit.screenTitle' => 'Edit Metadata', + 'metadataEdit.basicInfo' => 'Basic Info', + 'metadataEdit.artwork' => 'Artwork', + 'metadataEdit.advancedSettings' => 'Advanced Settings', + 'metadataEdit.title' => 'Title', + 'metadataEdit.sortTitle' => 'Sort Title', + 'metadataEdit.originalTitle' => 'Original Title', + 'metadataEdit.releaseDate' => 'Release Date', + 'metadataEdit.contentRating' => 'Content Rating', + 'metadataEdit.studio' => 'Studio', + 'metadataEdit.tagline' => 'Tagline', + 'metadataEdit.summary' => 'Summary', + 'metadataEdit.poster' => 'Poster', + 'metadataEdit.background' => 'Background', + 'metadataEdit.selectPoster' => 'Select Poster', + 'metadataEdit.selectBackground' => 'Select Background', + 'metadataEdit.fromUrl' => 'From URL', + 'metadataEdit.uploadFile' => 'Upload File', + 'metadataEdit.enterImageUrl' => 'Enter image URL', + 'metadataEdit.imageUrl' => 'Image URL', + 'metadataEdit.metadataUpdated' => 'Metadata updated', + 'metadataEdit.metadataUpdateFailed' => 'Failed to update metadata', + 'metadataEdit.artworkUpdated' => 'Artwork updated', + 'metadataEdit.artworkUpdateFailed' => 'Failed to update artwork', + 'metadataEdit.noArtworkAvailable' => 'No artwork available', + 'metadataEdit.notSet' => 'Not set', + 'metadataEdit.libraryDefault' => 'Library default', + 'metadataEdit.accountDefault' => 'Account default', + 'metadataEdit.seriesDefault' => 'Series default', + 'metadataEdit.episodeSorting' => 'Episode Sorting', + 'metadataEdit.oldestFirst' => 'Oldest first', + 'metadataEdit.newestFirst' => 'Newest first', + 'metadataEdit.keep' => 'Keep', + 'metadataEdit.allEpisodes' => 'All episodes', + 'metadataEdit.latestEpisodes' => ({required Object count}) => '${count} latest episodes', + 'metadataEdit.latestEpisode' => 'Latest episode', + 'metadataEdit.episodesAddedPastDays' => ({required Object count}) => 'Episodes added in the past ${count} days', + 'metadataEdit.deleteAfterPlaying' => 'Delete Episodes After Playing', + 'metadataEdit.never' => 'Never', + 'metadataEdit.afterADay' => 'After a day', + 'metadataEdit.afterAWeek' => 'After a week', + 'metadataEdit.afterAMonth' => 'After a month', + 'metadataEdit.onNextRefresh' => 'On next refresh', + 'metadataEdit.seasons' => 'Seasons', + 'metadataEdit.show' => 'Show', + 'metadataEdit.hide' => 'Hide', + 'metadataEdit.episodeOrdering' => 'Episode Ordering', + 'metadataEdit.tmdbAiring' => 'The Movie Database (Aired)', + 'metadataEdit.tvdbAiring' => 'TheTVDB (Aired)', + 'metadataEdit.tvdbAbsolute' => 'TheTVDB (Absolute)', + 'metadataEdit.metadataLanguage' => 'Metadata Language', + 'metadataEdit.useOriginalTitle' => 'Use Original Title', + 'metadataEdit.preferredAudioLanguage' => 'Preferred Audio Language', + 'metadataEdit.preferredSubtitleLanguage' => 'Preferred Subtitle Language', + 'metadataEdit.subtitleMode' => 'Auto-Select Subtitle Mode', + 'metadataEdit.manuallySelected' => 'Manually selected', + 'metadataEdit.shownWithForeignAudio' => 'Shown with foreign audio', + 'metadataEdit.alwaysEnabled' => 'Always enabled', _ => null, }; } diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 38a79b93..9a95b944 100644 --- a/lib/i18n/strings_es.g.dart +++ b/lib/i18n/strings_es.g.dart @@ -74,6 +74,7 @@ class TranslationsEs with BaseTranslations implements T @override late final _TranslationsCompanionRemoteEs companionRemote = _TranslationsCompanionRemoteEs._(_root); @override late final _TranslationsVideoSettingsEs videoSettings = _TranslationsVideoSettingsEs._(_root); @override late final _TranslationsExternalPlayerEs externalPlayer = _TranslationsExternalPlayerEs._(_root); + @override late final _TranslationsMetadataEditEs metadataEdit = _TranslationsMetadataEditEs._(_root); } // Path: app @@ -1050,6 +1051,74 @@ class _TranslationsExternalPlayerEs implements TranslationsExternalPlayerEn { @override String get playInExternalPlayer => 'Reproducir en reproductor externo'; } +// Path: metadataEdit +class _TranslationsMetadataEditEs implements TranslationsMetadataEditEn { + _TranslationsMetadataEditEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get editMetadata => 'Editar...'; + @override String get screenTitle => 'Editar metadatos'; + @override String get basicInfo => 'Información básica'; + @override String get artwork => 'Artwork'; + @override String get advancedSettings => 'Ajustes avanzados'; + @override String get title => 'Título'; + @override String get sortTitle => 'Título de ordenación'; + @override String get originalTitle => 'Título original'; + @override String get releaseDate => 'Fecha de estreno'; + @override String get contentRating => 'Clasificación de contenido'; + @override String get studio => 'Estudio'; + @override String get tagline => 'Eslogan'; + @override String get summary => 'Resumen'; + @override String get poster => 'Póster'; + @override String get background => 'Fondo'; + @override String get selectPoster => 'Seleccionar póster'; + @override String get selectBackground => 'Seleccionar fondo'; + @override String get fromUrl => 'Desde URL'; + @override String get uploadFile => 'Subir archivo'; + @override String get enterImageUrl => 'Introducir URL de imagen'; + @override String get imageUrl => 'URL de imagen'; + @override String get metadataUpdated => 'Metadatos actualizados'; + @override String get metadataUpdateFailed => 'Error al actualizar los metadatos'; + @override String get artworkUpdated => 'Artwork actualizado'; + @override String get artworkUpdateFailed => 'Error al actualizar el artwork'; + @override String get noArtworkAvailable => 'No hay artwork disponible'; + @override String get notSet => 'No establecido'; + @override String get libraryDefault => 'Predeterminado de biblioteca'; + @override String get accountDefault => 'Predeterminado de cuenta'; + @override String get seriesDefault => 'Predeterminado de serie'; + @override String get episodeSorting => 'Orden de episodios'; + @override String get oldestFirst => 'Más antiguos primero'; + @override String get newestFirst => 'Más recientes primero'; + @override String get keep => 'Conservar'; + @override String get allEpisodes => 'Todos los episodios'; + @override String latestEpisodes({required Object count}) => '${count} episodios más recientes'; + @override String get latestEpisode => 'Episodio más reciente'; + @override String episodesAddedPastDays({required Object count}) => 'Episodios añadidos en los últimos ${count} días'; + @override String get deleteAfterPlaying => 'Eliminar episodios después de reproducir'; + @override String get never => 'Nunca'; + @override String get afterADay => 'Después de un día'; + @override String get afterAWeek => 'Después de una semana'; + @override String get afterAMonth => 'Después de un mes'; + @override String get onNextRefresh => 'En la próxima actualización'; + @override String get seasons => 'Temporadas'; + @override String get show => 'Mostrar'; + @override String get hide => 'Ocultar'; + @override String get episodeOrdering => 'Orden de episodios'; + @override String get tmdbAiring => 'The Movie Database (Emisión)'; + @override String get tvdbAiring => 'TheTVDB (Emisión)'; + @override String get tvdbAbsolute => 'TheTVDB (Absoluto)'; + @override String get metadataLanguage => 'Idioma de metadatos'; + @override String get useOriginalTitle => 'Usar título original'; + @override String get preferredAudioLanguage => 'Idioma de audio preferido'; + @override String get preferredSubtitleLanguage => 'Idioma de subtítulos preferido'; + @override String get subtitleMode => 'Selección automática de subtítulos'; + @override String get manuallySelected => 'Seleccionado manualmente'; + @override String get shownWithForeignAudio => 'Mostrar con audio extranjero'; + @override String get alwaysEnabled => 'Siempre activado'; +} + // Path: hotkeys.actions class _TranslationsHotkeysActionsEs implements TranslationsHotkeysActionsEn { _TranslationsHotkeysActionsEs._(this._root); @@ -1979,6 +2048,65 @@ extension on TranslationsEs { 'externalPlayer.launchFailed' => 'No se pudo abrir el reproductor externo', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} no está instalado', 'externalPlayer.playInExternalPlayer' => 'Reproducir en reproductor externo', + 'metadataEdit.editMetadata' => 'Editar...', + 'metadataEdit.screenTitle' => 'Editar metadatos', + 'metadataEdit.basicInfo' => 'Información básica', + 'metadataEdit.artwork' => 'Artwork', + 'metadataEdit.advancedSettings' => 'Ajustes avanzados', + 'metadataEdit.title' => 'Título', + 'metadataEdit.sortTitle' => 'Título de ordenación', + 'metadataEdit.originalTitle' => 'Título original', + 'metadataEdit.releaseDate' => 'Fecha de estreno', + 'metadataEdit.contentRating' => 'Clasificación de contenido', + 'metadataEdit.studio' => 'Estudio', + 'metadataEdit.tagline' => 'Eslogan', + 'metadataEdit.summary' => 'Resumen', + 'metadataEdit.poster' => 'Póster', + 'metadataEdit.background' => 'Fondo', + 'metadataEdit.selectPoster' => 'Seleccionar póster', + 'metadataEdit.selectBackground' => 'Seleccionar fondo', + 'metadataEdit.fromUrl' => 'Desde URL', + 'metadataEdit.uploadFile' => 'Subir archivo', + 'metadataEdit.enterImageUrl' => 'Introducir URL de imagen', + 'metadataEdit.imageUrl' => 'URL de imagen', + 'metadataEdit.metadataUpdated' => 'Metadatos actualizados', + 'metadataEdit.metadataUpdateFailed' => 'Error al actualizar los metadatos', + 'metadataEdit.artworkUpdated' => 'Artwork actualizado', + 'metadataEdit.artworkUpdateFailed' => 'Error al actualizar el artwork', + 'metadataEdit.noArtworkAvailable' => 'No hay artwork disponible', + 'metadataEdit.notSet' => 'No establecido', + 'metadataEdit.libraryDefault' => 'Predeterminado de biblioteca', + 'metadataEdit.accountDefault' => 'Predeterminado de cuenta', + 'metadataEdit.seriesDefault' => 'Predeterminado de serie', + 'metadataEdit.episodeSorting' => 'Orden de episodios', + 'metadataEdit.oldestFirst' => 'Más antiguos primero', + 'metadataEdit.newestFirst' => 'Más recientes primero', + 'metadataEdit.keep' => 'Conservar', + 'metadataEdit.allEpisodes' => 'Todos los episodios', + 'metadataEdit.latestEpisodes' => ({required Object count}) => '${count} episodios más recientes', + 'metadataEdit.latestEpisode' => 'Episodio más reciente', + 'metadataEdit.episodesAddedPastDays' => ({required Object count}) => 'Episodios añadidos en los últimos ${count} días', + 'metadataEdit.deleteAfterPlaying' => 'Eliminar episodios después de reproducir', + 'metadataEdit.never' => 'Nunca', + 'metadataEdit.afterADay' => 'Después de un día', + 'metadataEdit.afterAWeek' => 'Después de una semana', + 'metadataEdit.afterAMonth' => 'Después de un mes', + 'metadataEdit.onNextRefresh' => 'En la próxima actualización', + 'metadataEdit.seasons' => 'Temporadas', + 'metadataEdit.show' => 'Mostrar', + 'metadataEdit.hide' => 'Ocultar', + 'metadataEdit.episodeOrdering' => 'Orden de episodios', + 'metadataEdit.tmdbAiring' => 'The Movie Database (Emisión)', + 'metadataEdit.tvdbAiring' => 'TheTVDB (Emisión)', + 'metadataEdit.tvdbAbsolute' => 'TheTVDB (Absoluto)', + 'metadataEdit.metadataLanguage' => 'Idioma de metadatos', + 'metadataEdit.useOriginalTitle' => 'Usar título original', + 'metadataEdit.preferredAudioLanguage' => 'Idioma de audio preferido', + 'metadataEdit.preferredSubtitleLanguage' => 'Idioma de subtítulos preferido', + 'metadataEdit.subtitleMode' => 'Selección automática de subtítulos', + 'metadataEdit.manuallySelected' => 'Seleccionado manualmente', + 'metadataEdit.shownWithForeignAudio' => 'Mostrar con audio extranjero', + 'metadataEdit.alwaysEnabled' => 'Siempre activado', _ => null, }; } diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index 961f0688..363a18cd 100644 --- a/lib/i18n/strings_fr.g.dart +++ b/lib/i18n/strings_fr.g.dart @@ -74,6 +74,7 @@ class TranslationsFr with BaseTranslations implements T @override late final _TranslationsCompanionRemoteFr companionRemote = _TranslationsCompanionRemoteFr._(_root); @override late final _TranslationsVideoSettingsFr videoSettings = _TranslationsVideoSettingsFr._(_root); @override late final _TranslationsExternalPlayerFr externalPlayer = _TranslationsExternalPlayerFr._(_root); + @override late final _TranslationsMetadataEditFr metadataEdit = _TranslationsMetadataEditFr._(_root); } // Path: app @@ -1050,6 +1051,74 @@ class _TranslationsExternalPlayerFr implements TranslationsExternalPlayerEn { @override String get playInExternalPlayer => 'Lire dans un lecteur externe'; } +// Path: metadataEdit +class _TranslationsMetadataEditFr implements TranslationsMetadataEditEn { + _TranslationsMetadataEditFr._(this._root); + + final TranslationsFr _root; // ignore: unused_field + + // Translations + @override String get editMetadata => 'Modifier...'; + @override String get screenTitle => 'Modifier les métadonnées'; + @override String get basicInfo => 'Informations de base'; + @override String get artwork => 'Artwork'; + @override String get advancedSettings => 'Paramètres avancés'; + @override String get title => 'Titre'; + @override String get sortTitle => 'Titre de tri'; + @override String get originalTitle => 'Titre original'; + @override String get releaseDate => 'Date de sortie'; + @override String get contentRating => 'Classification'; + @override String get studio => 'Studio'; + @override String get tagline => 'Slogan'; + @override String get summary => 'Résumé'; + @override String get poster => 'Affiche'; + @override String get background => 'Arrière-plan'; + @override String get selectPoster => 'Sélectionner l\'affiche'; + @override String get selectBackground => 'Sélectionner l\'arrière-plan'; + @override String get fromUrl => 'Depuis une URL'; + @override String get uploadFile => 'Importer un fichier'; + @override String get enterImageUrl => 'Entrer l\'URL de l\'image'; + @override String get imageUrl => 'URL de l\'image'; + @override String get metadataUpdated => 'Métadonnées mises à jour'; + @override String get metadataUpdateFailed => 'Échec de la mise à jour des métadonnées'; + @override String get artworkUpdated => 'Artwork mis à jour'; + @override String get artworkUpdateFailed => 'Échec de la mise à jour de l\'artwork'; + @override String get noArtworkAvailable => 'Aucun artwork disponible'; + @override String get notSet => 'Non défini'; + @override String get libraryDefault => 'Par défaut de la bibliothèque'; + @override String get accountDefault => 'Par défaut du compte'; + @override String get seriesDefault => 'Par défaut de la série'; + @override String get episodeSorting => 'Tri des épisodes'; + @override String get oldestFirst => 'Plus anciens en premier'; + @override String get newestFirst => 'Plus récents en premier'; + @override String get keep => 'Conserver'; + @override String get allEpisodes => 'Tous les épisodes'; + @override String latestEpisodes({required Object count}) => '${count} derniers épisodes'; + @override String get latestEpisode => 'Dernier épisode'; + @override String episodesAddedPastDays({required Object count}) => 'Épisodes ajoutés ces ${count} derniers jours'; + @override String get deleteAfterPlaying => 'Supprimer les épisodes après lecture'; + @override String get never => 'Jamais'; + @override String get afterADay => 'Après un jour'; + @override String get afterAWeek => 'Après une semaine'; + @override String get afterAMonth => 'Après un mois'; + @override String get onNextRefresh => 'Au prochain rafraîchissement'; + @override String get seasons => 'Saisons'; + @override String get show => 'Afficher'; + @override String get hide => 'Masquer'; + @override String get episodeOrdering => 'Ordre des épisodes'; + @override String get tmdbAiring => 'The Movie Database (Diffusion)'; + @override String get tvdbAiring => 'TheTVDB (Diffusion)'; + @override String get tvdbAbsolute => 'TheTVDB (Absolu)'; + @override String get metadataLanguage => 'Langue des métadonnées'; + @override String get useOriginalTitle => 'Utiliser le titre original'; + @override String get preferredAudioLanguage => 'Langue audio préférée'; + @override String get preferredSubtitleLanguage => 'Langue de sous-titres préférée'; + @override String get subtitleMode => 'Sélection automatique des sous-titres'; + @override String get manuallySelected => 'Sélectionné manuellement'; + @override String get shownWithForeignAudio => 'Affichés avec audio étranger'; + @override String get alwaysEnabled => 'Toujours activé'; +} + // Path: hotkeys.actions class _TranslationsHotkeysActionsFr implements TranslationsHotkeysActionsEn { _TranslationsHotkeysActionsFr._(this._root); @@ -1979,6 +2048,65 @@ extension on TranslationsFr { 'externalPlayer.launchFailed' => 'Impossible d\'ouvrir le lecteur externe', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} n\'est pas installé', 'externalPlayer.playInExternalPlayer' => 'Lire dans un lecteur externe', + 'metadataEdit.editMetadata' => 'Modifier...', + 'metadataEdit.screenTitle' => 'Modifier les métadonnées', + 'metadataEdit.basicInfo' => 'Informations de base', + 'metadataEdit.artwork' => 'Artwork', + 'metadataEdit.advancedSettings' => 'Paramètres avancés', + 'metadataEdit.title' => 'Titre', + 'metadataEdit.sortTitle' => 'Titre de tri', + 'metadataEdit.originalTitle' => 'Titre original', + 'metadataEdit.releaseDate' => 'Date de sortie', + 'metadataEdit.contentRating' => 'Classification', + 'metadataEdit.studio' => 'Studio', + 'metadataEdit.tagline' => 'Slogan', + 'metadataEdit.summary' => 'Résumé', + 'metadataEdit.poster' => 'Affiche', + 'metadataEdit.background' => 'Arrière-plan', + 'metadataEdit.selectPoster' => 'Sélectionner l\'affiche', + 'metadataEdit.selectBackground' => 'Sélectionner l\'arrière-plan', + 'metadataEdit.fromUrl' => 'Depuis une URL', + 'metadataEdit.uploadFile' => 'Importer un fichier', + 'metadataEdit.enterImageUrl' => 'Entrer l\'URL de l\'image', + 'metadataEdit.imageUrl' => 'URL de l\'image', + 'metadataEdit.metadataUpdated' => 'Métadonnées mises à jour', + 'metadataEdit.metadataUpdateFailed' => 'Échec de la mise à jour des métadonnées', + 'metadataEdit.artworkUpdated' => 'Artwork mis à jour', + 'metadataEdit.artworkUpdateFailed' => 'Échec de la mise à jour de l\'artwork', + 'metadataEdit.noArtworkAvailable' => 'Aucun artwork disponible', + 'metadataEdit.notSet' => 'Non défini', + 'metadataEdit.libraryDefault' => 'Par défaut de la bibliothèque', + 'metadataEdit.accountDefault' => 'Par défaut du compte', + 'metadataEdit.seriesDefault' => 'Par défaut de la série', + 'metadataEdit.episodeSorting' => 'Tri des épisodes', + 'metadataEdit.oldestFirst' => 'Plus anciens en premier', + 'metadataEdit.newestFirst' => 'Plus récents en premier', + 'metadataEdit.keep' => 'Conserver', + 'metadataEdit.allEpisodes' => 'Tous les épisodes', + 'metadataEdit.latestEpisodes' => ({required Object count}) => '${count} derniers épisodes', + 'metadataEdit.latestEpisode' => 'Dernier épisode', + 'metadataEdit.episodesAddedPastDays' => ({required Object count}) => 'Épisodes ajoutés ces ${count} derniers jours', + 'metadataEdit.deleteAfterPlaying' => 'Supprimer les épisodes après lecture', + 'metadataEdit.never' => 'Jamais', + 'metadataEdit.afterADay' => 'Après un jour', + 'metadataEdit.afterAWeek' => 'Après une semaine', + 'metadataEdit.afterAMonth' => 'Après un mois', + 'metadataEdit.onNextRefresh' => 'Au prochain rafraîchissement', + 'metadataEdit.seasons' => 'Saisons', + 'metadataEdit.show' => 'Afficher', + 'metadataEdit.hide' => 'Masquer', + 'metadataEdit.episodeOrdering' => 'Ordre des épisodes', + 'metadataEdit.tmdbAiring' => 'The Movie Database (Diffusion)', + 'metadataEdit.tvdbAiring' => 'TheTVDB (Diffusion)', + 'metadataEdit.tvdbAbsolute' => 'TheTVDB (Absolu)', + 'metadataEdit.metadataLanguage' => 'Langue des métadonnées', + 'metadataEdit.useOriginalTitle' => 'Utiliser le titre original', + 'metadataEdit.preferredAudioLanguage' => 'Langue audio préférée', + 'metadataEdit.preferredSubtitleLanguage' => 'Langue de sous-titres préférée', + 'metadataEdit.subtitleMode' => 'Sélection automatique des sous-titres', + 'metadataEdit.manuallySelected' => 'Sélectionné manuellement', + 'metadataEdit.shownWithForeignAudio' => 'Affichés avec audio étranger', + 'metadataEdit.alwaysEnabled' => 'Toujours activé', _ => null, }; } diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index f97e14d0..3f5022c3 100644 --- a/lib/i18n/strings_it.g.dart +++ b/lib/i18n/strings_it.g.dart @@ -74,6 +74,7 @@ class TranslationsIt with BaseTranslations implements T @override late final _TranslationsCompanionRemoteIt companionRemote = _TranslationsCompanionRemoteIt._(_root); @override late final _TranslationsVideoSettingsIt videoSettings = _TranslationsVideoSettingsIt._(_root); @override late final _TranslationsExternalPlayerIt externalPlayer = _TranslationsExternalPlayerIt._(_root); + @override late final _TranslationsMetadataEditIt metadataEdit = _TranslationsMetadataEditIt._(_root); } // Path: app @@ -1050,6 +1051,74 @@ class _TranslationsExternalPlayerIt implements TranslationsExternalPlayerEn { @override String get playInExternalPlayer => 'Riproduci in lettore esterno'; } +// Path: metadataEdit +class _TranslationsMetadataEditIt implements TranslationsMetadataEditEn { + _TranslationsMetadataEditIt._(this._root); + + final TranslationsIt _root; // ignore: unused_field + + // Translations + @override String get editMetadata => 'Modifica...'; + @override String get screenTitle => 'Modifica metadati'; + @override String get basicInfo => 'Informazioni di base'; + @override String get artwork => 'Artwork'; + @override String get advancedSettings => 'Impostazioni avanzate'; + @override String get title => 'Titolo'; + @override String get sortTitle => 'Titolo di ordinamento'; + @override String get originalTitle => 'Titolo originale'; + @override String get releaseDate => 'Data di uscita'; + @override String get contentRating => 'Classificazione'; + @override String get studio => 'Studio'; + @override String get tagline => 'Tagline'; + @override String get summary => 'Trama'; + @override String get poster => 'Poster'; + @override String get background => 'Sfondo'; + @override String get selectPoster => 'Seleziona poster'; + @override String get selectBackground => 'Seleziona sfondo'; + @override String get fromUrl => 'Da URL'; + @override String get uploadFile => 'Carica file'; + @override String get enterImageUrl => 'Inserisci URL immagine'; + @override String get imageUrl => 'URL immagine'; + @override String get metadataUpdated => 'Metadati aggiornati'; + @override String get metadataUpdateFailed => 'Aggiornamento metadati non riuscito'; + @override String get artworkUpdated => 'Artwork aggiornato'; + @override String get artworkUpdateFailed => 'Aggiornamento artwork non riuscito'; + @override String get noArtworkAvailable => 'Nessun artwork disponibile'; + @override String get notSet => 'Non impostato'; + @override String get libraryDefault => 'Predefinito libreria'; + @override String get accountDefault => 'Predefinito account'; + @override String get seriesDefault => 'Predefinito serie'; + @override String get episodeSorting => 'Ordinamento episodi'; + @override String get oldestFirst => 'Più vecchi prima'; + @override String get newestFirst => 'Più recenti prima'; + @override String get keep => 'Conserva'; + @override String get allEpisodes => 'Tutti gli episodi'; + @override String latestEpisodes({required Object count}) => '${count} episodi più recenti'; + @override String get latestEpisode => 'Episodio più recente'; + @override String episodesAddedPastDays({required Object count}) => 'Episodi aggiunti negli ultimi ${count} giorni'; + @override String get deleteAfterPlaying => 'Elimina episodi dopo la riproduzione'; + @override String get never => 'Mai'; + @override String get afterADay => 'Dopo un giorno'; + @override String get afterAWeek => 'Dopo una settimana'; + @override String get afterAMonth => 'Dopo un mese'; + @override String get onNextRefresh => 'Al prossimo aggiornamento'; + @override String get seasons => 'Stagioni'; + @override String get show => 'Mostra'; + @override String get hide => 'Nascondi'; + @override String get episodeOrdering => 'Ordine episodi'; + @override String get tmdbAiring => 'The Movie Database (In onda)'; + @override String get tvdbAiring => 'TheTVDB (In onda)'; + @override String get tvdbAbsolute => 'TheTVDB (Assoluto)'; + @override String get metadataLanguage => 'Lingua metadati'; + @override String get useOriginalTitle => 'Usa titolo originale'; + @override String get preferredAudioLanguage => 'Lingua audio preferita'; + @override String get preferredSubtitleLanguage => 'Lingua sottotitoli preferita'; + @override String get subtitleMode => 'Selezione automatica sottotitoli'; + @override String get manuallySelected => 'Selezionato manualmente'; + @override String get shownWithForeignAudio => 'Mostrati con audio straniero'; + @override String get alwaysEnabled => 'Sempre attivo'; +} + // Path: hotkeys.actions class _TranslationsHotkeysActionsIt implements TranslationsHotkeysActionsEn { _TranslationsHotkeysActionsIt._(this._root); @@ -1979,6 +2048,65 @@ extension on TranslationsIt { 'externalPlayer.launchFailed' => 'Impossibile aprire il lettore esterno', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} non è installato', 'externalPlayer.playInExternalPlayer' => 'Riproduci in lettore esterno', + 'metadataEdit.editMetadata' => 'Modifica...', + 'metadataEdit.screenTitle' => 'Modifica metadati', + 'metadataEdit.basicInfo' => 'Informazioni di base', + 'metadataEdit.artwork' => 'Artwork', + 'metadataEdit.advancedSettings' => 'Impostazioni avanzate', + 'metadataEdit.title' => 'Titolo', + 'metadataEdit.sortTitle' => 'Titolo di ordinamento', + 'metadataEdit.originalTitle' => 'Titolo originale', + 'metadataEdit.releaseDate' => 'Data di uscita', + 'metadataEdit.contentRating' => 'Classificazione', + 'metadataEdit.studio' => 'Studio', + 'metadataEdit.tagline' => 'Tagline', + 'metadataEdit.summary' => 'Trama', + 'metadataEdit.poster' => 'Poster', + 'metadataEdit.background' => 'Sfondo', + 'metadataEdit.selectPoster' => 'Seleziona poster', + 'metadataEdit.selectBackground' => 'Seleziona sfondo', + 'metadataEdit.fromUrl' => 'Da URL', + 'metadataEdit.uploadFile' => 'Carica file', + 'metadataEdit.enterImageUrl' => 'Inserisci URL immagine', + 'metadataEdit.imageUrl' => 'URL immagine', + 'metadataEdit.metadataUpdated' => 'Metadati aggiornati', + 'metadataEdit.metadataUpdateFailed' => 'Aggiornamento metadati non riuscito', + 'metadataEdit.artworkUpdated' => 'Artwork aggiornato', + 'metadataEdit.artworkUpdateFailed' => 'Aggiornamento artwork non riuscito', + 'metadataEdit.noArtworkAvailable' => 'Nessun artwork disponibile', + 'metadataEdit.notSet' => 'Non impostato', + 'metadataEdit.libraryDefault' => 'Predefinito libreria', + 'metadataEdit.accountDefault' => 'Predefinito account', + 'metadataEdit.seriesDefault' => 'Predefinito serie', + 'metadataEdit.episodeSorting' => 'Ordinamento episodi', + 'metadataEdit.oldestFirst' => 'Più vecchi prima', + 'metadataEdit.newestFirst' => 'Più recenti prima', + 'metadataEdit.keep' => 'Conserva', + 'metadataEdit.allEpisodes' => 'Tutti gli episodi', + 'metadataEdit.latestEpisodes' => ({required Object count}) => '${count} episodi più recenti', + 'metadataEdit.latestEpisode' => 'Episodio più recente', + 'metadataEdit.episodesAddedPastDays' => ({required Object count}) => 'Episodi aggiunti negli ultimi ${count} giorni', + 'metadataEdit.deleteAfterPlaying' => 'Elimina episodi dopo la riproduzione', + 'metadataEdit.never' => 'Mai', + 'metadataEdit.afterADay' => 'Dopo un giorno', + 'metadataEdit.afterAWeek' => 'Dopo una settimana', + 'metadataEdit.afterAMonth' => 'Dopo un mese', + 'metadataEdit.onNextRefresh' => 'Al prossimo aggiornamento', + 'metadataEdit.seasons' => 'Stagioni', + 'metadataEdit.show' => 'Mostra', + 'metadataEdit.hide' => 'Nascondi', + 'metadataEdit.episodeOrdering' => 'Ordine episodi', + 'metadataEdit.tmdbAiring' => 'The Movie Database (In onda)', + 'metadataEdit.tvdbAiring' => 'TheTVDB (In onda)', + 'metadataEdit.tvdbAbsolute' => 'TheTVDB (Assoluto)', + 'metadataEdit.metadataLanguage' => 'Lingua metadati', + 'metadataEdit.useOriginalTitle' => 'Usa titolo originale', + 'metadataEdit.preferredAudioLanguage' => 'Lingua audio preferita', + 'metadataEdit.preferredSubtitleLanguage' => 'Lingua sottotitoli preferita', + 'metadataEdit.subtitleMode' => 'Selezione automatica sottotitoli', + 'metadataEdit.manuallySelected' => 'Selezionato manualmente', + 'metadataEdit.shownWithForeignAudio' => 'Mostrati con audio straniero', + 'metadataEdit.alwaysEnabled' => 'Sempre attivo', _ => null, }; } diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 6f39ee99..9bb6d101 100644 --- a/lib/i18n/strings_ko.g.dart +++ b/lib/i18n/strings_ko.g.dart @@ -74,6 +74,7 @@ class TranslationsKo with BaseTranslations implements T @override late final _TranslationsCompanionRemoteKo companionRemote = _TranslationsCompanionRemoteKo._(_root); @override late final _TranslationsVideoSettingsKo videoSettings = _TranslationsVideoSettingsKo._(_root); @override late final _TranslationsExternalPlayerKo externalPlayer = _TranslationsExternalPlayerKo._(_root); + @override late final _TranslationsMetadataEditKo metadataEdit = _TranslationsMetadataEditKo._(_root); } // Path: app @@ -1050,6 +1051,74 @@ class _TranslationsExternalPlayerKo implements TranslationsExternalPlayerEn { @override String get playInExternalPlayer => '외부 플레이어에서 재생'; } +// Path: metadataEdit +class _TranslationsMetadataEditKo implements TranslationsMetadataEditEn { + _TranslationsMetadataEditKo._(this._root); + + final TranslationsKo _root; // ignore: unused_field + + // Translations + @override String get editMetadata => '편집...'; + @override String get screenTitle => '메타데이터 편집'; + @override String get basicInfo => '기본 정보'; + @override String get artwork => '아트워크'; + @override String get advancedSettings => '고급 설정'; + @override String get title => '제목'; + @override String get sortTitle => '정렬 제목'; + @override String get originalTitle => '원제'; + @override String get releaseDate => '출시일'; + @override String get contentRating => '콘텐츠 등급'; + @override String get studio => '스튜디오'; + @override String get tagline => '태그라인'; + @override String get summary => '줄거리'; + @override String get poster => '포스터'; + @override String get background => '배경'; + @override String get selectPoster => '포스터 선택'; + @override String get selectBackground => '배경 선택'; + @override String get fromUrl => 'URL에서'; + @override String get uploadFile => '파일 업로드'; + @override String get enterImageUrl => '이미지 URL 입력'; + @override String get imageUrl => '이미지 URL'; + @override String get metadataUpdated => '메타데이터가 업데이트되었습니다'; + @override String get metadataUpdateFailed => '메타데이터 업데이트 실패'; + @override String get artworkUpdated => '아트워크가 업데이트되었습니다'; + @override String get artworkUpdateFailed => '아트워크 업데이트 실패'; + @override String get noArtworkAvailable => '사용 가능한 아트워크 없음'; + @override String get notSet => '설정되지 않음'; + @override String get libraryDefault => '라이브러리 기본값'; + @override String get accountDefault => '계정 기본값'; + @override String get seriesDefault => '시리즈 기본값'; + @override String get episodeSorting => '에피소드 정렬'; + @override String get oldestFirst => '오래된 순'; + @override String get newestFirst => '최신 순'; + @override String get keep => '유지'; + @override String get allEpisodes => '모든 에피소드'; + @override String latestEpisodes({required Object count}) => '최신 에피소드 ${count}개'; + @override String get latestEpisode => '최신 에피소드'; + @override String episodesAddedPastDays({required Object count}) => '최근 ${count}일 내 추가된 에피소드'; + @override String get deleteAfterPlaying => '재생 후 에피소드 삭제'; + @override String get never => '안 함'; + @override String get afterADay => '하루 후'; + @override String get afterAWeek => '일주일 후'; + @override String get afterAMonth => '한 달 후'; + @override String get onNextRefresh => '다음 새로고침 시'; + @override String get seasons => '시즌'; + @override String get show => '표시'; + @override String get hide => '숨기기'; + @override String get episodeOrdering => '에피소드 순서'; + @override String get tmdbAiring => 'The Movie Database (방영순)'; + @override String get tvdbAiring => 'TheTVDB (방영순)'; + @override String get tvdbAbsolute => 'TheTVDB (절대순)'; + @override String get metadataLanguage => '메타데이터 언어'; + @override String get useOriginalTitle => '원제 사용'; + @override String get preferredAudioLanguage => '선호 오디오 언어'; + @override String get preferredSubtitleLanguage => '선호 자막 언어'; + @override String get subtitleMode => '자막 자동 선택 모드'; + @override String get manuallySelected => '수동 선택'; + @override String get shownWithForeignAudio => '외국어 오디오 시 표시'; + @override String get alwaysEnabled => '항상 활성화'; +} + // Path: hotkeys.actions class _TranslationsHotkeysActionsKo implements TranslationsHotkeysActionsEn { _TranslationsHotkeysActionsKo._(this._root); @@ -1979,6 +2048,65 @@ extension on TranslationsKo { 'externalPlayer.launchFailed' => '외부 플레이어를 열 수 없습니다', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name}이(가) 설치되어 있지 않습니다', 'externalPlayer.playInExternalPlayer' => '외부 플레이어에서 재생', + 'metadataEdit.editMetadata' => '편집...', + 'metadataEdit.screenTitle' => '메타데이터 편집', + 'metadataEdit.basicInfo' => '기본 정보', + 'metadataEdit.artwork' => '아트워크', + 'metadataEdit.advancedSettings' => '고급 설정', + 'metadataEdit.title' => '제목', + 'metadataEdit.sortTitle' => '정렬 제목', + 'metadataEdit.originalTitle' => '원제', + 'metadataEdit.releaseDate' => '출시일', + 'metadataEdit.contentRating' => '콘텐츠 등급', + 'metadataEdit.studio' => '스튜디오', + 'metadataEdit.tagline' => '태그라인', + 'metadataEdit.summary' => '줄거리', + 'metadataEdit.poster' => '포스터', + 'metadataEdit.background' => '배경', + 'metadataEdit.selectPoster' => '포스터 선택', + 'metadataEdit.selectBackground' => '배경 선택', + 'metadataEdit.fromUrl' => 'URL에서', + 'metadataEdit.uploadFile' => '파일 업로드', + 'metadataEdit.enterImageUrl' => '이미지 URL 입력', + 'metadataEdit.imageUrl' => '이미지 URL', + 'metadataEdit.metadataUpdated' => '메타데이터가 업데이트되었습니다', + 'metadataEdit.metadataUpdateFailed' => '메타데이터 업데이트 실패', + 'metadataEdit.artworkUpdated' => '아트워크가 업데이트되었습니다', + 'metadataEdit.artworkUpdateFailed' => '아트워크 업데이트 실패', + 'metadataEdit.noArtworkAvailable' => '사용 가능한 아트워크 없음', + 'metadataEdit.notSet' => '설정되지 않음', + 'metadataEdit.libraryDefault' => '라이브러리 기본값', + 'metadataEdit.accountDefault' => '계정 기본값', + 'metadataEdit.seriesDefault' => '시리즈 기본값', + 'metadataEdit.episodeSorting' => '에피소드 정렬', + 'metadataEdit.oldestFirst' => '오래된 순', + 'metadataEdit.newestFirst' => '최신 순', + 'metadataEdit.keep' => '유지', + 'metadataEdit.allEpisodes' => '모든 에피소드', + 'metadataEdit.latestEpisodes' => ({required Object count}) => '최신 에피소드 ${count}개', + 'metadataEdit.latestEpisode' => '최신 에피소드', + 'metadataEdit.episodesAddedPastDays' => ({required Object count}) => '최근 ${count}일 내 추가된 에피소드', + 'metadataEdit.deleteAfterPlaying' => '재생 후 에피소드 삭제', + 'metadataEdit.never' => '안 함', + 'metadataEdit.afterADay' => '하루 후', + 'metadataEdit.afterAWeek' => '일주일 후', + 'metadataEdit.afterAMonth' => '한 달 후', + 'metadataEdit.onNextRefresh' => '다음 새로고침 시', + 'metadataEdit.seasons' => '시즌', + 'metadataEdit.show' => '표시', + 'metadataEdit.hide' => '숨기기', + 'metadataEdit.episodeOrdering' => '에피소드 순서', + 'metadataEdit.tmdbAiring' => 'The Movie Database (방영순)', + 'metadataEdit.tvdbAiring' => 'TheTVDB (방영순)', + 'metadataEdit.tvdbAbsolute' => 'TheTVDB (절대순)', + 'metadataEdit.metadataLanguage' => '메타데이터 언어', + 'metadataEdit.useOriginalTitle' => '원제 사용', + 'metadataEdit.preferredAudioLanguage' => '선호 오디오 언어', + 'metadataEdit.preferredSubtitleLanguage' => '선호 자막 언어', + 'metadataEdit.subtitleMode' => '자막 자동 선택 모드', + 'metadataEdit.manuallySelected' => '수동 선택', + 'metadataEdit.shownWithForeignAudio' => '외국어 오디오 시 표시', + 'metadataEdit.alwaysEnabled' => '항상 활성화', _ => null, }; } diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index fbf984de..04f62f64 100644 --- a/lib/i18n/strings_nl.g.dart +++ b/lib/i18n/strings_nl.g.dart @@ -74,6 +74,7 @@ class TranslationsNl with BaseTranslations implements T @override late final _TranslationsCompanionRemoteNl companionRemote = _TranslationsCompanionRemoteNl._(_root); @override late final _TranslationsVideoSettingsNl videoSettings = _TranslationsVideoSettingsNl._(_root); @override late final _TranslationsExternalPlayerNl externalPlayer = _TranslationsExternalPlayerNl._(_root); + @override late final _TranslationsMetadataEditNl metadataEdit = _TranslationsMetadataEditNl._(_root); } // Path: app @@ -1050,6 +1051,74 @@ class _TranslationsExternalPlayerNl implements TranslationsExternalPlayerEn { @override String get playInExternalPlayer => 'Afspelen in externe speler'; } +// Path: metadataEdit +class _TranslationsMetadataEditNl implements TranslationsMetadataEditEn { + _TranslationsMetadataEditNl._(this._root); + + final TranslationsNl _root; // ignore: unused_field + + // Translations + @override String get editMetadata => 'Bewerken...'; + @override String get screenTitle => 'Metadata bewerken'; + @override String get basicInfo => 'Basisinformatie'; + @override String get artwork => 'Artwork'; + @override String get advancedSettings => 'Geavanceerde instellingen'; + @override String get title => 'Titel'; + @override String get sortTitle => 'Sorteertitel'; + @override String get originalTitle => 'Oorspronkelijke titel'; + @override String get releaseDate => 'Releasedatum'; + @override String get contentRating => 'Leeftijdsclassificatie'; + @override String get studio => 'Studio'; + @override String get tagline => 'Tagline'; + @override String get summary => 'Samenvatting'; + @override String get poster => 'Poster'; + @override String get background => 'Achtergrond'; + @override String get selectPoster => 'Poster selecteren'; + @override String get selectBackground => 'Achtergrond selecteren'; + @override String get fromUrl => 'Vanaf URL'; + @override String get uploadFile => 'Bestand uploaden'; + @override String get enterImageUrl => 'Voer afbeeldings-URL in'; + @override String get imageUrl => 'Afbeeldings-URL'; + @override String get metadataUpdated => 'Metadata bijgewerkt'; + @override String get metadataUpdateFailed => 'Metadata bijwerken mislukt'; + @override String get artworkUpdated => 'Artwork bijgewerkt'; + @override String get artworkUpdateFailed => 'Artwork bijwerken mislukt'; + @override String get noArtworkAvailable => 'Geen artwork beschikbaar'; + @override String get notSet => 'Niet ingesteld'; + @override String get libraryDefault => 'Bibliotheekstandaard'; + @override String get accountDefault => 'Accountstandaard'; + @override String get seriesDefault => 'Seriestandaard'; + @override String get episodeSorting => 'Afleveringen sorteren'; + @override String get oldestFirst => 'Oudste eerst'; + @override String get newestFirst => 'Nieuwste eerst'; + @override String get keep => 'Bewaren'; + @override String get allEpisodes => 'Alle afleveringen'; + @override String latestEpisodes({required Object count}) => '${count} nieuwste afleveringen'; + @override String get latestEpisode => 'Nieuwste aflevering'; + @override String episodesAddedPastDays({required Object count}) => 'Afleveringen toegevoegd in de afgelopen ${count} dagen'; + @override String get deleteAfterPlaying => 'Afleveringen verwijderen na afspelen'; + @override String get never => 'Nooit'; + @override String get afterADay => 'Na een dag'; + @override String get afterAWeek => 'Na een week'; + @override String get afterAMonth => 'Na een maand'; + @override String get onNextRefresh => 'Bij volgende verversing'; + @override String get seasons => 'Seizoenen'; + @override String get show => 'Tonen'; + @override String get hide => 'Verbergen'; + @override String get episodeOrdering => 'Afleveringsvolgorde'; + @override String get tmdbAiring => 'The Movie Database (Uitgezonden)'; + @override String get tvdbAiring => 'TheTVDB (Uitgezonden)'; + @override String get tvdbAbsolute => 'TheTVDB (Absoluut)'; + @override String get metadataLanguage => 'Metadatataal'; + @override String get useOriginalTitle => 'Oorspronkelijke titel gebruiken'; + @override String get preferredAudioLanguage => 'Voorkeurstaal audio'; + @override String get preferredSubtitleLanguage => 'Voorkeurstaal ondertiteling'; + @override String get subtitleMode => 'Automatische ondertitelselectie'; + @override String get manuallySelected => 'Handmatig geselecteerd'; + @override String get shownWithForeignAudio => 'Weergeven bij anderstalig geluid'; + @override String get alwaysEnabled => 'Altijd ingeschakeld'; +} + // Path: hotkeys.actions class _TranslationsHotkeysActionsNl implements TranslationsHotkeysActionsEn { _TranslationsHotkeysActionsNl._(this._root); @@ -1979,6 +2048,65 @@ extension on TranslationsNl { 'externalPlayer.launchFailed' => 'Kan externe speler niet openen', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} is niet geïnstalleerd', 'externalPlayer.playInExternalPlayer' => 'Afspelen in externe speler', + 'metadataEdit.editMetadata' => 'Bewerken...', + 'metadataEdit.screenTitle' => 'Metadata bewerken', + 'metadataEdit.basicInfo' => 'Basisinformatie', + 'metadataEdit.artwork' => 'Artwork', + 'metadataEdit.advancedSettings' => 'Geavanceerde instellingen', + 'metadataEdit.title' => 'Titel', + 'metadataEdit.sortTitle' => 'Sorteertitel', + 'metadataEdit.originalTitle' => 'Oorspronkelijke titel', + 'metadataEdit.releaseDate' => 'Releasedatum', + 'metadataEdit.contentRating' => 'Leeftijdsclassificatie', + 'metadataEdit.studio' => 'Studio', + 'metadataEdit.tagline' => 'Tagline', + 'metadataEdit.summary' => 'Samenvatting', + 'metadataEdit.poster' => 'Poster', + 'metadataEdit.background' => 'Achtergrond', + 'metadataEdit.selectPoster' => 'Poster selecteren', + 'metadataEdit.selectBackground' => 'Achtergrond selecteren', + 'metadataEdit.fromUrl' => 'Vanaf URL', + 'metadataEdit.uploadFile' => 'Bestand uploaden', + 'metadataEdit.enterImageUrl' => 'Voer afbeeldings-URL in', + 'metadataEdit.imageUrl' => 'Afbeeldings-URL', + 'metadataEdit.metadataUpdated' => 'Metadata bijgewerkt', + 'metadataEdit.metadataUpdateFailed' => 'Metadata bijwerken mislukt', + 'metadataEdit.artworkUpdated' => 'Artwork bijgewerkt', + 'metadataEdit.artworkUpdateFailed' => 'Artwork bijwerken mislukt', + 'metadataEdit.noArtworkAvailable' => 'Geen artwork beschikbaar', + 'metadataEdit.notSet' => 'Niet ingesteld', + 'metadataEdit.libraryDefault' => 'Bibliotheekstandaard', + 'metadataEdit.accountDefault' => 'Accountstandaard', + 'metadataEdit.seriesDefault' => 'Seriestandaard', + 'metadataEdit.episodeSorting' => 'Afleveringen sorteren', + 'metadataEdit.oldestFirst' => 'Oudste eerst', + 'metadataEdit.newestFirst' => 'Nieuwste eerst', + 'metadataEdit.keep' => 'Bewaren', + 'metadataEdit.allEpisodes' => 'Alle afleveringen', + 'metadataEdit.latestEpisodes' => ({required Object count}) => '${count} nieuwste afleveringen', + 'metadataEdit.latestEpisode' => 'Nieuwste aflevering', + 'metadataEdit.episodesAddedPastDays' => ({required Object count}) => 'Afleveringen toegevoegd in de afgelopen ${count} dagen', + 'metadataEdit.deleteAfterPlaying' => 'Afleveringen verwijderen na afspelen', + 'metadataEdit.never' => 'Nooit', + 'metadataEdit.afterADay' => 'Na een dag', + 'metadataEdit.afterAWeek' => 'Na een week', + 'metadataEdit.afterAMonth' => 'Na een maand', + 'metadataEdit.onNextRefresh' => 'Bij volgende verversing', + 'metadataEdit.seasons' => 'Seizoenen', + 'metadataEdit.show' => 'Tonen', + 'metadataEdit.hide' => 'Verbergen', + 'metadataEdit.episodeOrdering' => 'Afleveringsvolgorde', + 'metadataEdit.tmdbAiring' => 'The Movie Database (Uitgezonden)', + 'metadataEdit.tvdbAiring' => 'TheTVDB (Uitgezonden)', + 'metadataEdit.tvdbAbsolute' => 'TheTVDB (Absoluut)', + 'metadataEdit.metadataLanguage' => 'Metadatataal', + 'metadataEdit.useOriginalTitle' => 'Oorspronkelijke titel gebruiken', + 'metadataEdit.preferredAudioLanguage' => 'Voorkeurstaal audio', + 'metadataEdit.preferredSubtitleLanguage' => 'Voorkeurstaal ondertiteling', + 'metadataEdit.subtitleMode' => 'Automatische ondertitelselectie', + 'metadataEdit.manuallySelected' => 'Handmatig geselecteerd', + 'metadataEdit.shownWithForeignAudio' => 'Weergeven bij anderstalig geluid', + 'metadataEdit.alwaysEnabled' => 'Altijd ingeschakeld', _ => null, }; } diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index b8b82f8c..99cdb232 100644 --- a/lib/i18n/strings_sv.g.dart +++ b/lib/i18n/strings_sv.g.dart @@ -74,6 +74,7 @@ class TranslationsSv with BaseTranslations implements T @override late final _TranslationsCompanionRemoteSv companionRemote = _TranslationsCompanionRemoteSv._(_root); @override late final _TranslationsVideoSettingsSv videoSettings = _TranslationsVideoSettingsSv._(_root); @override late final _TranslationsExternalPlayerSv externalPlayer = _TranslationsExternalPlayerSv._(_root); + @override late final _TranslationsMetadataEditSv metadataEdit = _TranslationsMetadataEditSv._(_root); } // Path: app @@ -1050,6 +1051,74 @@ class _TranslationsExternalPlayerSv implements TranslationsExternalPlayerEn { @override String get playInExternalPlayer => 'Spela i extern spelare'; } +// Path: metadataEdit +class _TranslationsMetadataEditSv implements TranslationsMetadataEditEn { + _TranslationsMetadataEditSv._(this._root); + + final TranslationsSv _root; // ignore: unused_field + + // Translations + @override String get editMetadata => 'Redigera...'; + @override String get screenTitle => 'Redigera metadata'; + @override String get basicInfo => 'Grundläggande info'; + @override String get artwork => 'Artwork'; + @override String get advancedSettings => 'Avancerade inställningar'; + @override String get title => 'Titel'; + @override String get sortTitle => 'Sorteringstitel'; + @override String get originalTitle => 'Originaltitel'; + @override String get releaseDate => 'Utgivningsdatum'; + @override String get contentRating => 'Åldersgräns'; + @override String get studio => 'Studio'; + @override String get tagline => 'Tagline'; + @override String get summary => 'Sammanfattning'; + @override String get poster => 'Poster'; + @override String get background => 'Bakgrund'; + @override String get selectPoster => 'Välj poster'; + @override String get selectBackground => 'Välj bakgrund'; + @override String get fromUrl => 'Från URL'; + @override String get uploadFile => 'Ladda upp fil'; + @override String get enterImageUrl => 'Ange bild-URL'; + @override String get imageUrl => 'Bild-URL'; + @override String get metadataUpdated => 'Metadata uppdaterad'; + @override String get metadataUpdateFailed => 'Kunde inte uppdatera metadata'; + @override String get artworkUpdated => 'Artwork uppdaterad'; + @override String get artworkUpdateFailed => 'Kunde inte uppdatera artwork'; + @override String get noArtworkAvailable => 'Ingen artwork tillgänglig'; + @override String get notSet => 'Inte angiven'; + @override String get libraryDefault => 'Biblioteksstandard'; + @override String get accountDefault => 'Kontostandard'; + @override String get seriesDefault => 'Seriestandard'; + @override String get episodeSorting => 'Avsnittsortering'; + @override String get oldestFirst => 'Äldst först'; + @override String get newestFirst => 'Nyast först'; + @override String get keep => 'Behåll'; + @override String get allEpisodes => 'Alla avsnitt'; + @override String latestEpisodes({required Object count}) => '${count} senaste avsnitten'; + @override String get latestEpisode => 'Senaste avsnittet'; + @override String episodesAddedPastDays({required Object count}) => 'Avsnitt tillagda de senaste ${count} dagarna'; + @override String get deleteAfterPlaying => 'Ta bort avsnitt efter uppspelning'; + @override String get never => 'Aldrig'; + @override String get afterADay => 'Efter en dag'; + @override String get afterAWeek => 'Efter en vecka'; + @override String get afterAMonth => 'Efter en månad'; + @override String get onNextRefresh => 'Vid nästa uppdatering'; + @override String get seasons => 'Säsonger'; + @override String get show => 'Visa'; + @override String get hide => 'Dölj'; + @override String get episodeOrdering => 'Avsnittsordning'; + @override String get tmdbAiring => 'The Movie Database (Sändning)'; + @override String get tvdbAiring => 'TheTVDB (Sändning)'; + @override String get tvdbAbsolute => 'TheTVDB (Absolut)'; + @override String get metadataLanguage => 'Metadataspråk'; + @override String get useOriginalTitle => 'Använd originaltitel'; + @override String get preferredAudioLanguage => 'Föredraget ljudspråk'; + @override String get preferredSubtitleLanguage => 'Föredraget undertextspråk'; + @override String get subtitleMode => 'Automatiskt val av undertexter'; + @override String get manuallySelected => 'Manuellt vald'; + @override String get shownWithForeignAudio => 'Visas vid främmande ljud'; + @override String get alwaysEnabled => 'Alltid aktiverad'; +} + // Path: hotkeys.actions class _TranslationsHotkeysActionsSv implements TranslationsHotkeysActionsEn { _TranslationsHotkeysActionsSv._(this._root); @@ -1979,6 +2048,65 @@ extension on TranslationsSv { 'externalPlayer.launchFailed' => 'Kunde inte öppna extern spelare', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} är inte installerad', 'externalPlayer.playInExternalPlayer' => 'Spela i extern spelare', + 'metadataEdit.editMetadata' => 'Redigera...', + 'metadataEdit.screenTitle' => 'Redigera metadata', + 'metadataEdit.basicInfo' => 'Grundläggande info', + 'metadataEdit.artwork' => 'Artwork', + 'metadataEdit.advancedSettings' => 'Avancerade inställningar', + 'metadataEdit.title' => 'Titel', + 'metadataEdit.sortTitle' => 'Sorteringstitel', + 'metadataEdit.originalTitle' => 'Originaltitel', + 'metadataEdit.releaseDate' => 'Utgivningsdatum', + 'metadataEdit.contentRating' => 'Åldersgräns', + 'metadataEdit.studio' => 'Studio', + 'metadataEdit.tagline' => 'Tagline', + 'metadataEdit.summary' => 'Sammanfattning', + 'metadataEdit.poster' => 'Poster', + 'metadataEdit.background' => 'Bakgrund', + 'metadataEdit.selectPoster' => 'Välj poster', + 'metadataEdit.selectBackground' => 'Välj bakgrund', + 'metadataEdit.fromUrl' => 'Från URL', + 'metadataEdit.uploadFile' => 'Ladda upp fil', + 'metadataEdit.enterImageUrl' => 'Ange bild-URL', + 'metadataEdit.imageUrl' => 'Bild-URL', + 'metadataEdit.metadataUpdated' => 'Metadata uppdaterad', + 'metadataEdit.metadataUpdateFailed' => 'Kunde inte uppdatera metadata', + 'metadataEdit.artworkUpdated' => 'Artwork uppdaterad', + 'metadataEdit.artworkUpdateFailed' => 'Kunde inte uppdatera artwork', + 'metadataEdit.noArtworkAvailable' => 'Ingen artwork tillgänglig', + 'metadataEdit.notSet' => 'Inte angiven', + 'metadataEdit.libraryDefault' => 'Biblioteksstandard', + 'metadataEdit.accountDefault' => 'Kontostandard', + 'metadataEdit.seriesDefault' => 'Seriestandard', + 'metadataEdit.episodeSorting' => 'Avsnittsortering', + 'metadataEdit.oldestFirst' => 'Äldst först', + 'metadataEdit.newestFirst' => 'Nyast först', + 'metadataEdit.keep' => 'Behåll', + 'metadataEdit.allEpisodes' => 'Alla avsnitt', + 'metadataEdit.latestEpisodes' => ({required Object count}) => '${count} senaste avsnitten', + 'metadataEdit.latestEpisode' => 'Senaste avsnittet', + 'metadataEdit.episodesAddedPastDays' => ({required Object count}) => 'Avsnitt tillagda de senaste ${count} dagarna', + 'metadataEdit.deleteAfterPlaying' => 'Ta bort avsnitt efter uppspelning', + 'metadataEdit.never' => 'Aldrig', + 'metadataEdit.afterADay' => 'Efter en dag', + 'metadataEdit.afterAWeek' => 'Efter en vecka', + 'metadataEdit.afterAMonth' => 'Efter en månad', + 'metadataEdit.onNextRefresh' => 'Vid nästa uppdatering', + 'metadataEdit.seasons' => 'Säsonger', + 'metadataEdit.show' => 'Visa', + 'metadataEdit.hide' => 'Dölj', + 'metadataEdit.episodeOrdering' => 'Avsnittsordning', + 'metadataEdit.tmdbAiring' => 'The Movie Database (Sändning)', + 'metadataEdit.tvdbAiring' => 'TheTVDB (Sändning)', + 'metadataEdit.tvdbAbsolute' => 'TheTVDB (Absolut)', + 'metadataEdit.metadataLanguage' => 'Metadataspråk', + 'metadataEdit.useOriginalTitle' => 'Använd originaltitel', + 'metadataEdit.preferredAudioLanguage' => 'Föredraget ljudspråk', + 'metadataEdit.preferredSubtitleLanguage' => 'Föredraget undertextspråk', + 'metadataEdit.subtitleMode' => 'Automatiskt val av undertexter', + 'metadataEdit.manuallySelected' => 'Manuellt vald', + 'metadataEdit.shownWithForeignAudio' => 'Visas vid främmande ljud', + 'metadataEdit.alwaysEnabled' => 'Alltid aktiverad', _ => null, }; } diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index c4dc843a..6f3f1aa2 100644 --- a/lib/i18n/strings_zh.g.dart +++ b/lib/i18n/strings_zh.g.dart @@ -74,6 +74,7 @@ class TranslationsZh with BaseTranslations implements T @override late final _TranslationsCompanionRemoteZh companionRemote = _TranslationsCompanionRemoteZh._(_root); @override late final _TranslationsVideoSettingsZh videoSettings = _TranslationsVideoSettingsZh._(_root); @override late final _TranslationsExternalPlayerZh externalPlayer = _TranslationsExternalPlayerZh._(_root); + @override late final _TranslationsMetadataEditZh metadataEdit = _TranslationsMetadataEditZh._(_root); } // Path: app @@ -1050,6 +1051,74 @@ class _TranslationsExternalPlayerZh implements TranslationsExternalPlayerEn { @override String get playInExternalPlayer => '在外部播放器中播放'; } +// Path: metadataEdit +class _TranslationsMetadataEditZh implements TranslationsMetadataEditEn { + _TranslationsMetadataEditZh._(this._root); + + final TranslationsZh _root; // ignore: unused_field + + // Translations + @override String get editMetadata => '编辑...'; + @override String get screenTitle => '编辑元数据'; + @override String get basicInfo => '基本信息'; + @override String get artwork => '封面图片'; + @override String get advancedSettings => '高级设置'; + @override String get title => '标题'; + @override String get sortTitle => '排序标题'; + @override String get originalTitle => '原始标题'; + @override String get releaseDate => '上映日期'; + @override String get contentRating => '内容分级'; + @override String get studio => '制片厂'; + @override String get tagline => '标语'; + @override String get summary => '简介'; + @override String get poster => '海报'; + @override String get background => '背景'; + @override String get selectPoster => '选择海报'; + @override String get selectBackground => '选择背景'; + @override String get fromUrl => '从 URL'; + @override String get uploadFile => '上传文件'; + @override String get enterImageUrl => '输入图片 URL'; + @override String get imageUrl => '图片 URL'; + @override String get metadataUpdated => '元数据已更新'; + @override String get metadataUpdateFailed => '元数据更新失败'; + @override String get artworkUpdated => '封面图片已更新'; + @override String get artworkUpdateFailed => '封面图片更新失败'; + @override String get noArtworkAvailable => '没有可用的封面图片'; + @override String get notSet => '未设置'; + @override String get libraryDefault => '媒体库默认'; + @override String get accountDefault => '账户默认'; + @override String get seriesDefault => '剧集默认'; + @override String get episodeSorting => '剧集排序'; + @override String get oldestFirst => '最旧优先'; + @override String get newestFirst => '最新优先'; + @override String get keep => '保留'; + @override String get allEpisodes => '所有剧集'; + @override String latestEpisodes({required Object count}) => '最新 ${count} 集'; + @override String get latestEpisode => '最新一集'; + @override String episodesAddedPastDays({required Object count}) => '过去 ${count} 天内添加的剧集'; + @override String get deleteAfterPlaying => '播放后删除剧集'; + @override String get never => '从不'; + @override String get afterADay => '一天后'; + @override String get afterAWeek => '一周后'; + @override String get afterAMonth => '一个月后'; + @override String get onNextRefresh => '下次刷新时'; + @override String get seasons => '季'; + @override String get show => '显示'; + @override String get hide => '隐藏'; + @override String get episodeOrdering => '剧集排列顺序'; + @override String get tmdbAiring => 'The Movie Database(播出顺序)'; + @override String get tvdbAiring => 'TheTVDB(播出顺序)'; + @override String get tvdbAbsolute => 'TheTVDB(绝对顺序)'; + @override String get metadataLanguage => '元数据语言'; + @override String get useOriginalTitle => '使用原始标题'; + @override String get preferredAudioLanguage => '首选音频语言'; + @override String get preferredSubtitleLanguage => '首选字幕语言'; + @override String get subtitleMode => '自动选择字幕模式'; + @override String get manuallySelected => '手动选择'; + @override String get shownWithForeignAudio => '外语音频时显示'; + @override String get alwaysEnabled => '始终启用'; +} + // Path: hotkeys.actions class _TranslationsHotkeysActionsZh implements TranslationsHotkeysActionsEn { _TranslationsHotkeysActionsZh._(this._root); @@ -1979,6 +2048,65 @@ extension on TranslationsZh { 'externalPlayer.launchFailed' => '无法打开外部播放器', 'externalPlayer.appNotInstalled' => ({required Object name}) => '${name} 未安装', 'externalPlayer.playInExternalPlayer' => '在外部播放器中播放', + 'metadataEdit.editMetadata' => '编辑...', + 'metadataEdit.screenTitle' => '编辑元数据', + 'metadataEdit.basicInfo' => '基本信息', + 'metadataEdit.artwork' => '封面图片', + 'metadataEdit.advancedSettings' => '高级设置', + 'metadataEdit.title' => '标题', + 'metadataEdit.sortTitle' => '排序标题', + 'metadataEdit.originalTitle' => '原始标题', + 'metadataEdit.releaseDate' => '上映日期', + 'metadataEdit.contentRating' => '内容分级', + 'metadataEdit.studio' => '制片厂', + 'metadataEdit.tagline' => '标语', + 'metadataEdit.summary' => '简介', + 'metadataEdit.poster' => '海报', + 'metadataEdit.background' => '背景', + 'metadataEdit.selectPoster' => '选择海报', + 'metadataEdit.selectBackground' => '选择背景', + 'metadataEdit.fromUrl' => '从 URL', + 'metadataEdit.uploadFile' => '上传文件', + 'metadataEdit.enterImageUrl' => '输入图片 URL', + 'metadataEdit.imageUrl' => '图片 URL', + 'metadataEdit.metadataUpdated' => '元数据已更新', + 'metadataEdit.metadataUpdateFailed' => '元数据更新失败', + 'metadataEdit.artworkUpdated' => '封面图片已更新', + 'metadataEdit.artworkUpdateFailed' => '封面图片更新失败', + 'metadataEdit.noArtworkAvailable' => '没有可用的封面图片', + 'metadataEdit.notSet' => '未设置', + 'metadataEdit.libraryDefault' => '媒体库默认', + 'metadataEdit.accountDefault' => '账户默认', + 'metadataEdit.seriesDefault' => '剧集默认', + 'metadataEdit.episodeSorting' => '剧集排序', + 'metadataEdit.oldestFirst' => '最旧优先', + 'metadataEdit.newestFirst' => '最新优先', + 'metadataEdit.keep' => '保留', + 'metadataEdit.allEpisodes' => '所有剧集', + 'metadataEdit.latestEpisodes' => ({required Object count}) => '最新 ${count} 集', + 'metadataEdit.latestEpisode' => '最新一集', + 'metadataEdit.episodesAddedPastDays' => ({required Object count}) => '过去 ${count} 天内添加的剧集', + 'metadataEdit.deleteAfterPlaying' => '播放后删除剧集', + 'metadataEdit.never' => '从不', + 'metadataEdit.afterADay' => '一天后', + 'metadataEdit.afterAWeek' => '一周后', + 'metadataEdit.afterAMonth' => '一个月后', + 'metadataEdit.onNextRefresh' => '下次刷新时', + 'metadataEdit.seasons' => '季', + 'metadataEdit.show' => '显示', + 'metadataEdit.hide' => '隐藏', + 'metadataEdit.episodeOrdering' => '剧集排列顺序', + 'metadataEdit.tmdbAiring' => 'The Movie Database(播出顺序)', + 'metadataEdit.tvdbAiring' => 'TheTVDB(播出顺序)', + 'metadataEdit.tvdbAbsolute' => 'TheTVDB(绝对顺序)', + 'metadataEdit.metadataLanguage' => '元数据语言', + 'metadataEdit.useOriginalTitle' => '使用原始标题', + 'metadataEdit.preferredAudioLanguage' => '首选音频语言', + 'metadataEdit.preferredSubtitleLanguage' => '首选字幕语言', + 'metadataEdit.subtitleMode' => '自动选择字幕模式', + 'metadataEdit.manuallySelected' => '手动选择', + 'metadataEdit.shownWithForeignAudio' => '外语音频时显示', + 'metadataEdit.alwaysEnabled' => '始终启用', _ => null, }; } diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index eb765336..31259987 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -1,7 +1,7 @@ { "app": { "title": "Plezy" -}, + }, "auth": { "signInWithPlex": "Logga in med Plex", "showQRCode": "Visa QR-kod", @@ -831,5 +831,66 @@ "launchFailed": "Kunde inte öppna extern spelare", "appNotInstalled": "${name} är inte installerad", "playInExternalPlayer": "Spela i extern spelare" + }, + "metadataEdit": { + "editMetadata": "Redigera...", + "screenTitle": "Redigera metadata", + "basicInfo": "Grundläggande info", + "artwork": "Artwork", + "advancedSettings": "Avancerade inställningar", + "title": "Titel", + "sortTitle": "Sorteringstitel", + "originalTitle": "Originaltitel", + "releaseDate": "Utgivningsdatum", + "contentRating": "Åldersgräns", + "studio": "Studio", + "tagline": "Tagline", + "summary": "Sammanfattning", + "poster": "Poster", + "background": "Bakgrund", + "selectPoster": "Välj poster", + "selectBackground": "Välj bakgrund", + "fromUrl": "Från URL", + "uploadFile": "Ladda upp fil", + "enterImageUrl": "Ange bild-URL", + "imageUrl": "Bild-URL", + "metadataUpdated": "Metadata uppdaterad", + "metadataUpdateFailed": "Kunde inte uppdatera metadata", + "artworkUpdated": "Artwork uppdaterad", + "artworkUpdateFailed": "Kunde inte uppdatera artwork", + "noArtworkAvailable": "Ingen artwork tillgänglig", + "notSet": "Inte angiven", + "libraryDefault": "Biblioteksstandard", + "accountDefault": "Kontostandard", + "seriesDefault": "Seriestandard", + "episodeSorting": "Avsnittsortering", + "oldestFirst": "Äldst först", + "newestFirst": "Nyast först", + "keep": "Behåll", + "allEpisodes": "Alla avsnitt", + "latestEpisodes": "${count} senaste avsnitten", + "latestEpisode": "Senaste avsnittet", + "episodesAddedPastDays": "Avsnitt tillagda de senaste ${count} dagarna", + "deleteAfterPlaying": "Ta bort avsnitt efter uppspelning", + "never": "Aldrig", + "afterADay": "Efter en dag", + "afterAWeek": "Efter en vecka", + "afterAMonth": "Efter en månad", + "onNextRefresh": "Vid nästa uppdatering", + "seasons": "Säsonger", + "show": "Visa", + "hide": "Dölj", + "episodeOrdering": "Avsnittsordning", + "tmdbAiring": "The Movie Database (Sändning)", + "tvdbAiring": "TheTVDB (Sändning)", + "tvdbAbsolute": "TheTVDB (Absolut)", + "metadataLanguage": "Metadataspråk", + "useOriginalTitle": "Använd originaltitel", + "preferredAudioLanguage": "Föredraget ljudspråk", + "preferredSubtitleLanguage": "Föredraget undertextspråk", + "subtitleMode": "Automatiskt val av undertexter", + "manuallySelected": "Manuellt vald", + "shownWithForeignAudio": "Visas vid främmande ljud", + "alwaysEnabled": "Alltid aktiverad" } } diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index 2c3d46aa..49fdc8ab 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -1,7 +1,7 @@ { "app": { "title": "Plezy" -}, + }, "auth": { "signInWithPlex": "使用 Plex 登录", "showQRCode": "显示二维码", @@ -831,5 +831,66 @@ "launchFailed": "无法打开外部播放器", "appNotInstalled": "${name} 未安装", "playInExternalPlayer": "在外部播放器中播放" + }, + "metadataEdit": { + "editMetadata": "编辑...", + "screenTitle": "编辑元数据", + "basicInfo": "基本信息", + "artwork": "封面图片", + "advancedSettings": "高级设置", + "title": "标题", + "sortTitle": "排序标题", + "originalTitle": "原始标题", + "releaseDate": "上映日期", + "contentRating": "内容分级", + "studio": "制片厂", + "tagline": "标语", + "summary": "简介", + "poster": "海报", + "background": "背景", + "selectPoster": "选择海报", + "selectBackground": "选择背景", + "fromUrl": "从 URL", + "uploadFile": "上传文件", + "enterImageUrl": "输入图片 URL", + "imageUrl": "图片 URL", + "metadataUpdated": "元数据已更新", + "metadataUpdateFailed": "元数据更新失败", + "artworkUpdated": "封面图片已更新", + "artworkUpdateFailed": "封面图片更新失败", + "noArtworkAvailable": "没有可用的封面图片", + "notSet": "未设置", + "libraryDefault": "媒体库默认", + "accountDefault": "账户默认", + "seriesDefault": "剧集默认", + "episodeSorting": "剧集排序", + "oldestFirst": "最旧优先", + "newestFirst": "最新优先", + "keep": "保留", + "allEpisodes": "所有剧集", + "latestEpisodes": "最新 ${count} 集", + "latestEpisode": "最新一集", + "episodesAddedPastDays": "过去 ${count} 天内添加的剧集", + "deleteAfterPlaying": "播放后删除剧集", + "never": "从不", + "afterADay": "一天后", + "afterAWeek": "一周后", + "afterAMonth": "一个月后", + "onNextRefresh": "下次刷新时", + "seasons": "季", + "show": "显示", + "hide": "隐藏", + "episodeOrdering": "剧集排列顺序", + "tmdbAiring": "The Movie Database(播出顺序)", + "tvdbAiring": "TheTVDB(播出顺序)", + "tvdbAbsolute": "TheTVDB(绝对顺序)", + "metadataLanguage": "元数据语言", + "useOriginalTitle": "使用原始标题", + "preferredAudioLanguage": "首选音频语言", + "preferredSubtitleLanguage": "首选字幕语言", + "subtitleMode": "自动选择字幕模式", + "manuallySelected": "手动选择", + "shownWithForeignAudio": "外语音频时显示", + "alwaysEnabled": "始终启用" } } diff --git a/lib/models/plex_metadata.dart b/lib/models/plex_metadata.dart index e1b0c2b1..2828828f 100644 --- a/lib/models/plex_metadata.dart +++ b/lib/models/plex_metadata.dart @@ -33,6 +33,18 @@ enum PlexMediaType { /// Whether this type can be played directly bool get isPlayable => isVideo || this == track; + + /// Plex API type number for metadata editing endpoints + int get typeNumber => switch (this) { + PlexMediaType.movie => 1, + PlexMediaType.show => 2, + PlexMediaType.season => 3, + PlexMediaType.episode => 4, + PlexMediaType.artist => 8, + PlexMediaType.album => 9, + PlexMediaType.track => 10, + _ => 0, + }; } @JsonSerializable() @@ -81,6 +93,8 @@ class PlexMetadata with MultiServerFields { final int? librarySectionID; // Library section ID this item belongs to final String? ratingImage; // Rating source URI (e.g. rottentomatoes://image.rating.ripe) final String? audienceRatingImage; // Audience rating source URI + final String? tagline; + final String? originalTitle; final String? subtype; // Clip subtype: "trailer", "behindTheScenes", "deleted", etc. final int? extraType; // Numeric extra type identifier final String? primaryExtraKey; // Points to main trailer (e.g., "/library/metadata/52601") @@ -161,6 +175,8 @@ class PlexMetadata with MultiServerFields { this.librarySectionID, this.ratingImage, this.audienceRatingImage, + this.tagline, + this.originalTitle, this.subtype, this.extraType, this.primaryExtraKey, @@ -214,6 +230,8 @@ class PlexMetadata with MultiServerFields { int? librarySectionID, String? ratingImage, String? audienceRatingImage, + String? tagline, + String? originalTitle, String? subtype, int? extraType, String? primaryExtraKey, @@ -265,6 +283,8 @@ class PlexMetadata with MultiServerFields { librarySectionID: librarySectionID ?? this.librarySectionID, ratingImage: ratingImage ?? this.ratingImage, audienceRatingImage: audienceRatingImage ?? this.audienceRatingImage, + tagline: tagline ?? this.tagline, + originalTitle: originalTitle ?? this.originalTitle, subtype: subtype ?? this.subtype, extraType: extraType ?? this.extraType, primaryExtraKey: primaryExtraKey ?? this.primaryExtraKey, diff --git a/lib/models/plex_metadata.g.dart b/lib/models/plex_metadata.g.dart index 54ac64b9..cd57d744 100644 --- a/lib/models/plex_metadata.g.dart +++ b/lib/models/plex_metadata.g.dart @@ -50,6 +50,8 @@ PlexMetadata _$PlexMetadataFromJson(Map json) => PlexMetadata( librarySectionID: (json['librarySectionID'] as num?)?.toInt(), ratingImage: json['ratingImage'] as String?, audienceRatingImage: json['audienceRatingImage'] as String?, + tagline: json['tagline'] as String?, + originalTitle: json['originalTitle'] as String?, subtype: json['subtype'] as String?, extraType: (json['extraType'] as num?)?.toInt(), primaryExtraKey: json['primaryExtraKey'] as String?, @@ -100,6 +102,8 @@ Map _$PlexMetadataToJson(PlexMetadata instance) => with WatchStateAw iconSize: 20, style: IconButton.styleFrom(minimumSize: const Size(48, 48), maximumSize: const Size(48, 48)), ), + // Edit metadata button (hidden in offline mode) + if (!widget.isOffline) ...[ + const SizedBox(width: 12), + IconButton.filledTonal( + onPressed: () async { + final result = await Navigator.push( + context, + MaterialPageRoute(builder: (context) => MetadataEditScreen(metadata: metadata)), + ); + if (result == true && mounted) { + _loadFullMetadata(); + } + }, + icon: const AppIcon(Symbols.edit_rounded, fill: 1), + tooltip: t.metadataEdit.editMetadata, + iconSize: 20, + style: IconButton.styleFrom(minimumSize: const Size(48, 48), maximumSize: const Size(48, 48)), + ), + ], ], ), ); diff --git a/lib/screens/metadata_edit_screen.dart b/lib/screens/metadata_edit_screen.dart new file mode 100644 index 00000000..d17fb0e8 --- /dev/null +++ b/lib/screens/metadata_edit_screen.dart @@ -0,0 +1,717 @@ +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import '../focus/focusable_button.dart'; +import '../i18n/strings.g.dart'; +import '../models/plex_metadata.dart'; +import '../services/plex_client.dart'; +import '../utils/dialogs.dart'; +import '../utils/language_codes.dart'; +import '../utils/provider_extensions.dart'; +import '../utils/snackbar_helper.dart'; +import '../widgets/app_icon.dart'; +import '../widgets/artwork_picker_dialog.dart'; +import '../widgets/focusable_list_tile.dart'; +import '../widgets/focused_scroll_scaffold.dart'; +import '../widgets/plex_optimized_image.dart'; + +class MetadataEditScreen extends StatefulWidget { + final PlexMetadata metadata; + + const MetadataEditScreen({super.key, required this.metadata}); + + @override + State createState() => _MetadataEditScreenState(); +} + +class _MetadataEditScreenState extends State { + late PlexClient _client; + PlexMetadata? _fullMetadata; + bool _isLoading = true; + bool _isSaving = false; + + // Text field values + String? _title; + String? _titleSort; + String? _originalTitle; + String? _originallyAvailableAt; + String? _contentRating; + String? _studio; + String? _tagline; + String? _summary; + + // Original values for change detection + String? _origTitle; + String? _origTitleSort; + String? _origOriginalTitle; + String? _origOriginallyAvailableAt; + String? _origContentRating; + String? _origStudio; + String? _origTagline; + String? _origSummary; + + // Advanced prefs (loaded from metadata JSON) + final Map _currentPrefs = {}; + + bool get _hasChanges => + _title != _origTitle || + _titleSort != _origTitleSort || + _originalTitle != _origOriginalTitle || + _originallyAvailableAt != _origOriginallyAvailableAt || + _contentRating != _origContentRating || + _studio != _origStudio || + _tagline != _origTagline || + _summary != _origSummary; + + PlexMediaType get _mediaType => widget.metadata.mediaType; + + @override + void initState() { + super.initState(); + _client = context.getClientWithFallback(widget.metadata.serverId); + _loadMetadata(); + } + + Future _loadMetadata() async { + try { + final meta = await _client.getMetadataWithImages(widget.metadata.ratingKey); + if (!mounted) return; + if (meta != null) { + _fullMetadata = meta; + _initFieldsFromMetadata(meta); + } else { + _initFieldsFromMetadata(widget.metadata); + } + setState(() => _isLoading = false); + } catch (e) { + if (!mounted) return; + _initFieldsFromMetadata(widget.metadata); + setState(() => _isLoading = false); + } + } + + void _initFieldsFromMetadata(PlexMetadata meta) { + _title = meta.title; + _titleSort = meta.titleSort ?? ''; + _originalTitle = meta.originalTitle ?? ''; + _originallyAvailableAt = meta.originallyAvailableAt ?? ''; + _contentRating = meta.contentRating ?? ''; + _studio = meta.studio ?? ''; + _tagline = meta.tagline ?? ''; + _summary = meta.summary ?? ''; + + _origTitle = _title; + _origTitleSort = _titleSort; + _origOriginalTitle = _originalTitle; + _origOriginallyAvailableAt = _originallyAvailableAt; + _origContentRating = _contentRating; + _origStudio = _studio; + _origTagline = _tagline; + _origSummary = _summary; + } + + Future _save() async { + if (!_hasChanges || _isSaving) return; + + final sectionId = _fullMetadata?.librarySectionID ?? widget.metadata.librarySectionID; + if (sectionId == null) { + if (mounted) showErrorSnackBar(context, t.metadataEdit.metadataUpdateFailed); + return; + } + + setState(() => _isSaving = true); + + final success = await _client.updateMetadata( + sectionId: sectionId, + ratingKey: widget.metadata.ratingKey, + typeNumber: _mediaType.typeNumber, + title: _title != _origTitle ? _title : null, + titleSort: _titleSort != _origTitleSort ? _titleSort : null, + originalTitle: _originalTitle != _origOriginalTitle ? _originalTitle : null, + originallyAvailableAt: _originallyAvailableAt != _origOriginallyAvailableAt ? _originallyAvailableAt : null, + contentRating: _contentRating != _origContentRating ? _contentRating : null, + studio: _studio != _origStudio ? _studio : null, + tagline: _tagline != _origTagline ? _tagline : null, + summary: _summary != _origSummary ? _summary : null, + ); + + if (!mounted) return; + setState(() => _isSaving = false); + + if (success) { + showSuccessSnackBar(context, t.metadataEdit.metadataUpdated); + Navigator.pop(context, true); + } else { + showErrorSnackBar(context, t.metadataEdit.metadataUpdateFailed); + } + } + + Future _editTextField({ + required String title, + required String label, + required String? currentValue, + required ValueChanged onChanged, + bool multiline = false, + }) async { + final String? result; + if (multiline) { + result = await showMultilineTextInputDialog( + context, + title: title, + labelText: label, + initialValue: currentValue, + ); + } else { + result = await showTextInputDialog( + context, + title: title, + labelText: label, + hintText: '', + initialValue: currentValue, + ); + } + + if (result != null && mounted) { + final value = result; + setState(() => onChanged(value)); + } + } + + Future _editDate() async { + DateTime initial = DateTime.now(); + if (_originallyAvailableAt != null && _originallyAvailableAt!.isNotEmpty) { + final parsed = DateTime.tryParse(_originallyAvailableAt!); + if (parsed != null) initial = parsed; + } + + final picked = await showDatePicker( + context: context, + initialDate: initial, + firstDate: DateTime(1800), + lastDate: DateTime(2200), + ); + + if (picked != null && mounted) { + setState(() { + _originallyAvailableAt = + '${picked.year}-${picked.month.toString().padLeft(2, '0')}-${picked.day.toString().padLeft(2, '0')}'; + }); + } + } + + Future _openArtworkPicker(String element) async { + final result = await showDialog( + context: context, + builder: (context) => ArtworkPickerDialog( + client: _client, + ratingKey: widget.metadata.ratingKey, + element: element, + ), + ); + + if (result == true && mounted) { + // Reload metadata to get updated artwork paths + _loadMetadata(); + } + } + + Future _showAdvancedSettingDialog({ + required String title, + required String prefKey, + required List<({String value, String label})> options, + }) async { + // Determine current value from metadata or default + final currentValue = _currentPrefs[prefKey] ?? _getMetadataPrefValue(prefKey); + + final result = await showDialog( + context: context, + builder: (dialogContext) { + String? selected = currentValue; + return StatefulBuilder( + builder: (context, setDialogState) { + return AlertDialog( + title: Text(title), + content: SizedBox( + width: double.maxFinite, + child: ListView( + shrinkWrap: true, + children: options.map((option) { + return FocusableRadioListTile( + title: Text(option.label), + value: option.value, + groupValue: selected, + onChanged: (val) { + setDialogState(() => selected = val); + Navigator.pop(dialogContext, val); + }, + ); + }).toList(), + ), + ), + actions: [ + FocusableButton( + autofocus: true, + onPressed: () => Navigator.pop(dialogContext), + child: TextButton( + onPressed: () => Navigator.pop(dialogContext), + child: Text(t.common.cancel), + ), + ), + ], + ); + }, + ); + }, + ); + + if (result != null && mounted) { + setState(() => _currentPrefs[prefKey] = result); + await _client.updateMetadataPrefs(widget.metadata.ratingKey, {prefKey: result}); + } + } + + String _getMetadataPrefValue(String key) { + // These prefs appear as keys on the raw metadata JSON when non-default. + // Since we use typed models, we check known fields. + final meta = _fullMetadata ?? widget.metadata; + switch (key) { + case 'audioLanguage': + return meta.audioLanguage ?? ''; + case 'subtitleLanguage': + return meta.subtitleLanguage ?? ''; + default: + return ''; + } + } + + String _getDisplayValueForPref(String prefKey, List<({String value, String label})> options) { + final val = _currentPrefs[prefKey] ?? _getMetadataPrefValue(prefKey); + for (final option in options) { + if (option.value == val) return option.label; + } + return options.first.label; + } + + // ===== Field visibility ===== + + bool get _showSortTitle => _mediaType != PlexMediaType.season; + bool get _showOriginalTitle => _mediaType == PlexMediaType.movie || _mediaType == PlexMediaType.show; + bool get _showReleaseDate => _mediaType != PlexMediaType.season; + bool get _showContentRating => _mediaType != PlexMediaType.season; + bool get _showStudio => _mediaType == PlexMediaType.movie || _mediaType == PlexMediaType.show; + bool get _showTagline => _mediaType == PlexMediaType.movie || _mediaType == PlexMediaType.show; + bool get _showBackground => + _mediaType == PlexMediaType.movie || _mediaType == PlexMediaType.show || _mediaType == PlexMediaType.episode; + bool get _showAdvanced => _mediaType != PlexMediaType.episode; + + @override + Widget build(BuildContext context) { + if (_isLoading) { + return FocusedScrollScaffold( + title: Text(t.metadataEdit.screenTitle), + slivers: [const SliverFillRemaining(child: Center(child: CircularProgressIndicator()))], + ); + } + + return FocusedScrollScaffold( + title: Text(t.metadataEdit.screenTitle), + actions: [ + if (_isSaving) + const Padding( + padding: EdgeInsets.all(12), + child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)), + ) + else + IconButton( + onPressed: _hasChanges ? _save : null, + icon: const AppIcon(Symbols.check_rounded, fill: 1), + ), + ], + slivers: [ + SliverPadding( + padding: const EdgeInsets.all(16), + sliver: SliverList( + delegate: SliverChildListDelegate([ + _buildBasicInfoCard(), + const SizedBox(height: 16), + _buildArtworkCard(), + if (_showAdvanced) ...[ + const SizedBox(height: 16), + _buildAdvancedSettingsCard(), + ], + ]), + ), + ), + ], + ); + } + + Widget _buildBasicInfoCard() { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + t.metadataEdit.basicInfo, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + _buildFieldTile( + label: t.metadataEdit.title, + value: _title, + onTap: () => _editTextField( + title: t.metadataEdit.title, + label: t.metadataEdit.title, + currentValue: _title, + onChanged: (v) => _title = v, + ), + ), + if (_showSortTitle) + _buildFieldTile( + label: t.metadataEdit.sortTitle, + value: _titleSort, + onTap: () => _editTextField( + title: t.metadataEdit.sortTitle, + label: t.metadataEdit.sortTitle, + currentValue: _titleSort, + onChanged: (v) => _titleSort = v, + ), + ), + if (_showOriginalTitle) + _buildFieldTile( + label: t.metadataEdit.originalTitle, + value: _originalTitle, + onTap: () => _editTextField( + title: t.metadataEdit.originalTitle, + label: t.metadataEdit.originalTitle, + currentValue: _originalTitle, + onChanged: (v) => _originalTitle = v, + ), + ), + if (_showReleaseDate) + _buildFieldTile( + label: t.metadataEdit.releaseDate, + value: _originallyAvailableAt, + onTap: _editDate, + ), + if (_showContentRating) + _buildFieldTile( + label: t.metadataEdit.contentRating, + value: _contentRating, + onTap: () => _editTextField( + title: t.metadataEdit.contentRating, + label: t.metadataEdit.contentRating, + currentValue: _contentRating, + onChanged: (v) => _contentRating = v, + ), + ), + if (_showStudio) + _buildFieldTile( + label: t.metadataEdit.studio, + value: _studio, + onTap: () => _editTextField( + title: t.metadataEdit.studio, + label: t.metadataEdit.studio, + currentValue: _studio, + onChanged: (v) => _studio = v, + ), + ), + if (_showTagline) + _buildFieldTile( + label: t.metadataEdit.tagline, + value: _tagline, + onTap: () => _editTextField( + title: t.metadataEdit.tagline, + label: t.metadataEdit.tagline, + currentValue: _tagline, + onChanged: (v) => _tagline = v, + ), + ), + _buildFieldTile( + label: t.metadataEdit.summary, + value: _summary, + onTap: () => _editTextField( + title: t.metadataEdit.summary, + label: t.metadataEdit.summary, + currentValue: _summary, + onChanged: (v) => _summary = v, + multiline: true, + ), + ), + ], + ), + ); + } + + Widget _buildFieldTile({required String label, String? value, required VoidCallback onTap}) { + final displayValue = (value == null || value.isEmpty) ? t.metadataEdit.notSet : value; + final isNotSet = value == null || value.isEmpty; + + return ListTile( + title: Text(label), + subtitle: Text( + displayValue, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: isNotSet ? TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant.withValues(alpha: 0.5)) : null, + ), + trailing: const AppIcon(Symbols.chevron_right_rounded), + onTap: onTap, + ); + } + + Widget _buildArtworkCard() { + final meta = _fullMetadata ?? widget.metadata; + + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + t.metadataEdit.artwork, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + ListTile( + leading: SizedBox( + width: 40, + height: 60, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: PlexOptimizedImage( + client: _client, + imagePath: meta.thumb, + width: 40, + height: 60, + fit: BoxFit.cover, + ), + ), + ), + title: Text(t.metadataEdit.poster), + trailing: const AppIcon(Symbols.chevron_right_rounded), + onTap: () => _openArtworkPicker('posters'), + ), + if (_showBackground) + ListTile( + leading: SizedBox( + width: 80, + height: 45, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: PlexOptimizedImage( + client: _client, + imagePath: meta.art, + width: 80, + height: 45, + fit: BoxFit.cover, + ), + ), + ), + title: Text(t.metadataEdit.background), + trailing: const AppIcon(Symbols.chevron_right_rounded), + onTap: () => _openArtworkPicker('arts'), + ), + ], + ), + ); + } + + Widget _buildAdvancedSettingsCard() { + return Card( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.all(16), + child: Text( + t.metadataEdit.advancedSettings, + style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.bold), + ), + ), + if (_mediaType == PlexMediaType.show) ..._buildShowAdvancedSettings(), + if (_mediaType == PlexMediaType.movie) ..._buildMovieAdvancedSettings(), + if (_mediaType == PlexMediaType.season) ..._buildSeasonAdvancedSettings(), + ], + ), + ); + } + + List _buildShowAdvancedSettings() { + return [ + _buildAdvancedTile( + title: t.metadataEdit.episodeSorting, + prefKey: 'episodeSort', + options: [ + (value: '-1', label: t.metadataEdit.libraryDefault), + (value: '0', label: t.metadataEdit.oldestFirst), + (value: '1', label: t.metadataEdit.newestFirst), + ], + ), + _buildAdvancedTile( + title: t.metadataEdit.keep, + prefKey: 'autoDeletionItemPolicyUnwatchedLibrary', + options: [ + (value: '0', label: t.metadataEdit.allEpisodes), + (value: '5', label: t.metadataEdit.latestEpisodes(count: '5')), + (value: '3', label: t.metadataEdit.latestEpisodes(count: '3')), + (value: '1', label: t.metadataEdit.latestEpisode), + (value: '-3', label: t.metadataEdit.episodesAddedPastDays(count: '3')), + (value: '-7', label: t.metadataEdit.episodesAddedPastDays(count: '7')), + (value: '-30', label: t.metadataEdit.episodesAddedPastDays(count: '30')), + ], + ), + _buildAdvancedTile( + title: t.metadataEdit.deleteAfterPlaying, + prefKey: 'autoDeletionItemPolicyWatchedLibrary', + options: [ + (value: '0', label: t.metadataEdit.never), + (value: '1', label: t.metadataEdit.afterADay), + (value: '7', label: t.metadataEdit.afterAWeek), + (value: '30', label: t.metadataEdit.afterAMonth), + (value: '100', label: t.metadataEdit.onNextRefresh), + ], + ), + _buildAdvancedTile( + title: t.metadataEdit.seasons, + prefKey: 'flattenSeasons', + options: [ + (value: '-1', label: t.metadataEdit.libraryDefault), + (value: '0', label: t.metadataEdit.show), + (value: '1', label: t.metadataEdit.hide), + ], + ), + _buildAdvancedTile( + title: t.metadataEdit.episodeOrdering, + prefKey: 'showOrdering', + options: [ + (value: '', label: t.metadataEdit.libraryDefault), + (value: 'tmdbAiring', label: t.metadataEdit.tmdbAiring), + (value: 'tvdbAiring', label: t.metadataEdit.tvdbAiring), + (value: 'tvdbAbsolute', label: t.metadataEdit.tvdbAbsolute), + ], + ), + _buildAdvancedTile( + title: t.metadataEdit.metadataLanguage, + prefKey: 'languageOverride', + options: _metadataLanguageOptions(t.metadataEdit.libraryDefault), + ), + _buildAdvancedTile( + title: t.metadataEdit.useOriginalTitle, + prefKey: 'useOriginalTitle', + options: [ + (value: '-1', label: t.metadataEdit.libraryDefault), + (value: '0', label: t.common.no), + (value: '1', label: t.common.yes), + ], + ), + _buildAdvancedTile( + title: t.metadataEdit.preferredAudioLanguage, + prefKey: 'audioLanguage', + options: _audioSubtitleLanguageOptions(t.metadataEdit.accountDefault), + ), + _buildAdvancedTile( + title: t.metadataEdit.preferredSubtitleLanguage, + prefKey: 'subtitleLanguage', + options: _audioSubtitleLanguageOptions(t.metadataEdit.accountDefault), + ), + _buildAdvancedTile( + title: t.metadataEdit.subtitleMode, + prefKey: 'subtitleMode', + options: [ + (value: '-1', label: t.metadataEdit.accountDefault), + (value: '0', label: t.metadataEdit.manuallySelected), + (value: '1', label: t.metadataEdit.shownWithForeignAudio), + (value: '2', label: t.metadataEdit.alwaysEnabled), + ], + ), + ]; + } + + List _buildMovieAdvancedSettings() { + return [ + _buildAdvancedTile( + title: t.metadataEdit.metadataLanguage, + prefKey: 'languageOverride', + options: _metadataLanguageOptions(t.metadataEdit.libraryDefault), + ), + _buildAdvancedTile( + title: t.metadataEdit.useOriginalTitle, + prefKey: 'useOriginalTitle', + options: [ + (value: '-1', label: t.metadataEdit.libraryDefault), + (value: '0', label: t.common.no), + (value: '1', label: t.common.yes), + ], + ), + ]; + } + + List _buildSeasonAdvancedSettings() { + return [ + _buildAdvancedTile( + title: t.metadataEdit.preferredAudioLanguage, + prefKey: 'audioLanguage', + options: _audioSubtitleLanguageOptions(t.metadataEdit.seriesDefault), + ), + _buildAdvancedTile( + title: t.metadataEdit.preferredSubtitleLanguage, + prefKey: 'subtitleLanguage', + options: _audioSubtitleLanguageOptions(t.metadataEdit.seriesDefault), + ), + _buildAdvancedTile( + title: t.metadataEdit.subtitleMode, + prefKey: 'subtitleMode', + options: [ + (value: '-1', label: t.metadataEdit.seriesDefault), + (value: '0', label: t.metadataEdit.manuallySelected), + (value: '1', label: t.metadataEdit.shownWithForeignAudio), + (value: '2', label: t.metadataEdit.alwaysEnabled), + ], + ), + ]; + } + + Widget _buildAdvancedTile({ + required String title, + required String prefKey, + required List<({String value, String label})> options, + }) { + return ListTile( + title: Text(title), + subtitle: Text(_getDisplayValueForPref(prefKey, options)), + trailing: const AppIcon(Symbols.chevron_right_rounded), + onTap: () => _showAdvancedSettingDialog(title: title, prefKey: prefKey, options: options), + ); + } +} + +// ===== Language option lists ===== + +// Plex locale codes for metadata agent language. +const _plexLocaleCodes = [ + 'ar-SA', 'bg-BG', 'ca-ES', 'zh-CN', 'zh-HK', 'zh-TW', 'hr-HR', 'cs-CZ', + 'da-DK', 'nl-NL', 'en-US', 'en-AU', 'en-CA', 'en-GB', 'et-EE', 'fi-FI', + 'fr-FR', 'fr-CA', 'de-DE', 'el-GR', 'he-IL', 'hi-IN', 'hu-HU', 'is-IS', + 'id-ID', 'it-IT', 'ja-JP', 'ko-KR', 'lv-LV', 'lt-LT', 'nb-NO', 'fa-IR', + 'pl-PL', 'pt-BR', 'pt-PT', 'ro-RO', 'ru-RU', 'sk-SK', 'es-ES', 'es-MX', + 'sv-SE', 'th-TH', 'tr-TR', 'uk-UA', 'vi-VN', +]; + +// Common 2-letter codes shown at the top of audio/subtitle pickers. +const _commonAudioSubtitleCodes = ['en', 'ja', 'fr', 'de', 'it', 'es', 'pt', 'ru', 'ar']; + +List<({String value, String label})> _buildLanguageOptions(String defaultLabel, List codes) { + return [ + (value: '', label: defaultLabel), + ...codes.map((c) => (value: c, label: LanguageCodes.getDisplayName(c))), + ]; +} + +List<({String value, String label})> _metadataLanguageOptions(String defaultLabel) => + _buildLanguageOptions(defaultLabel, _plexLocaleCodes); + +List<({String value, String label})> _audioSubtitleLanguageOptions(String defaultLabel) => + _buildLanguageOptions(defaultLabel, [..._commonAudioSubtitleCodes, ..._plexLocaleCodes]); diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index 60dc92a0..75d85b74 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -1565,6 +1565,94 @@ class PlexClient { ); } + // ============================================================================ + // Metadata Editing Methods + // ============================================================================ + + /// Update metadata fields for a media item + Future updateMetadata({ + required int sectionId, + required String ratingKey, + required int typeNumber, + String? title, + String? titleSort, + String? originalTitle, + String? originallyAvailableAt, + String? contentRating, + String? studio, + String? tagline, + String? summary, + }) { + final queryParams = { + 'type': typeNumber, + 'id': ratingKey, + }; + + void addField(String name, String? value) { + if (value != null) { + queryParams['$name.value'] = value; + queryParams['$name.locked'] = '1'; + } + } + + addField('title', title); + addField('titleSort', titleSort); + addField('originalTitle', originalTitle); + addField('originallyAvailableAt', originallyAvailableAt); + addField('contentRating', contentRating); + addField('studio', studio); + addField('tagline', tagline); + addField('summary', summary); + + return _wrapBoolApiCall( + () => _dio.put('/library/sections/$sectionId/all', queryParameters: queryParams), + 'Failed to update metadata', + ); + } + + /// Get available artwork (posters or backgrounds) for a media item + Future>> getAvailableArtwork(String ratingKey, String element) async { + try { + final response = await _dio.get('/library/metadata/$ratingKey/$element'); + final container = _getMediaContainer(response); + if (container != null && container['Metadata'] != null) { + return (container['Metadata'] as List).cast>(); + } + return []; + } catch (e) { + appLogger.e('Failed to get available artwork', error: e); + return []; + } + } + + /// Set artwork from a URL (can be a Plex internal path or external URL) + Future setArtworkFromUrl(String ratingKey, String element, String url) { + return _wrapBoolApiCall( + () => _dio.post('/library/metadata/$ratingKey/$element', queryParameters: {'url': url}), + 'Failed to set artwork from URL', + ); + } + + /// Upload artwork from binary data + Future uploadArtwork(String ratingKey, String element, List bytes) { + return _wrapBoolApiCall( + () => _dio.post( + '/library/metadata/$ratingKey/$element', + data: bytes, + options: Options(headers: {'Content-Length': bytes.length}), + ), + 'Failed to upload artwork', + ); + } + + /// Update per-media advanced preferences + Future updateMetadataPrefs(String ratingKey, Map prefs) { + return _wrapBoolApiCall( + () => _dio.put('/library/metadata/$ratingKey/prefs', queryParameters: prefs), + 'Failed to update metadata preferences', + ); + } + // ============================================================================ // Collection Methods // ============================================================================ diff --git a/lib/utils/dialogs.dart b/lib/utils/dialogs.dart index d3a1c27d..02bb4efc 100644 --- a/lib/utils/dialogs.dart +++ b/lib/utils/dialogs.dart @@ -135,6 +135,81 @@ Future showTextInputDialog( ); } +/// Shows a multiline text input dialog for editing longer text like summaries. +/// Returns the entered text, or null if cancelled. +/// Allows empty text to be submitted (for clearing fields). +Future showMultilineTextInputDialog( + BuildContext context, { + required String title, + required String labelText, + String? initialValue, +}) { + return showDialog( + context: context, + builder: (context) => _MultilineTextInputDialog(title: title, labelText: labelText, initialValue: initialValue), + ); +} + +class _MultilineTextInputDialog extends StatefulWidget { + final String title; + final String labelText; + final String? initialValue; + + const _MultilineTextInputDialog({required this.title, required this.labelText, this.initialValue}); + + @override + State<_MultilineTextInputDialog> createState() => _MultilineTextInputDialogState(); +} + +class _MultilineTextInputDialogState extends State<_MultilineTextInputDialog> { + late final TextEditingController _controller; + final _saveFocusNode = FocusNode(); + + @override + void initState() { + super.initState(); + _controller = TextEditingController(text: widget.initialValue); + } + + @override + void dispose() { + _controller.dispose(); + _saveFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(widget.title), + content: SizedBox( + width: 400, + child: TextField( + controller: _controller, + autofocus: true, + decoration: InputDecoration(labelText: widget.labelText), + maxLines: 8, + minLines: 3, + ), + ), + actions: [ + FocusableButton( + onPressed: () => Navigator.pop(context), + child: TextButton(onPressed: () => Navigator.pop(context), child: Text(t.common.cancel)), + ), + FocusableButton( + focusNode: _saveFocusNode, + onPressed: () => Navigator.pop(context, _controller.text), + child: TextButton( + onPressed: () => Navigator.pop(context, _controller.text), + child: Text(t.common.save), + ), + ), + ], + ); + } +} + class _TextInputDialog extends StatefulWidget { final String title; final String labelText; diff --git a/lib/utils/language_codes.dart b/lib/utils/language_codes.dart index 5f2abab1..20fe3543 100644 --- a/lib/utils/language_codes.dart +++ b/lib/utils/language_codes.dart @@ -70,6 +70,38 @@ class LanguageCodes { return variations.toList(); } + /// Get a display name for a language/locale code. + /// Handles plain codes ("en" → "English") and locale codes ("en-US" → "English", + /// "en-AU" → "English (Australia)"). + /// Uses [_regionNames] for the country qualifier on non-primary variants. + static String getDisplayName(String code) { + if (code.isEmpty) return code; + + // Plain language code (e.g. "en", "ja") + if (!code.contains('-')) { + return getLanguageName(code) ?? code; + } + + // Locale code (e.g. "en-US", "zh-TW") + final parts = code.split('-'); + final langName = getLanguageName(parts[0]) ?? parts[0]; + final region = parts.length > 1 ? _regionNames[parts[1]] : null; + return region != null ? '$langName ($region)' : langName; + } + + // Region qualifiers for Plex locale codes that differ from the primary variant. + // Only entries where Plex shows a country qualifier in its UI are listed. + static const _regionNames = { + 'AU': 'Australia', + 'BR': 'Brazil', + 'CA': 'Canada', + 'GB': 'UK', + 'HK': 'Hong Kong', + 'MX': 'Mexico', + 'PT': 'Portugal', + 'TW': 'Taiwan', + }; + /// Get the English name of a language from its code static String? getLanguageName(String languageCode) { if (_codes == null) return null; diff --git a/lib/widgets/artwork_picker_dialog.dart b/lib/widgets/artwork_picker_dialog.dart new file mode 100644 index 00000000..3c011cf6 --- /dev/null +++ b/lib/widgets/artwork_picker_dialog.dart @@ -0,0 +1,226 @@ +import 'package:file_picker/file_picker.dart'; +import 'package:flutter/material.dart'; +import 'package:material_symbols_icons/symbols.dart'; +import '../focus/focusable_button.dart'; +import '../focus/focusable_wrapper.dart'; +import '../i18n/strings.g.dart'; +import '../services/plex_client.dart'; +import '../utils/dialogs.dart'; +import '../utils/snackbar_helper.dart'; +import '../widgets/app_icon.dart'; +import '../widgets/plex_optimized_image.dart'; + +class ArtworkPickerDialog extends StatefulWidget { + final PlexClient client; + final String ratingKey; + final String element; // "posters" or "arts" + + const ArtworkPickerDialog({ + super.key, + required this.client, + required this.ratingKey, + required this.element, + }); + + @override + State createState() => _ArtworkPickerDialogState(); +} + +class _ArtworkPickerDialogState extends State { + List>? _artworkList; + bool _isLoading = true; + bool _isApplying = false; + + bool get _isPosters => widget.element == 'posters'; + + @override + void initState() { + super.initState(); + _loadArtwork(); + } + + Future _loadArtwork() async { + final artwork = await widget.client.getAvailableArtwork(widget.ratingKey, widget.element); + if (!mounted) return; + setState(() { + _artworkList = artwork; + _isLoading = false; + }); + } + + Future _selectArtwork(Map artwork) async { + final key = artwork['key'] as String?; + if (key == null || _isApplying) return; + + setState(() => _isApplying = true); + + final success = await widget.client.setArtworkFromUrl(widget.ratingKey, widget.element, key); + + if (!mounted) return; + setState(() => _isApplying = false); + + if (success) { + showSuccessSnackBar(context, t.metadataEdit.artworkUpdated); + Navigator.pop(context, true); + } else { + showErrorSnackBar(context, t.metadataEdit.artworkUpdateFailed); + } + } + + Future _addFromUrl() async { + final url = await showTextInputDialog( + context, + title: t.metadataEdit.fromUrl, + labelText: t.metadataEdit.imageUrl, + hintText: t.metadataEdit.enterImageUrl, + ); + + if (url == null || url.isEmpty || !mounted) return; + + setState(() => _isApplying = true); + + final success = await widget.client.setArtworkFromUrl(widget.ratingKey, widget.element, url); + + if (!mounted) return; + setState(() => _isApplying = false); + + if (success) { + showSuccessSnackBar(context, t.metadataEdit.artworkUpdated); + Navigator.pop(context, true); + } else { + showErrorSnackBar(context, t.metadataEdit.artworkUpdateFailed); + } + } + + Future _uploadFile() async { + final result = await FilePicker.platform.pickFiles( + type: FileType.image, + withData: true, + ); + + if (result == null || result.files.isEmpty || !mounted) return; + + final bytes = result.files.first.bytes; + if (bytes == null) return; + + setState(() => _isApplying = true); + + final success = await widget.client.uploadArtwork(widget.ratingKey, widget.element, bytes); + + if (!mounted) return; + setState(() => _isApplying = false); + + if (success) { + showSuccessSnackBar(context, t.metadataEdit.artworkUpdated); + Navigator.pop(context, true); + } else { + showErrorSnackBar(context, t.metadataEdit.artworkUpdateFailed); + } + } + + @override + Widget build(BuildContext context) { + final title = _isPosters ? t.metadataEdit.selectPoster : t.metadataEdit.selectBackground; + + return AlertDialog( + title: Text(title), + content: SizedBox( + width: 500, + height: 400, + child: _isLoading + ? const Center(child: CircularProgressIndicator()) + : _artworkList == null || _artworkList!.isEmpty + ? Center(child: Text(t.metadataEdit.noArtworkAvailable)) + : _buildGrid(), + ), + actions: [ + if (_isApplying) + const Padding( + padding: EdgeInsets.all(8), + child: SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)), + ), + FocusableButton( + onPressed: _addFromUrl, + child: TextButton.icon( + onPressed: _addFromUrl, + icon: const AppIcon(Symbols.link_rounded, size: 18), + label: Text(t.metadataEdit.fromUrl), + ), + ), + FocusableButton( + onPressed: _uploadFile, + child: TextButton.icon( + onPressed: _uploadFile, + icon: const AppIcon(Symbols.upload_rounded, size: 18), + label: Text(t.metadataEdit.uploadFile), + ), + ), + FocusableButton( + autofocus: true, + onPressed: () => Navigator.pop(context), + child: TextButton( + onPressed: () => Navigator.pop(context), + child: Text(t.common.cancel), + ), + ), + ], + ); + } + + Widget _buildGrid() { + final crossAxisCount = _isPosters ? 3 : 2; + final aspectRatio = _isPosters ? 2.0 / 3.0 : 16.0 / 9.0; + + return GridView.builder( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: crossAxisCount, + crossAxisSpacing: 8, + mainAxisSpacing: 8, + childAspectRatio: aspectRatio, + ), + itemCount: _artworkList!.length, + itemBuilder: (context, index) { + final artwork = _artworkList![index]; + final thumbUrl = artwork['thumb'] as String?; + final isSelected = artwork['selected'] == true; + + return FocusableWrapper( + borderRadius: 8, + onSelect: () => _selectArtwork(artwork), + child: Stack( + fit: StackFit.expand, + children: [ + Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(8), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: PlexOptimizedImage( + client: widget.client, + imagePath: thumbUrl, + fit: BoxFit.contain, + ), + ), + ), + if (isSelected) + Positioned( + right: 6, + bottom: 6, + child: Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.primary, + shape: BoxShape.circle, + ), + child: Icon(Symbols.check_rounded, size: 16, color: Theme.of(context).colorScheme.onPrimary), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 4c19e6bc..768e9246 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -20,6 +20,7 @@ import '../services/external_player_service.dart'; import '../focus/focusable_button.dart'; import '../focus/dpad_navigator.dart'; import '../screens/media_detail_screen.dart'; +import '../screens/metadata_edit_screen.dart'; import '../screens/season_detail_screen.dart'; import '../utils/smart_deletion_handler.dart'; import '../utils/deletion_notifier.dart'; @@ -199,6 +200,16 @@ class MediaContextMenuState extends State { ); } + // Edit Metadata (for movies, shows, seasons, and episodes) + if (mediaType == PlexMediaType.movie || + mediaType == PlexMediaType.show || + mediaType == PlexMediaType.season || + mediaType == PlexMediaType.episode) { + menuActions.add( + _MenuAction(value: 'edit_metadata', icon: Symbols.edit_rounded, label: t.metadataEdit.editMetadata), + ); + } + // Remove from Collection (only when viewing items within a collection) if (widget.collectionId != null) { menuActions.add( @@ -415,6 +426,17 @@ class MediaContextMenuState extends State { } break; + case 'edit_metadata': + didNavigate = true; + if (context.mounted) { + await Navigator.push( + context, + MaterialPageRoute(builder: (context) => MetadataEditScreen(metadata: metadata!)), + ); + widget.onRefresh?.call(metadata!.ratingKey); + } + break; + case 'remove_from_collection': await _handleRemoveFromCollection(context, metadata!); break;