From 431c20f81754c9f992a807810a3b48b54aada0a2 Mon Sep 17 00:00:00 2001 From: edde746 <86283021+edde746@users.noreply.github.com> Date: Thu, 23 Apr 2026 19:02:14 +0200 Subject: [PATCH] feat: match/unmatch metadata --- lib/focus/focusable_text_field.dart | 91 ++++++++++ lib/i18n/da.i18n.json | 14 ++ lib/i18n/de.i18n.json | 14 ++ lib/i18n/en.i18n.json | 14 ++ lib/i18n/es.i18n.json | 14 ++ lib/i18n/fr.i18n.json | 14 ++ lib/i18n/it.i18n.json | 14 ++ lib/i18n/ja.i18n.json | 14 ++ lib/i18n/ko.i18n.json | 14 ++ lib/i18n/nb.i18n.json | 14 ++ lib/i18n/nl.i18n.json | 14 ++ lib/i18n/pl.i18n.json | 14 ++ lib/i18n/pt.i18n.json | 14 ++ lib/i18n/ru.i18n.json | 14 ++ lib/i18n/strings.g.dart | 4 +- lib/i18n/strings_da.g.dart | 34 ++++ lib/i18n/strings_de.g.dart | 34 ++++ lib/i18n/strings_en.g.dart | 58 +++++++ lib/i18n/strings_es.g.dart | 34 ++++ lib/i18n/strings_fr.g.dart | 34 ++++ lib/i18n/strings_it.g.dart | 34 ++++ lib/i18n/strings_ja.g.dart | 34 ++++ lib/i18n/strings_ko.g.dart | 34 ++++ lib/i18n/strings_nb.g.dart | 34 ++++ lib/i18n/strings_nl.g.dart | 34 ++++ lib/i18n/strings_pl.g.dart | 34 ++++ lib/i18n/strings_pt.g.dart | 34 ++++ lib/i18n/strings_ru.g.dart | 34 ++++ lib/i18n/strings_sv.g.dart | 34 ++++ lib/i18n/strings_zh.g.dart | 34 ++++ lib/i18n/sv.i18n.json | 14 ++ lib/i18n/zh.i18n.json | 14 ++ lib/models/plex_match_result.dart | 36 ++++ lib/models/plex_metadata.dart | 8 + lib/screens/match_screen.dart | 249 ++++++++++++++++++++++++++++ lib/services/plex_client.dart | 55 ++++++ lib/widgets/media_context_menu.dart | 53 ++++++ 37 files changed, 1238 insertions(+), 2 deletions(-) create mode 100644 lib/focus/focusable_text_field.dart create mode 100644 lib/models/plex_match_result.dart create mode 100644 lib/screens/match_screen.dart diff --git a/lib/focus/focusable_text_field.dart b/lib/focus/focusable_text_field.dart new file mode 100644 index 00000000..202fefdd --- /dev/null +++ b/lib/focus/focusable_text_field.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import 'dpad_navigator.dart'; + +/// A [TextField] wrapper that exposes D-pad navigation callbacks with +/// caret-aware edge escapes — so LEFT at the start of the field and RIGHT +/// at the end escape to neighbouring focus targets instead of bouncing +/// against the caret boundary, while UP/DOWN always escape. +/// +/// Collapsed selection only: if text is selected, LEFT/RIGHT fall through +/// to the TextField's default caret movement. +class FocusableTextField extends StatelessWidget { + final TextEditingController controller; + final FocusNode? focusNode; + final InputDecoration? decoration; + final TextInputType? keyboardType; + final TextInputAction? textInputAction; + final List? inputFormatters; + final ValueChanged? onChanged; + final ValueChanged? onSubmitted; + final bool autofocus; + + final VoidCallback? onNavigateLeft; + final VoidCallback? onNavigateRight; + final VoidCallback? onNavigateUp; + final VoidCallback? onNavigateDown; + + const FocusableTextField({ + super.key, + required this.controller, + this.focusNode, + this.decoration, + this.keyboardType, + this.textInputAction, + this.inputFormatters, + this.onChanged, + this.onSubmitted, + this.autofocus = false, + this.onNavigateLeft, + this.onNavigateRight, + this.onNavigateUp, + this.onNavigateDown, + }); + + KeyEventResult _handleKey(FocusNode _, KeyEvent event) { + if (!event.isActionable) return KeyEventResult.ignored; + final key = event.logicalKey; + + if (key.isUpKey && onNavigateUp != null) { + onNavigateUp!(); + return KeyEventResult.handled; + } + if (key.isDownKey && onNavigateDown != null) { + onNavigateDown!(); + return KeyEventResult.handled; + } + + final sel = controller.selection; + if (sel.isCollapsed) { + if (key.isLeftKey && sel.baseOffset == 0 && onNavigateLeft != null) { + onNavigateLeft!(); + return KeyEventResult.handled; + } + if (key.isRightKey && sel.baseOffset == controller.text.length && onNavigateRight != null) { + onNavigateRight!(); + return KeyEventResult.handled; + } + } + + return KeyEventResult.ignored; + } + + @override + Widget build(BuildContext context) { + return Focus( + onKeyEvent: _handleKey, + child: TextField( + controller: controller, + focusNode: focusNode, + decoration: decoration, + keyboardType: keyboardType, + textInputAction: textInputAction, + inputFormatters: inputFormatters, + onChanged: onChanged, + onSubmitted: onSubmitted, + autofocus: autofocus, + ), + ); + } +} diff --git a/lib/i18n/da.i18n.json b/lib/i18n/da.i18n.json index 46989ab2..137b6208 100644 --- a/lib/i18n/da.i18n.json +++ b/lib/i18n/da.i18n.json @@ -986,6 +986,20 @@ "style": "Stil", "mood": "Stemning" }, + "matchScreen": { + "match": "Match...", + "fixMatch": "Ret match...", + "unmatch": "Fjern match", + "unmatchConfirm": "Ryd det aktuelle match for dette element? Plex behandler det som ikke matchet, indtil du matcher det igen.", + "unmatchSuccess": "Match fjernet", + "unmatchFailed": "Kunne ikke fjerne match", + "matchApplied": "Match anvendt", + "matchFailed": "Kunne ikke anvende match", + "titleHint": "Titel", + "yearHint": "År", + "search": "Søg", + "noMatchesFound": "Ingen match fundet" + }, "serverTasks": { "title": "Serveropgaver", "failedToLoad": "Kunne ikke indlæse opgaver", diff --git a/lib/i18n/de.i18n.json b/lib/i18n/de.i18n.json index 95337579..4b9a1cbd 100644 --- a/lib/i18n/de.i18n.json +++ b/lib/i18n/de.i18n.json @@ -986,6 +986,20 @@ "style": "Stil", "mood": "Stimmung" }, + "matchScreen": { + "match": "Zuordnen...", + "fixMatch": "Zuordnung korrigieren...", + "unmatch": "Zuordnung aufheben", + "unmatchConfirm": "Die aktuelle Zuordnung dieses Eintrags entfernen? Plex behandelt ihn als nicht zugeordnet, bis du ihn erneut zuordnest.", + "unmatchSuccess": "Zuordnung aufgehoben", + "unmatchFailed": "Zuordnung konnte nicht aufgehoben werden", + "matchApplied": "Zuordnung angewendet", + "matchFailed": "Zuordnung konnte nicht angewendet werden", + "titleHint": "Titel", + "yearHint": "Jahr", + "search": "Suchen", + "noMatchesFound": "Keine Treffer gefunden" + }, "serverTasks": { "title": "Serveraufgaben", "failedToLoad": "Aufgaben konnten nicht geladen werden", diff --git a/lib/i18n/en.i18n.json b/lib/i18n/en.i18n.json index e7efca03..109d63a2 100644 --- a/lib/i18n/en.i18n.json +++ b/lib/i18n/en.i18n.json @@ -986,6 +986,20 @@ "style": "Style", "mood": "Mood" }, + "matchScreen": { + "match": "Match...", + "fixMatch": "Fix Match...", + "unmatch": "Unmatch", + "unmatchConfirm": "Clear the current match for this item? Plex will treat it as unmatched until you match it again.", + "unmatchSuccess": "Item unmatched", + "unmatchFailed": "Failed to unmatch item", + "matchApplied": "Match applied", + "matchFailed": "Failed to apply match", + "titleHint": "Title", + "yearHint": "Year", + "search": "Search", + "noMatchesFound": "No matches found" + }, "serverTasks": { "title": "Server Tasks", "failedToLoad": "Failed to load tasks", diff --git a/lib/i18n/es.i18n.json b/lib/i18n/es.i18n.json index c3144fd8..88fc9474 100644 --- a/lib/i18n/es.i18n.json +++ b/lib/i18n/es.i18n.json @@ -986,6 +986,20 @@ "style": "Estilo", "mood": "Estado de ánimo" }, + "matchScreen": { + "match": "Emparejar...", + "fixMatch": "Corregir coincidencia...", + "unmatch": "Desvincular", + "unmatchConfirm": "¿Quitar la coincidencia actual de este elemento? Plex lo tratará como sin coincidencia hasta que vuelvas a emparejarlo.", + "unmatchSuccess": "Elemento desvinculado", + "unmatchFailed": "No se pudo desvincular el elemento", + "matchApplied": "Coincidencia aplicada", + "matchFailed": "No se pudo aplicar la coincidencia", + "titleHint": "Título", + "yearHint": "Año", + "search": "Buscar", + "noMatchesFound": "No se encontraron coincidencias" + }, "serverTasks": { "title": "Tareas del servidor", "failedToLoad": "Error al cargar tareas", diff --git a/lib/i18n/fr.i18n.json b/lib/i18n/fr.i18n.json index 3e81f25e..74bce1b9 100644 --- a/lib/i18n/fr.i18n.json +++ b/lib/i18n/fr.i18n.json @@ -986,6 +986,20 @@ "style": "Style", "mood": "Ambiance" }, + "matchScreen": { + "match": "Associer...", + "fixMatch": "Corriger l'association...", + "unmatch": "Dissocier", + "unmatchConfirm": "Effacer l'association actuelle de cet élément ? Plex le traitera comme non associé jusqu'à ce que vous l'associiez de nouveau.", + "unmatchSuccess": "Association supprimée", + "unmatchFailed": "Échec de la dissociation", + "matchApplied": "Association appliquée", + "matchFailed": "Échec de l'application", + "titleHint": "Titre", + "yearHint": "Année", + "search": "Rechercher", + "noMatchesFound": "Aucune correspondance" + }, "serverTasks": { "title": "Tâches du serveur", "failedToLoad": "Échec du chargement des tâches", diff --git a/lib/i18n/it.i18n.json b/lib/i18n/it.i18n.json index dc23e841..80a7e2b9 100644 --- a/lib/i18n/it.i18n.json +++ b/lib/i18n/it.i18n.json @@ -986,6 +986,20 @@ "style": "Stile", "mood": "Atmosfera" }, + "matchScreen": { + "match": "Abbina...", + "fixMatch": "Correggi abbinamento...", + "unmatch": "Rimuovi abbinamento", + "unmatchConfirm": "Cancellare l'abbinamento corrente di questo elemento? Plex lo tratterà come non abbinato finché non lo abbinerai di nuovo.", + "unmatchSuccess": "Abbinamento rimosso", + "unmatchFailed": "Rimozione dell'abbinamento non riuscita", + "matchApplied": "Abbinamento applicato", + "matchFailed": "Applicazione dell'abbinamento non riuscita", + "titleHint": "Titolo", + "yearHint": "Anno", + "search": "Cerca", + "noMatchesFound": "Nessun risultato" + }, "serverTasks": { "title": "Attività del server", "failedToLoad": "Impossibile caricare le attività", diff --git a/lib/i18n/ja.i18n.json b/lib/i18n/ja.i18n.json index e268293d..13c9389c 100644 --- a/lib/i18n/ja.i18n.json +++ b/lib/i18n/ja.i18n.json @@ -986,6 +986,20 @@ "style": "スタイル", "mood": "ムード" }, + "matchScreen": { + "match": "マッチ...", + "fixMatch": "マッチを修正...", + "unmatch": "マッチ解除", + "unmatchConfirm": "このアイテムの現在のマッチを解除しますか?再度マッチするまで、Plex は未マッチとして扱います。", + "unmatchSuccess": "マッチを解除しました", + "unmatchFailed": "マッチの解除に失敗しました", + "matchApplied": "マッチを適用しました", + "matchFailed": "マッチの適用に失敗しました", + "titleHint": "タイトル", + "yearHint": "年", + "search": "検索", + "noMatchesFound": "一致する項目がありません" + }, "serverTasks": { "title": "サーバータスク", "failedToLoad": "タスクの読み込みに失敗しました", diff --git a/lib/i18n/ko.i18n.json b/lib/i18n/ko.i18n.json index dc76a174..61267db7 100644 --- a/lib/i18n/ko.i18n.json +++ b/lib/i18n/ko.i18n.json @@ -986,6 +986,20 @@ "style": "스타일", "mood": "분위기" }, + "matchScreen": { + "match": "일치...", + "fixMatch": "일치 수정...", + "unmatch": "일치 해제", + "unmatchConfirm": "이 항목의 현재 일치를 해제하시겠습니까? 다시 일치시킬 때까지 Plex는 일치되지 않은 것으로 처리합니다.", + "unmatchSuccess": "일치 해제됨", + "unmatchFailed": "일치 해제에 실패했습니다", + "matchApplied": "일치 적용됨", + "matchFailed": "일치 적용에 실패했습니다", + "titleHint": "제목", + "yearHint": "연도", + "search": "검색", + "noMatchesFound": "일치하는 항목이 없습니다" + }, "serverTasks": { "title": "서버 작업", "failedToLoad": "작업을 불러올 수 없습니다", diff --git a/lib/i18n/nb.i18n.json b/lib/i18n/nb.i18n.json index e35e926f..612d5f2d 100644 --- a/lib/i18n/nb.i18n.json +++ b/lib/i18n/nb.i18n.json @@ -986,6 +986,20 @@ "style": "Stil", "mood": "Stemning" }, + "matchScreen": { + "match": "Match...", + "fixMatch": "Rett match...", + "unmatch": "Fjern match", + "unmatchConfirm": "Fjerne gjeldende match for dette elementet? Plex behandler det som umatchet til du matcher det igjen.", + "unmatchSuccess": "Match fjernet", + "unmatchFailed": "Kunne ikke fjerne match", + "matchApplied": "Match anvendt", + "matchFailed": "Kunne ikke anvende match", + "titleHint": "Tittel", + "yearHint": "År", + "search": "Søk", + "noMatchesFound": "Ingen treff funnet" + }, "serverTasks": { "title": "Serveroppgaver", "failedToLoad": "Kunne ikke laste oppgaver", diff --git a/lib/i18n/nl.i18n.json b/lib/i18n/nl.i18n.json index 85dd499f..36a36204 100644 --- a/lib/i18n/nl.i18n.json +++ b/lib/i18n/nl.i18n.json @@ -986,6 +986,20 @@ "style": "Stijl", "mood": "Stemming" }, + "matchScreen": { + "match": "Koppelen...", + "fixMatch": "Koppeling herstellen...", + "unmatch": "Ontkoppelen", + "unmatchConfirm": "De huidige koppeling voor dit item wissen? Plex behandelt het als ongekoppeld totdat je het opnieuw koppelt.", + "unmatchSuccess": "Item ontkoppeld", + "unmatchFailed": "Kon item niet ontkoppelen", + "matchApplied": "Koppeling toegepast", + "matchFailed": "Koppeling kon niet worden toegepast", + "titleHint": "Titel", + "yearHint": "Jaar", + "search": "Zoeken", + "noMatchesFound": "Geen overeenkomsten gevonden" + }, "serverTasks": { "title": "Servertaken", "failedToLoad": "Taken konden niet worden geladen", diff --git a/lib/i18n/pl.i18n.json b/lib/i18n/pl.i18n.json index 44164dce..55fd7c94 100644 --- a/lib/i18n/pl.i18n.json +++ b/lib/i18n/pl.i18n.json @@ -986,6 +986,20 @@ "style": "Styl", "mood": "Nastrój" }, + "matchScreen": { + "match": "Dopasuj...", + "fixMatch": "Popraw dopasowanie...", + "unmatch": "Usuń dopasowanie", + "unmatchConfirm": "Usunąć obecne dopasowanie tego elementu? Plex będzie traktował go jako niedopasowany, dopóki nie dopasujesz go ponownie.", + "unmatchSuccess": "Dopasowanie usunięte", + "unmatchFailed": "Nie udało się usunąć dopasowania", + "matchApplied": "Dopasowanie zastosowane", + "matchFailed": "Nie udało się zastosować dopasowania", + "titleHint": "Tytuł", + "yearHint": "Rok", + "search": "Szukaj", + "noMatchesFound": "Nie znaleziono dopasowań" + }, "serverTasks": { "title": "Zadania serwera", "failedToLoad": "Nie udało się załadować zadań", diff --git a/lib/i18n/pt.i18n.json b/lib/i18n/pt.i18n.json index 7a2308cd..586bd737 100644 --- a/lib/i18n/pt.i18n.json +++ b/lib/i18n/pt.i18n.json @@ -986,6 +986,20 @@ "style": "Estilo", "mood": "Humor" }, + "matchScreen": { + "match": "Associar...", + "fixMatch": "Corrigir correspondência...", + "unmatch": "Desassociar", + "unmatchConfirm": "Limpar a correspondência atual deste item? O Plex vai tratá-lo como não correspondido até que o associe novamente.", + "unmatchSuccess": "Item desassociado", + "unmatchFailed": "Falha ao desassociar item", + "matchApplied": "Correspondência aplicada", + "matchFailed": "Falha ao aplicar correspondência", + "titleHint": "Título", + "yearHint": "Ano", + "search": "Pesquisar", + "noMatchesFound": "Nenhuma correspondência encontrada" + }, "serverTasks": { "title": "Tarefas do servidor", "failedToLoad": "Falha ao carregar tarefas", diff --git a/lib/i18n/ru.i18n.json b/lib/i18n/ru.i18n.json index 77194888..71fae8b1 100644 --- a/lib/i18n/ru.i18n.json +++ b/lib/i18n/ru.i18n.json @@ -986,6 +986,20 @@ "style": "Стиль", "mood": "Настроение" }, + "matchScreen": { + "match": "Сопоставить...", + "fixMatch": "Исправить сопоставление...", + "unmatch": "Сбросить сопоставление", + "unmatchConfirm": "Сбросить текущее сопоставление этого элемента? Plex будет считать его несопоставленным, пока вы не сопоставите его снова.", + "unmatchSuccess": "Сопоставление сброшено", + "unmatchFailed": "Не удалось сбросить сопоставление", + "matchApplied": "Сопоставление применено", + "matchFailed": "Не удалось применить сопоставление", + "titleHint": "Название", + "yearHint": "Год", + "search": "Поиск", + "noMatchesFound": "Совпадений не найдено" + }, "serverTasks": { "title": "Задачи сервера", "failedToLoad": "Не удалось загрузить задачи", diff --git a/lib/i18n/strings.g.dart b/lib/i18n/strings.g.dart index 9a0f99d0..71531e0c 100644 --- a/lib/i18n/strings.g.dart +++ b/lib/i18n/strings.g.dart @@ -4,9 +4,9 @@ /// To regenerate, run: `dart run slang` /// /// Locales: 15 -/// Strings: 13755 (917 per locale) +/// Strings: 13935 (929 per locale) /// -/// Built on 2026-04-23 at 15:55 UTC +/// Built on 2026-04-23 at 16:38 UTC // coverage:ignore-file // ignore_for_file: type=lint, unused_import diff --git a/lib/i18n/strings_da.g.dart b/lib/i18n/strings_da.g.dart index 02af12df..bef3a56e 100644 --- a/lib/i18n/strings_da.g.dart +++ b/lib/i18n/strings_da.g.dart @@ -74,6 +74,7 @@ class TranslationsDa with BaseTranslations implements T @override late final _TranslationsVideoSettingsDa videoSettings = _TranslationsVideoSettingsDa._(_root); @override late final _TranslationsExternalPlayerDa externalPlayer = _TranslationsExternalPlayerDa._(_root); @override late final _TranslationsMetadataEditDa metadataEdit = _TranslationsMetadataEditDa._(_root); + @override late final _TranslationsMatchScreenDa matchScreen = _TranslationsMatchScreenDa._(_root); @override late final _TranslationsServerTasksDa serverTasks = _TranslationsServerTasksDa._(_root); @override late final _TranslationsTraktDa trakt = _TranslationsTraktDa._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditDa implements TranslationsMetadataEditEn { @override String get mood => 'Stemning'; } +// Path: matchScreen +class _TranslationsMatchScreenDa implements TranslationsMatchScreenEn { + _TranslationsMatchScreenDa._(this._root); + + final TranslationsDa _root; // ignore: unused_field + + // Translations + @override String get match => 'Match...'; + @override String get fixMatch => 'Ret match...'; + @override String get unmatch => 'Fjern match'; + @override String get unmatchConfirm => 'Ryd det aktuelle match for dette element? Plex behandler det som ikke matchet, indtil du matcher det igen.'; + @override String get unmatchSuccess => 'Match fjernet'; + @override String get unmatchFailed => 'Kunne ikke fjerne match'; + @override String get matchApplied => 'Match anvendt'; + @override String get matchFailed => 'Kunne ikke anvende match'; + @override String get titleHint => 'Titel'; + @override String get yearHint => 'År'; + @override String get search => 'Søg'; + @override String get noMatchesFound => 'Ingen match fundet'; +} + // Path: serverTasks class _TranslationsServerTasksDa implements TranslationsServerTasksEn { _TranslationsServerTasksDa._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsDa { 'metadataEdit.label' => 'Etiket', 'metadataEdit.style' => 'Stil', 'metadataEdit.mood' => 'Stemning', + 'matchScreen.match' => 'Match...', + 'matchScreen.fixMatch' => 'Ret match...', + 'matchScreen.unmatch' => 'Fjern match', + 'matchScreen.unmatchConfirm' => 'Ryd det aktuelle match for dette element? Plex behandler det som ikke matchet, indtil du matcher det igen.', + 'matchScreen.unmatchSuccess' => 'Match fjernet', + 'matchScreen.unmatchFailed' => 'Kunne ikke fjerne match', + 'matchScreen.matchApplied' => 'Match anvendt', + 'matchScreen.matchFailed' => 'Kunne ikke anvende match', + 'matchScreen.titleHint' => 'Titel', + 'matchScreen.yearHint' => 'År', + 'matchScreen.search' => 'Søg', + 'matchScreen.noMatchesFound' => 'Ingen match fundet', 'serverTasks.title' => 'Serveropgaver', 'serverTasks.failedToLoad' => 'Kunne ikke indlæse opgaver', 'serverTasks.noTasks' => 'Ingen opgaver kører', diff --git a/lib/i18n/strings_de.g.dart b/lib/i18n/strings_de.g.dart index becdb444..aca6cf0a 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 _TranslationsVideoSettingsDe videoSettings = _TranslationsVideoSettingsDe._(_root); @override late final _TranslationsExternalPlayerDe externalPlayer = _TranslationsExternalPlayerDe._(_root); @override late final _TranslationsMetadataEditDe metadataEdit = _TranslationsMetadataEditDe._(_root); + @override late final _TranslationsMatchScreenDe matchScreen = _TranslationsMatchScreenDe._(_root); @override late final _TranslationsServerTasksDe serverTasks = _TranslationsServerTasksDe._(_root); @override late final _TranslationsTraktDe trakt = _TranslationsTraktDe._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditDe implements TranslationsMetadataEditEn { @override String get mood => 'Stimmung'; } +// Path: matchScreen +class _TranslationsMatchScreenDe implements TranslationsMatchScreenEn { + _TranslationsMatchScreenDe._(this._root); + + final TranslationsDe _root; // ignore: unused_field + + // Translations + @override String get match => 'Zuordnen...'; + @override String get fixMatch => 'Zuordnung korrigieren...'; + @override String get unmatch => 'Zuordnung aufheben'; + @override String get unmatchConfirm => 'Die aktuelle Zuordnung dieses Eintrags entfernen? Plex behandelt ihn als nicht zugeordnet, bis du ihn erneut zuordnest.'; + @override String get unmatchSuccess => 'Zuordnung aufgehoben'; + @override String get unmatchFailed => 'Zuordnung konnte nicht aufgehoben werden'; + @override String get matchApplied => 'Zuordnung angewendet'; + @override String get matchFailed => 'Zuordnung konnte nicht angewendet werden'; + @override String get titleHint => 'Titel'; + @override String get yearHint => 'Jahr'; + @override String get search => 'Suchen'; + @override String get noMatchesFound => 'Keine Treffer gefunden'; +} + // Path: serverTasks class _TranslationsServerTasksDe implements TranslationsServerTasksEn { _TranslationsServerTasksDe._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsDe { 'metadataEdit.label' => 'Label', 'metadataEdit.style' => 'Stil', 'metadataEdit.mood' => 'Stimmung', + 'matchScreen.match' => 'Zuordnen...', + 'matchScreen.fixMatch' => 'Zuordnung korrigieren...', + 'matchScreen.unmatch' => 'Zuordnung aufheben', + 'matchScreen.unmatchConfirm' => 'Die aktuelle Zuordnung dieses Eintrags entfernen? Plex behandelt ihn als nicht zugeordnet, bis du ihn erneut zuordnest.', + 'matchScreen.unmatchSuccess' => 'Zuordnung aufgehoben', + 'matchScreen.unmatchFailed' => 'Zuordnung konnte nicht aufgehoben werden', + 'matchScreen.matchApplied' => 'Zuordnung angewendet', + 'matchScreen.matchFailed' => 'Zuordnung konnte nicht angewendet werden', + 'matchScreen.titleHint' => 'Titel', + 'matchScreen.yearHint' => 'Jahr', + 'matchScreen.search' => 'Suchen', + 'matchScreen.noMatchesFound' => 'Keine Treffer gefunden', 'serverTasks.title' => 'Serveraufgaben', 'serverTasks.failedToLoad' => 'Aufgaben konnten nicht geladen werden', 'serverTasks.noTasks' => 'Keine laufenden Aufgaben', diff --git a/lib/i18n/strings_en.g.dart b/lib/i18n/strings_en.g.dart index 96807f62..e851ce2c 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 TranslationsVideoSettingsEn videoSettings = TranslationsVideoSettingsEn._(_root); late final TranslationsExternalPlayerEn externalPlayer = TranslationsExternalPlayerEn._(_root); late final TranslationsMetadataEditEn metadataEdit = TranslationsMetadataEditEn._(_root); + late final TranslationsMatchScreenEn matchScreen = TranslationsMatchScreenEn._(_root); late final TranslationsServerTasksEn serverTasks = TranslationsServerTasksEn._(_root); late final TranslationsTraktEn trakt = TranslationsTraktEn._(_root); } @@ -2843,6 +2844,51 @@ class TranslationsMetadataEditEn { String get mood => 'Mood'; } +// Path: matchScreen +class TranslationsMatchScreenEn { + TranslationsMatchScreenEn._(this._root); + + final Translations _root; // ignore: unused_field + + // Translations + + /// en: 'Match...' + String get match => 'Match...'; + + /// en: 'Fix Match...' + String get fixMatch => 'Fix Match...'; + + /// en: 'Unmatch' + String get unmatch => 'Unmatch'; + + /// en: 'Clear the current match for this item? Plex will treat it as unmatched until you match it again.' + String get unmatchConfirm => 'Clear the current match for this item? Plex will treat it as unmatched until you match it again.'; + + /// en: 'Item unmatched' + String get unmatchSuccess => 'Item unmatched'; + + /// en: 'Failed to unmatch item' + String get unmatchFailed => 'Failed to unmatch item'; + + /// en: 'Match applied' + String get matchApplied => 'Match applied'; + + /// en: 'Failed to apply match' + String get matchFailed => 'Failed to apply match'; + + /// en: 'Title' + String get titleHint => 'Title'; + + /// en: 'Year' + String get yearHint => 'Year'; + + /// en: 'Search' + String get search => 'Search'; + + /// en: 'No matches found' + String get noMatchesFound => 'No matches found'; +} + // Path: serverTasks class TranslationsServerTasksEn { TranslationsServerTasksEn._(this._root); @@ -4166,6 +4212,18 @@ extension on Translations { 'metadataEdit.label' => 'Label', 'metadataEdit.style' => 'Style', 'metadataEdit.mood' => 'Mood', + 'matchScreen.match' => 'Match...', + 'matchScreen.fixMatch' => 'Fix Match...', + 'matchScreen.unmatch' => 'Unmatch', + 'matchScreen.unmatchConfirm' => 'Clear the current match for this item? Plex will treat it as unmatched until you match it again.', + 'matchScreen.unmatchSuccess' => 'Item unmatched', + 'matchScreen.unmatchFailed' => 'Failed to unmatch item', + 'matchScreen.matchApplied' => 'Match applied', + 'matchScreen.matchFailed' => 'Failed to apply match', + 'matchScreen.titleHint' => 'Title', + 'matchScreen.yearHint' => 'Year', + 'matchScreen.search' => 'Search', + 'matchScreen.noMatchesFound' => 'No matches found', 'serverTasks.title' => 'Server Tasks', 'serverTasks.failedToLoad' => 'Failed to load tasks', 'serverTasks.noTasks' => 'No tasks running', diff --git a/lib/i18n/strings_es.g.dart b/lib/i18n/strings_es.g.dart index 805147ed..591cf2f2 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 _TranslationsVideoSettingsEs videoSettings = _TranslationsVideoSettingsEs._(_root); @override late final _TranslationsExternalPlayerEs externalPlayer = _TranslationsExternalPlayerEs._(_root); @override late final _TranslationsMetadataEditEs metadataEdit = _TranslationsMetadataEditEs._(_root); + @override late final _TranslationsMatchScreenEs matchScreen = _TranslationsMatchScreenEs._(_root); @override late final _TranslationsServerTasksEs serverTasks = _TranslationsServerTasksEs._(_root); @override late final _TranslationsTraktEs trakt = _TranslationsTraktEs._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditEs implements TranslationsMetadataEditEn { @override String get mood => 'Estado de ánimo'; } +// Path: matchScreen +class _TranslationsMatchScreenEs implements TranslationsMatchScreenEn { + _TranslationsMatchScreenEs._(this._root); + + final TranslationsEs _root; // ignore: unused_field + + // Translations + @override String get match => 'Emparejar...'; + @override String get fixMatch => 'Corregir coincidencia...'; + @override String get unmatch => 'Desvincular'; + @override String get unmatchConfirm => '¿Quitar la coincidencia actual de este elemento? Plex lo tratará como sin coincidencia hasta que vuelvas a emparejarlo.'; + @override String get unmatchSuccess => 'Elemento desvinculado'; + @override String get unmatchFailed => 'No se pudo desvincular el elemento'; + @override String get matchApplied => 'Coincidencia aplicada'; + @override String get matchFailed => 'No se pudo aplicar la coincidencia'; + @override String get titleHint => 'Título'; + @override String get yearHint => 'Año'; + @override String get search => 'Buscar'; + @override String get noMatchesFound => 'No se encontraron coincidencias'; +} + // Path: serverTasks class _TranslationsServerTasksEs implements TranslationsServerTasksEn { _TranslationsServerTasksEs._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsEs { 'metadataEdit.label' => 'Etiqueta', 'metadataEdit.style' => 'Estilo', 'metadataEdit.mood' => 'Estado de ánimo', + 'matchScreen.match' => 'Emparejar...', + 'matchScreen.fixMatch' => 'Corregir coincidencia...', + 'matchScreen.unmatch' => 'Desvincular', + 'matchScreen.unmatchConfirm' => '¿Quitar la coincidencia actual de este elemento? Plex lo tratará como sin coincidencia hasta que vuelvas a emparejarlo.', + 'matchScreen.unmatchSuccess' => 'Elemento desvinculado', + 'matchScreen.unmatchFailed' => 'No se pudo desvincular el elemento', + 'matchScreen.matchApplied' => 'Coincidencia aplicada', + 'matchScreen.matchFailed' => 'No se pudo aplicar la coincidencia', + 'matchScreen.titleHint' => 'Título', + 'matchScreen.yearHint' => 'Año', + 'matchScreen.search' => 'Buscar', + 'matchScreen.noMatchesFound' => 'No se encontraron coincidencias', 'serverTasks.title' => 'Tareas del servidor', 'serverTasks.failedToLoad' => 'Error al cargar tareas', 'serverTasks.noTasks' => 'No hay tareas en ejecución', diff --git a/lib/i18n/strings_fr.g.dart b/lib/i18n/strings_fr.g.dart index b8cd8bbb..a5e145fe 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 _TranslationsVideoSettingsFr videoSettings = _TranslationsVideoSettingsFr._(_root); @override late final _TranslationsExternalPlayerFr externalPlayer = _TranslationsExternalPlayerFr._(_root); @override late final _TranslationsMetadataEditFr metadataEdit = _TranslationsMetadataEditFr._(_root); + @override late final _TranslationsMatchScreenFr matchScreen = _TranslationsMatchScreenFr._(_root); @override late final _TranslationsServerTasksFr serverTasks = _TranslationsServerTasksFr._(_root); @override late final _TranslationsTraktFr trakt = _TranslationsTraktFr._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditFr implements TranslationsMetadataEditEn { @override String get mood => 'Ambiance'; } +// Path: matchScreen +class _TranslationsMatchScreenFr implements TranslationsMatchScreenEn { + _TranslationsMatchScreenFr._(this._root); + + final TranslationsFr _root; // ignore: unused_field + + // Translations + @override String get match => 'Associer...'; + @override String get fixMatch => 'Corriger l\'association...'; + @override String get unmatch => 'Dissocier'; + @override String get unmatchConfirm => 'Effacer l\'association actuelle de cet élément ? Plex le traitera comme non associé jusqu\'à ce que vous l\'associiez de nouveau.'; + @override String get unmatchSuccess => 'Association supprimée'; + @override String get unmatchFailed => 'Échec de la dissociation'; + @override String get matchApplied => 'Association appliquée'; + @override String get matchFailed => 'Échec de l\'application'; + @override String get titleHint => 'Titre'; + @override String get yearHint => 'Année'; + @override String get search => 'Rechercher'; + @override String get noMatchesFound => 'Aucune correspondance'; +} + // Path: serverTasks class _TranslationsServerTasksFr implements TranslationsServerTasksEn { _TranslationsServerTasksFr._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsFr { 'metadataEdit.label' => 'Label', 'metadataEdit.style' => 'Style', 'metadataEdit.mood' => 'Ambiance', + 'matchScreen.match' => 'Associer...', + 'matchScreen.fixMatch' => 'Corriger l\'association...', + 'matchScreen.unmatch' => 'Dissocier', + 'matchScreen.unmatchConfirm' => 'Effacer l\'association actuelle de cet élément ? Plex le traitera comme non associé jusqu\'à ce que vous l\'associiez de nouveau.', + 'matchScreen.unmatchSuccess' => 'Association supprimée', + 'matchScreen.unmatchFailed' => 'Échec de la dissociation', + 'matchScreen.matchApplied' => 'Association appliquée', + 'matchScreen.matchFailed' => 'Échec de l\'application', + 'matchScreen.titleHint' => 'Titre', + 'matchScreen.yearHint' => 'Année', + 'matchScreen.search' => 'Rechercher', + 'matchScreen.noMatchesFound' => 'Aucune correspondance', 'serverTasks.title' => 'Tâches du serveur', 'serverTasks.failedToLoad' => 'Échec du chargement des tâches', 'serverTasks.noTasks' => 'Aucune tâche en cours', diff --git a/lib/i18n/strings_it.g.dart b/lib/i18n/strings_it.g.dart index 2ccf47ac..124c5033 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 _TranslationsVideoSettingsIt videoSettings = _TranslationsVideoSettingsIt._(_root); @override late final _TranslationsExternalPlayerIt externalPlayer = _TranslationsExternalPlayerIt._(_root); @override late final _TranslationsMetadataEditIt metadataEdit = _TranslationsMetadataEditIt._(_root); + @override late final _TranslationsMatchScreenIt matchScreen = _TranslationsMatchScreenIt._(_root); @override late final _TranslationsServerTasksIt serverTasks = _TranslationsServerTasksIt._(_root); @override late final _TranslationsTraktIt trakt = _TranslationsTraktIt._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditIt implements TranslationsMetadataEditEn { @override String get mood => 'Atmosfera'; } +// Path: matchScreen +class _TranslationsMatchScreenIt implements TranslationsMatchScreenEn { + _TranslationsMatchScreenIt._(this._root); + + final TranslationsIt _root; // ignore: unused_field + + // Translations + @override String get match => 'Abbina...'; + @override String get fixMatch => 'Correggi abbinamento...'; + @override String get unmatch => 'Rimuovi abbinamento'; + @override String get unmatchConfirm => 'Cancellare l\'abbinamento corrente di questo elemento? Plex lo tratterà come non abbinato finché non lo abbinerai di nuovo.'; + @override String get unmatchSuccess => 'Abbinamento rimosso'; + @override String get unmatchFailed => 'Rimozione dell\'abbinamento non riuscita'; + @override String get matchApplied => 'Abbinamento applicato'; + @override String get matchFailed => 'Applicazione dell\'abbinamento non riuscita'; + @override String get titleHint => 'Titolo'; + @override String get yearHint => 'Anno'; + @override String get search => 'Cerca'; + @override String get noMatchesFound => 'Nessun risultato'; +} + // Path: serverTasks class _TranslationsServerTasksIt implements TranslationsServerTasksEn { _TranslationsServerTasksIt._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsIt { 'metadataEdit.label' => 'Etichetta', 'metadataEdit.style' => 'Stile', 'metadataEdit.mood' => 'Atmosfera', + 'matchScreen.match' => 'Abbina...', + 'matchScreen.fixMatch' => 'Correggi abbinamento...', + 'matchScreen.unmatch' => 'Rimuovi abbinamento', + 'matchScreen.unmatchConfirm' => 'Cancellare l\'abbinamento corrente di questo elemento? Plex lo tratterà come non abbinato finché non lo abbinerai di nuovo.', + 'matchScreen.unmatchSuccess' => 'Abbinamento rimosso', + 'matchScreen.unmatchFailed' => 'Rimozione dell\'abbinamento non riuscita', + 'matchScreen.matchApplied' => 'Abbinamento applicato', + 'matchScreen.matchFailed' => 'Applicazione dell\'abbinamento non riuscita', + 'matchScreen.titleHint' => 'Titolo', + 'matchScreen.yearHint' => 'Anno', + 'matchScreen.search' => 'Cerca', + 'matchScreen.noMatchesFound' => 'Nessun risultato', 'serverTasks.title' => 'Attività del server', 'serverTasks.failedToLoad' => 'Impossibile caricare le attività', 'serverTasks.noTasks' => 'Nessuna attività in corso', diff --git a/lib/i18n/strings_ja.g.dart b/lib/i18n/strings_ja.g.dart index 7ff49430..a86b57e1 100644 --- a/lib/i18n/strings_ja.g.dart +++ b/lib/i18n/strings_ja.g.dart @@ -74,6 +74,7 @@ class TranslationsJa with BaseTranslations implements T @override late final _TranslationsVideoSettingsJa videoSettings = _TranslationsVideoSettingsJa._(_root); @override late final _TranslationsExternalPlayerJa externalPlayer = _TranslationsExternalPlayerJa._(_root); @override late final _TranslationsMetadataEditJa metadataEdit = _TranslationsMetadataEditJa._(_root); + @override late final _TranslationsMatchScreenJa matchScreen = _TranslationsMatchScreenJa._(_root); @override late final _TranslationsServerTasksJa serverTasks = _TranslationsServerTasksJa._(_root); @override late final _TranslationsTraktJa trakt = _TranslationsTraktJa._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditJa implements TranslationsMetadataEditEn { @override String get mood => 'ムード'; } +// Path: matchScreen +class _TranslationsMatchScreenJa implements TranslationsMatchScreenEn { + _TranslationsMatchScreenJa._(this._root); + + final TranslationsJa _root; // ignore: unused_field + + // Translations + @override String get match => 'マッチ...'; + @override String get fixMatch => 'マッチを修正...'; + @override String get unmatch => 'マッチ解除'; + @override String get unmatchConfirm => 'このアイテムの現在のマッチを解除しますか?再度マッチするまで、Plex は未マッチとして扱います。'; + @override String get unmatchSuccess => 'マッチを解除しました'; + @override String get unmatchFailed => 'マッチの解除に失敗しました'; + @override String get matchApplied => 'マッチを適用しました'; + @override String get matchFailed => 'マッチの適用に失敗しました'; + @override String get titleHint => 'タイトル'; + @override String get yearHint => '年'; + @override String get search => '検索'; + @override String get noMatchesFound => '一致する項目がありません'; +} + // Path: serverTasks class _TranslationsServerTasksJa implements TranslationsServerTasksEn { _TranslationsServerTasksJa._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsJa { 'metadataEdit.label' => 'ラベル', 'metadataEdit.style' => 'スタイル', 'metadataEdit.mood' => 'ムード', + 'matchScreen.match' => 'マッチ...', + 'matchScreen.fixMatch' => 'マッチを修正...', + 'matchScreen.unmatch' => 'マッチ解除', + 'matchScreen.unmatchConfirm' => 'このアイテムの現在のマッチを解除しますか?再度マッチするまで、Plex は未マッチとして扱います。', + 'matchScreen.unmatchSuccess' => 'マッチを解除しました', + 'matchScreen.unmatchFailed' => 'マッチの解除に失敗しました', + 'matchScreen.matchApplied' => 'マッチを適用しました', + 'matchScreen.matchFailed' => 'マッチの適用に失敗しました', + 'matchScreen.titleHint' => 'タイトル', + 'matchScreen.yearHint' => '年', + 'matchScreen.search' => '検索', + 'matchScreen.noMatchesFound' => '一致する項目がありません', 'serverTasks.title' => 'サーバータスク', 'serverTasks.failedToLoad' => 'タスクの読み込みに失敗しました', 'serverTasks.noTasks' => '実行中のタスクはありません', diff --git a/lib/i18n/strings_ko.g.dart b/lib/i18n/strings_ko.g.dart index 3e7ff177..ef4d7963 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 _TranslationsVideoSettingsKo videoSettings = _TranslationsVideoSettingsKo._(_root); @override late final _TranslationsExternalPlayerKo externalPlayer = _TranslationsExternalPlayerKo._(_root); @override late final _TranslationsMetadataEditKo metadataEdit = _TranslationsMetadataEditKo._(_root); + @override late final _TranslationsMatchScreenKo matchScreen = _TranslationsMatchScreenKo._(_root); @override late final _TranslationsServerTasksKo serverTasks = _TranslationsServerTasksKo._(_root); @override late final _TranslationsTraktKo trakt = _TranslationsTraktKo._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditKo implements TranslationsMetadataEditEn { @override String get mood => '분위기'; } +// Path: matchScreen +class _TranslationsMatchScreenKo implements TranslationsMatchScreenEn { + _TranslationsMatchScreenKo._(this._root); + + final TranslationsKo _root; // ignore: unused_field + + // Translations + @override String get match => '일치...'; + @override String get fixMatch => '일치 수정...'; + @override String get unmatch => '일치 해제'; + @override String get unmatchConfirm => '이 항목의 현재 일치를 해제하시겠습니까? 다시 일치시킬 때까지 Plex는 일치되지 않은 것으로 처리합니다.'; + @override String get unmatchSuccess => '일치 해제됨'; + @override String get unmatchFailed => '일치 해제에 실패했습니다'; + @override String get matchApplied => '일치 적용됨'; + @override String get matchFailed => '일치 적용에 실패했습니다'; + @override String get titleHint => '제목'; + @override String get yearHint => '연도'; + @override String get search => '검색'; + @override String get noMatchesFound => '일치하는 항목이 없습니다'; +} + // Path: serverTasks class _TranslationsServerTasksKo implements TranslationsServerTasksEn { _TranslationsServerTasksKo._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsKo { 'metadataEdit.label' => '라벨', 'metadataEdit.style' => '스타일', 'metadataEdit.mood' => '분위기', + 'matchScreen.match' => '일치...', + 'matchScreen.fixMatch' => '일치 수정...', + 'matchScreen.unmatch' => '일치 해제', + 'matchScreen.unmatchConfirm' => '이 항목의 현재 일치를 해제하시겠습니까? 다시 일치시킬 때까지 Plex는 일치되지 않은 것으로 처리합니다.', + 'matchScreen.unmatchSuccess' => '일치 해제됨', + 'matchScreen.unmatchFailed' => '일치 해제에 실패했습니다', + 'matchScreen.matchApplied' => '일치 적용됨', + 'matchScreen.matchFailed' => '일치 적용에 실패했습니다', + 'matchScreen.titleHint' => '제목', + 'matchScreen.yearHint' => '연도', + 'matchScreen.search' => '검색', + 'matchScreen.noMatchesFound' => '일치하는 항목이 없습니다', 'serverTasks.title' => '서버 작업', 'serverTasks.failedToLoad' => '작업을 불러올 수 없습니다', 'serverTasks.noTasks' => '실행 중인 작업 없음', diff --git a/lib/i18n/strings_nb.g.dart b/lib/i18n/strings_nb.g.dart index c5f60432..01f14b5c 100644 --- a/lib/i18n/strings_nb.g.dart +++ b/lib/i18n/strings_nb.g.dart @@ -74,6 +74,7 @@ class TranslationsNb with BaseTranslations implements T @override late final _TranslationsVideoSettingsNb videoSettings = _TranslationsVideoSettingsNb._(_root); @override late final _TranslationsExternalPlayerNb externalPlayer = _TranslationsExternalPlayerNb._(_root); @override late final _TranslationsMetadataEditNb metadataEdit = _TranslationsMetadataEditNb._(_root); + @override late final _TranslationsMatchScreenNb matchScreen = _TranslationsMatchScreenNb._(_root); @override late final _TranslationsServerTasksNb serverTasks = _TranslationsServerTasksNb._(_root); @override late final _TranslationsTraktNb trakt = _TranslationsTraktNb._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditNb implements TranslationsMetadataEditEn { @override String get mood => 'Stemning'; } +// Path: matchScreen +class _TranslationsMatchScreenNb implements TranslationsMatchScreenEn { + _TranslationsMatchScreenNb._(this._root); + + final TranslationsNb _root; // ignore: unused_field + + // Translations + @override String get match => 'Match...'; + @override String get fixMatch => 'Rett match...'; + @override String get unmatch => 'Fjern match'; + @override String get unmatchConfirm => 'Fjerne gjeldende match for dette elementet? Plex behandler det som umatchet til du matcher det igjen.'; + @override String get unmatchSuccess => 'Match fjernet'; + @override String get unmatchFailed => 'Kunne ikke fjerne match'; + @override String get matchApplied => 'Match anvendt'; + @override String get matchFailed => 'Kunne ikke anvende match'; + @override String get titleHint => 'Tittel'; + @override String get yearHint => 'År'; + @override String get search => 'Søk'; + @override String get noMatchesFound => 'Ingen treff funnet'; +} + // Path: serverTasks class _TranslationsServerTasksNb implements TranslationsServerTasksEn { _TranslationsServerTasksNb._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsNb { 'metadataEdit.label' => 'Etikett', 'metadataEdit.style' => 'Stil', 'metadataEdit.mood' => 'Stemning', + 'matchScreen.match' => 'Match...', + 'matchScreen.fixMatch' => 'Rett match...', + 'matchScreen.unmatch' => 'Fjern match', + 'matchScreen.unmatchConfirm' => 'Fjerne gjeldende match for dette elementet? Plex behandler det som umatchet til du matcher det igjen.', + 'matchScreen.unmatchSuccess' => 'Match fjernet', + 'matchScreen.unmatchFailed' => 'Kunne ikke fjerne match', + 'matchScreen.matchApplied' => 'Match anvendt', + 'matchScreen.matchFailed' => 'Kunne ikke anvende match', + 'matchScreen.titleHint' => 'Tittel', + 'matchScreen.yearHint' => 'År', + 'matchScreen.search' => 'Søk', + 'matchScreen.noMatchesFound' => 'Ingen treff funnet', 'serverTasks.title' => 'Serveroppgaver', 'serverTasks.failedToLoad' => 'Kunne ikke laste oppgaver', 'serverTasks.noTasks' => 'Ingen oppgaver kjører', diff --git a/lib/i18n/strings_nl.g.dart b/lib/i18n/strings_nl.g.dart index 033bccc1..9ef51f40 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 _TranslationsVideoSettingsNl videoSettings = _TranslationsVideoSettingsNl._(_root); @override late final _TranslationsExternalPlayerNl externalPlayer = _TranslationsExternalPlayerNl._(_root); @override late final _TranslationsMetadataEditNl metadataEdit = _TranslationsMetadataEditNl._(_root); + @override late final _TranslationsMatchScreenNl matchScreen = _TranslationsMatchScreenNl._(_root); @override late final _TranslationsServerTasksNl serverTasks = _TranslationsServerTasksNl._(_root); @override late final _TranslationsTraktNl trakt = _TranslationsTraktNl._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditNl implements TranslationsMetadataEditEn { @override String get mood => 'Stemming'; } +// Path: matchScreen +class _TranslationsMatchScreenNl implements TranslationsMatchScreenEn { + _TranslationsMatchScreenNl._(this._root); + + final TranslationsNl _root; // ignore: unused_field + + // Translations + @override String get match => 'Koppelen...'; + @override String get fixMatch => 'Koppeling herstellen...'; + @override String get unmatch => 'Ontkoppelen'; + @override String get unmatchConfirm => 'De huidige koppeling voor dit item wissen? Plex behandelt het als ongekoppeld totdat je het opnieuw koppelt.'; + @override String get unmatchSuccess => 'Item ontkoppeld'; + @override String get unmatchFailed => 'Kon item niet ontkoppelen'; + @override String get matchApplied => 'Koppeling toegepast'; + @override String get matchFailed => 'Koppeling kon niet worden toegepast'; + @override String get titleHint => 'Titel'; + @override String get yearHint => 'Jaar'; + @override String get search => 'Zoeken'; + @override String get noMatchesFound => 'Geen overeenkomsten gevonden'; +} + // Path: serverTasks class _TranslationsServerTasksNl implements TranslationsServerTasksEn { _TranslationsServerTasksNl._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsNl { 'metadataEdit.label' => 'Label', 'metadataEdit.style' => 'Stijl', 'metadataEdit.mood' => 'Stemming', + 'matchScreen.match' => 'Koppelen...', + 'matchScreen.fixMatch' => 'Koppeling herstellen...', + 'matchScreen.unmatch' => 'Ontkoppelen', + 'matchScreen.unmatchConfirm' => 'De huidige koppeling voor dit item wissen? Plex behandelt het als ongekoppeld totdat je het opnieuw koppelt.', + 'matchScreen.unmatchSuccess' => 'Item ontkoppeld', + 'matchScreen.unmatchFailed' => 'Kon item niet ontkoppelen', + 'matchScreen.matchApplied' => 'Koppeling toegepast', + 'matchScreen.matchFailed' => 'Koppeling kon niet worden toegepast', + 'matchScreen.titleHint' => 'Titel', + 'matchScreen.yearHint' => 'Jaar', + 'matchScreen.search' => 'Zoeken', + 'matchScreen.noMatchesFound' => 'Geen overeenkomsten gevonden', 'serverTasks.title' => 'Servertaken', 'serverTasks.failedToLoad' => 'Taken konden niet worden geladen', 'serverTasks.noTasks' => 'Geen actieve taken', diff --git a/lib/i18n/strings_pl.g.dart b/lib/i18n/strings_pl.g.dart index d9dacbff..b5dacf65 100644 --- a/lib/i18n/strings_pl.g.dart +++ b/lib/i18n/strings_pl.g.dart @@ -74,6 +74,7 @@ class TranslationsPl with BaseTranslations implements T @override late final _TranslationsVideoSettingsPl videoSettings = _TranslationsVideoSettingsPl._(_root); @override late final _TranslationsExternalPlayerPl externalPlayer = _TranslationsExternalPlayerPl._(_root); @override late final _TranslationsMetadataEditPl metadataEdit = _TranslationsMetadataEditPl._(_root); + @override late final _TranslationsMatchScreenPl matchScreen = _TranslationsMatchScreenPl._(_root); @override late final _TranslationsServerTasksPl serverTasks = _TranslationsServerTasksPl._(_root); @override late final _TranslationsTraktPl trakt = _TranslationsTraktPl._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditPl implements TranslationsMetadataEditEn { @override String get mood => 'Nastrój'; } +// Path: matchScreen +class _TranslationsMatchScreenPl implements TranslationsMatchScreenEn { + _TranslationsMatchScreenPl._(this._root); + + final TranslationsPl _root; // ignore: unused_field + + // Translations + @override String get match => 'Dopasuj...'; + @override String get fixMatch => 'Popraw dopasowanie...'; + @override String get unmatch => 'Usuń dopasowanie'; + @override String get unmatchConfirm => 'Usunąć obecne dopasowanie tego elementu? Plex będzie traktował go jako niedopasowany, dopóki nie dopasujesz go ponownie.'; + @override String get unmatchSuccess => 'Dopasowanie usunięte'; + @override String get unmatchFailed => 'Nie udało się usunąć dopasowania'; + @override String get matchApplied => 'Dopasowanie zastosowane'; + @override String get matchFailed => 'Nie udało się zastosować dopasowania'; + @override String get titleHint => 'Tytuł'; + @override String get yearHint => 'Rok'; + @override String get search => 'Szukaj'; + @override String get noMatchesFound => 'Nie znaleziono dopasowań'; +} + // Path: serverTasks class _TranslationsServerTasksPl implements TranslationsServerTasksEn { _TranslationsServerTasksPl._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsPl { 'metadataEdit.label' => 'Etykieta', 'metadataEdit.style' => 'Styl', 'metadataEdit.mood' => 'Nastrój', + 'matchScreen.match' => 'Dopasuj...', + 'matchScreen.fixMatch' => 'Popraw dopasowanie...', + 'matchScreen.unmatch' => 'Usuń dopasowanie', + 'matchScreen.unmatchConfirm' => 'Usunąć obecne dopasowanie tego elementu? Plex będzie traktował go jako niedopasowany, dopóki nie dopasujesz go ponownie.', + 'matchScreen.unmatchSuccess' => 'Dopasowanie usunięte', + 'matchScreen.unmatchFailed' => 'Nie udało się usunąć dopasowania', + 'matchScreen.matchApplied' => 'Dopasowanie zastosowane', + 'matchScreen.matchFailed' => 'Nie udało się zastosować dopasowania', + 'matchScreen.titleHint' => 'Tytuł', + 'matchScreen.yearHint' => 'Rok', + 'matchScreen.search' => 'Szukaj', + 'matchScreen.noMatchesFound' => 'Nie znaleziono dopasowań', 'serverTasks.title' => 'Zadania serwera', 'serverTasks.failedToLoad' => 'Nie udało się załadować zadań', 'serverTasks.noTasks' => 'Brak uruchomionych zadań', diff --git a/lib/i18n/strings_pt.g.dart b/lib/i18n/strings_pt.g.dart index 10308cef..d0c2d0e3 100644 --- a/lib/i18n/strings_pt.g.dart +++ b/lib/i18n/strings_pt.g.dart @@ -74,6 +74,7 @@ class TranslationsPt with BaseTranslations implements T @override late final _TranslationsVideoSettingsPt videoSettings = _TranslationsVideoSettingsPt._(_root); @override late final _TranslationsExternalPlayerPt externalPlayer = _TranslationsExternalPlayerPt._(_root); @override late final _TranslationsMetadataEditPt metadataEdit = _TranslationsMetadataEditPt._(_root); + @override late final _TranslationsMatchScreenPt matchScreen = _TranslationsMatchScreenPt._(_root); @override late final _TranslationsServerTasksPt serverTasks = _TranslationsServerTasksPt._(_root); @override late final _TranslationsTraktPt trakt = _TranslationsTraktPt._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditPt implements TranslationsMetadataEditEn { @override String get mood => 'Humor'; } +// Path: matchScreen +class _TranslationsMatchScreenPt implements TranslationsMatchScreenEn { + _TranslationsMatchScreenPt._(this._root); + + final TranslationsPt _root; // ignore: unused_field + + // Translations + @override String get match => 'Associar...'; + @override String get fixMatch => 'Corrigir correspondência...'; + @override String get unmatch => 'Desassociar'; + @override String get unmatchConfirm => 'Limpar a correspondência atual deste item? O Plex vai tratá-lo como não correspondido até que o associe novamente.'; + @override String get unmatchSuccess => 'Item desassociado'; + @override String get unmatchFailed => 'Falha ao desassociar item'; + @override String get matchApplied => 'Correspondência aplicada'; + @override String get matchFailed => 'Falha ao aplicar correspondência'; + @override String get titleHint => 'Título'; + @override String get yearHint => 'Ano'; + @override String get search => 'Pesquisar'; + @override String get noMatchesFound => 'Nenhuma correspondência encontrada'; +} + // Path: serverTasks class _TranslationsServerTasksPt implements TranslationsServerTasksEn { _TranslationsServerTasksPt._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsPt { 'metadataEdit.label' => 'Rótulo', 'metadataEdit.style' => 'Estilo', 'metadataEdit.mood' => 'Humor', + 'matchScreen.match' => 'Associar...', + 'matchScreen.fixMatch' => 'Corrigir correspondência...', + 'matchScreen.unmatch' => 'Desassociar', + 'matchScreen.unmatchConfirm' => 'Limpar a correspondência atual deste item? O Plex vai tratá-lo como não correspondido até que o associe novamente.', + 'matchScreen.unmatchSuccess' => 'Item desassociado', + 'matchScreen.unmatchFailed' => 'Falha ao desassociar item', + 'matchScreen.matchApplied' => 'Correspondência aplicada', + 'matchScreen.matchFailed' => 'Falha ao aplicar correspondência', + 'matchScreen.titleHint' => 'Título', + 'matchScreen.yearHint' => 'Ano', + 'matchScreen.search' => 'Pesquisar', + 'matchScreen.noMatchesFound' => 'Nenhuma correspondência encontrada', 'serverTasks.title' => 'Tarefas do servidor', 'serverTasks.failedToLoad' => 'Falha ao carregar tarefas', 'serverTasks.noTasks' => 'Nenhuma tarefa em execução', diff --git a/lib/i18n/strings_ru.g.dart b/lib/i18n/strings_ru.g.dart index 70d25819..72b32500 100644 --- a/lib/i18n/strings_ru.g.dart +++ b/lib/i18n/strings_ru.g.dart @@ -74,6 +74,7 @@ class TranslationsRu with BaseTranslations implements T @override late final _TranslationsVideoSettingsRu videoSettings = _TranslationsVideoSettingsRu._(_root); @override late final _TranslationsExternalPlayerRu externalPlayer = _TranslationsExternalPlayerRu._(_root); @override late final _TranslationsMetadataEditRu metadataEdit = _TranslationsMetadataEditRu._(_root); + @override late final _TranslationsMatchScreenRu matchScreen = _TranslationsMatchScreenRu._(_root); @override late final _TranslationsServerTasksRu serverTasks = _TranslationsServerTasksRu._(_root); @override late final _TranslationsTraktRu trakt = _TranslationsTraktRu._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditRu implements TranslationsMetadataEditEn { @override String get mood => 'Настроение'; } +// Path: matchScreen +class _TranslationsMatchScreenRu implements TranslationsMatchScreenEn { + _TranslationsMatchScreenRu._(this._root); + + final TranslationsRu _root; // ignore: unused_field + + // Translations + @override String get match => 'Сопоставить...'; + @override String get fixMatch => 'Исправить сопоставление...'; + @override String get unmatch => 'Сбросить сопоставление'; + @override String get unmatchConfirm => 'Сбросить текущее сопоставление этого элемента? Plex будет считать его несопоставленным, пока вы не сопоставите его снова.'; + @override String get unmatchSuccess => 'Сопоставление сброшено'; + @override String get unmatchFailed => 'Не удалось сбросить сопоставление'; + @override String get matchApplied => 'Сопоставление применено'; + @override String get matchFailed => 'Не удалось применить сопоставление'; + @override String get titleHint => 'Название'; + @override String get yearHint => 'Год'; + @override String get search => 'Поиск'; + @override String get noMatchesFound => 'Совпадений не найдено'; +} + // Path: serverTasks class _TranslationsServerTasksRu implements TranslationsServerTasksEn { _TranslationsServerTasksRu._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsRu { 'metadataEdit.label' => 'Метка', 'metadataEdit.style' => 'Стиль', 'metadataEdit.mood' => 'Настроение', + 'matchScreen.match' => 'Сопоставить...', + 'matchScreen.fixMatch' => 'Исправить сопоставление...', + 'matchScreen.unmatch' => 'Сбросить сопоставление', + 'matchScreen.unmatchConfirm' => 'Сбросить текущее сопоставление этого элемента? Plex будет считать его несопоставленным, пока вы не сопоставите его снова.', + 'matchScreen.unmatchSuccess' => 'Сопоставление сброшено', + 'matchScreen.unmatchFailed' => 'Не удалось сбросить сопоставление', + 'matchScreen.matchApplied' => 'Сопоставление применено', + 'matchScreen.matchFailed' => 'Не удалось применить сопоставление', + 'matchScreen.titleHint' => 'Название', + 'matchScreen.yearHint' => 'Год', + 'matchScreen.search' => 'Поиск', + 'matchScreen.noMatchesFound' => 'Совпадений не найдено', 'serverTasks.title' => 'Задачи сервера', 'serverTasks.failedToLoad' => 'Не удалось загрузить задачи', 'serverTasks.noTasks' => 'Нет выполняемых задач', diff --git a/lib/i18n/strings_sv.g.dart b/lib/i18n/strings_sv.g.dart index 06143c9e..db4e9dac 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 _TranslationsVideoSettingsSv videoSettings = _TranslationsVideoSettingsSv._(_root); @override late final _TranslationsExternalPlayerSv externalPlayer = _TranslationsExternalPlayerSv._(_root); @override late final _TranslationsMetadataEditSv metadataEdit = _TranslationsMetadataEditSv._(_root); + @override late final _TranslationsMatchScreenSv matchScreen = _TranslationsMatchScreenSv._(_root); @override late final _TranslationsServerTasksSv serverTasks = _TranslationsServerTasksSv._(_root); @override late final _TranslationsTraktSv trakt = _TranslationsTraktSv._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditSv implements TranslationsMetadataEditEn { @override String get mood => 'Stämning'; } +// Path: matchScreen +class _TranslationsMatchScreenSv implements TranslationsMatchScreenEn { + _TranslationsMatchScreenSv._(this._root); + + final TranslationsSv _root; // ignore: unused_field + + // Translations + @override String get match => 'Matcha...'; + @override String get fixMatch => 'Rätta matchning...'; + @override String get unmatch => 'Ta bort matchning'; + @override String get unmatchConfirm => 'Ta bort den aktuella matchningen för det här objektet? Plex behandlar det som omatchat tills du matchar det igen.'; + @override String get unmatchSuccess => 'Matchning borttagen'; + @override String get unmatchFailed => 'Det gick inte att ta bort matchningen'; + @override String get matchApplied => 'Matchning tillämpad'; + @override String get matchFailed => 'Det gick inte att tillämpa matchningen'; + @override String get titleHint => 'Titel'; + @override String get yearHint => 'År'; + @override String get search => 'Sök'; + @override String get noMatchesFound => 'Inga matchningar hittades'; +} + // Path: serverTasks class _TranslationsServerTasksSv implements TranslationsServerTasksEn { _TranslationsServerTasksSv._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsSv { 'metadataEdit.label' => 'Etikett', 'metadataEdit.style' => 'Stil', 'metadataEdit.mood' => 'Stämning', + 'matchScreen.match' => 'Matcha...', + 'matchScreen.fixMatch' => 'Rätta matchning...', + 'matchScreen.unmatch' => 'Ta bort matchning', + 'matchScreen.unmatchConfirm' => 'Ta bort den aktuella matchningen för det här objektet? Plex behandlar det som omatchat tills du matchar det igen.', + 'matchScreen.unmatchSuccess' => 'Matchning borttagen', + 'matchScreen.unmatchFailed' => 'Det gick inte att ta bort matchningen', + 'matchScreen.matchApplied' => 'Matchning tillämpad', + 'matchScreen.matchFailed' => 'Det gick inte att tillämpa matchningen', + 'matchScreen.titleHint' => 'Titel', + 'matchScreen.yearHint' => 'År', + 'matchScreen.search' => 'Sök', + 'matchScreen.noMatchesFound' => 'Inga matchningar hittades', 'serverTasks.title' => 'Serveruppgifter', 'serverTasks.failedToLoad' => 'Kunde inte ladda uppgifter', 'serverTasks.noTasks' => 'Inga pågående uppgifter', diff --git a/lib/i18n/strings_zh.g.dart b/lib/i18n/strings_zh.g.dart index e2cf9607..28216c1e 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 _TranslationsVideoSettingsZh videoSettings = _TranslationsVideoSettingsZh._(_root); @override late final _TranslationsExternalPlayerZh externalPlayer = _TranslationsExternalPlayerZh._(_root); @override late final _TranslationsMetadataEditZh metadataEdit = _TranslationsMetadataEditZh._(_root); + @override late final _TranslationsMatchScreenZh matchScreen = _TranslationsMatchScreenZh._(_root); @override late final _TranslationsServerTasksZh serverTasks = _TranslationsServerTasksZh._(_root); @override late final _TranslationsTraktZh trakt = _TranslationsTraktZh._(_root); } @@ -1224,6 +1225,27 @@ class _TranslationsMetadataEditZh implements TranslationsMetadataEditEn { @override String get mood => '氛围'; } +// Path: matchScreen +class _TranslationsMatchScreenZh implements TranslationsMatchScreenEn { + _TranslationsMatchScreenZh._(this._root); + + final TranslationsZh _root; // ignore: unused_field + + // Translations + @override String get match => '匹配...'; + @override String get fixMatch => '修正匹配...'; + @override String get unmatch => '取消匹配'; + @override String get unmatchConfirm => '清除此项目的当前匹配吗?在重新匹配之前,Plex 会将其视为未匹配。'; + @override String get unmatchSuccess => '已取消匹配'; + @override String get unmatchFailed => '无法取消匹配'; + @override String get matchApplied => '匹配已应用'; + @override String get matchFailed => '应用匹配失败'; + @override String get titleHint => '标题'; + @override String get yearHint => '年份'; + @override String get search => '搜索'; + @override String get noMatchesFound => '未找到匹配项'; +} + // Path: serverTasks class _TranslationsServerTasksZh implements TranslationsServerTasksEn { _TranslationsServerTasksZh._(this._root); @@ -2325,6 +2347,18 @@ extension on TranslationsZh { 'metadataEdit.label' => '标记', 'metadataEdit.style' => '风格', 'metadataEdit.mood' => '氛围', + 'matchScreen.match' => '匹配...', + 'matchScreen.fixMatch' => '修正匹配...', + 'matchScreen.unmatch' => '取消匹配', + 'matchScreen.unmatchConfirm' => '清除此项目的当前匹配吗?在重新匹配之前,Plex 会将其视为未匹配。', + 'matchScreen.unmatchSuccess' => '已取消匹配', + 'matchScreen.unmatchFailed' => '无法取消匹配', + 'matchScreen.matchApplied' => '匹配已应用', + 'matchScreen.matchFailed' => '应用匹配失败', + 'matchScreen.titleHint' => '标题', + 'matchScreen.yearHint' => '年份', + 'matchScreen.search' => '搜索', + 'matchScreen.noMatchesFound' => '未找到匹配项', 'serverTasks.title' => '服务器任务', 'serverTasks.failedToLoad' => '加载任务失败', 'serverTasks.noTasks' => '没有正在运行的任务', diff --git a/lib/i18n/sv.i18n.json b/lib/i18n/sv.i18n.json index e0a00b5f..56963329 100644 --- a/lib/i18n/sv.i18n.json +++ b/lib/i18n/sv.i18n.json @@ -986,6 +986,20 @@ "style": "Stil", "mood": "Stämning" }, + "matchScreen": { + "match": "Matcha...", + "fixMatch": "Rätta matchning...", + "unmatch": "Ta bort matchning", + "unmatchConfirm": "Ta bort den aktuella matchningen för det här objektet? Plex behandlar det som omatchat tills du matchar det igen.", + "unmatchSuccess": "Matchning borttagen", + "unmatchFailed": "Det gick inte att ta bort matchningen", + "matchApplied": "Matchning tillämpad", + "matchFailed": "Det gick inte att tillämpa matchningen", + "titleHint": "Titel", + "yearHint": "År", + "search": "Sök", + "noMatchesFound": "Inga matchningar hittades" + }, "serverTasks": { "title": "Serveruppgifter", "failedToLoad": "Kunde inte ladda uppgifter", diff --git a/lib/i18n/zh.i18n.json b/lib/i18n/zh.i18n.json index bf536503..f23d19e5 100644 --- a/lib/i18n/zh.i18n.json +++ b/lib/i18n/zh.i18n.json @@ -986,6 +986,20 @@ "style": "风格", "mood": "氛围" }, + "matchScreen": { + "match": "匹配...", + "fixMatch": "修正匹配...", + "unmatch": "取消匹配", + "unmatchConfirm": "清除此项目的当前匹配吗?在重新匹配之前,Plex 会将其视为未匹配。", + "unmatchSuccess": "已取消匹配", + "unmatchFailed": "无法取消匹配", + "matchApplied": "匹配已应用", + "matchFailed": "应用匹配失败", + "titleHint": "标题", + "yearHint": "年份", + "search": "搜索", + "noMatchesFound": "未找到匹配项" + }, "serverTasks": { "title": "服务器任务", "failedToLoad": "加载任务失败", diff --git a/lib/models/plex_match_result.dart b/lib/models/plex_match_result.dart new file mode 100644 index 00000000..d819683d --- /dev/null +++ b/lib/models/plex_match_result.dart @@ -0,0 +1,36 @@ +import '../utils/json_utils.dart'; + +class PlexMatchResult { + final String guid; + final String name; + final int? year; + final int? score; + final String? thumb; + final String? summary; + final String? type; + final bool matched; + + PlexMatchResult({ + required this.guid, + required this.name, + this.year, + this.score, + this.thumb, + this.summary, + this.type, + this.matched = false, + }); + + factory PlexMatchResult.fromJson(Map json) { + return PlexMatchResult( + guid: json['guid']?.toString() ?? '', + name: json['name']?.toString() ?? '', + year: flexibleInt(json['year']), + score: flexibleInt(json['score']), + thumb: json['thumb']?.toString(), + summary: json['summary']?.toString(), + type: json['type']?.toString(), + matched: flexibleBool(json['matched']), + ); + } +} diff --git a/lib/models/plex_metadata.dart b/lib/models/plex_metadata.dart index 5ce21b02..97ab3a4e 100644 --- a/lib/models/plex_metadata.dart +++ b/lib/models/plex_metadata.dart @@ -55,6 +55,10 @@ enum PlexMediaType { }; } +/// Shared suffix of both unmatched-agent URL schemes: legacy +/// `com.plexapp.agents.none://` and new-style `tv.plex.agents.none://`. +const _unmatchedAgentMarker = 'agents.none://'; + @JsonSerializable() class PlexMetadata with MultiServerFields { @JsonKey(readValue: _readRatingKey) @@ -160,6 +164,10 @@ class PlexMetadata with MultiServerFields { /// These have keys like `/library/sections/5/all` instead of `/library/metadata/12345`. bool get isLibrarySection => key != null && key!.startsWith('/library/sections/'); + /// Whether this item has no metadata agent match. Unmatched items get a + /// synthetic `*.agents.none://` guid from the server. + bool get isUnmatched => guid == null || guid!.isEmpty || guid!.contains(_unmatchedAgentMarker); + /// Extract the library section ID from a library-section item's key. /// Returns null if this is not a library section item. String? get librarySectionKey { diff --git a/lib/screens/match_screen.dart b/lib/screens/match_screen.dart new file mode 100644 index 00000000..5bc325a2 --- /dev/null +++ b/lib/screens/match_screen.dart @@ -0,0 +1,249 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:material_symbols_icons/symbols.dart'; + +import '../focus/focusable_button.dart'; +import '../focus/focusable_text_field.dart'; +import '../focus/input_mode_tracker.dart'; +import '../i18n/strings.g.dart'; +import '../models/plex_match_result.dart'; +import '../models/plex_metadata.dart'; +import '../services/plex_client.dart'; +import '../utils/provider_extensions.dart'; +import '../utils/snackbar_helper.dart'; +import '../widgets/app_icon.dart'; +import '../widgets/focusable_list_tile.dart'; +import '../widgets/focused_scroll_scaffold.dart'; +import '../widgets/pill_input_decoration.dart'; +import '../widgets/plex_optimized_image.dart'; + +/// Fix / apply a metadata match on a movie or show. +/// +/// On success, pops `true` so the caller can refresh its view. +class MatchScreen extends StatefulWidget { + final PlexMetadata metadata; + + const MatchScreen({super.key, required this.metadata}); + + @override + State createState() => _MatchScreenState(); +} + +class _MatchScreenState extends State { + late final PlexClient _client; + late final TextEditingController _nameController; + late final TextEditingController _yearController; + final _nameFocus = FocusNode(); + final _yearFocus = FocusNode(); + final _searchFocus = FocusNode(); + + List? _results; + bool _isSearching = false; + String? _applyingGuid; + + bool get _isApplying => _applyingGuid != null; + + @override + void initState() { + super.initState(); + _client = context.getClientWithFallback(widget.metadata.serverId); + _nameController = TextEditingController(text: widget.metadata.title); + _yearController = TextEditingController(text: widget.metadata.year?.toString() ?? ''); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + if (InputModeTracker.isKeyboardMode(context)) { + _nameFocus.requestFocus(); + } + _search(); + }); + } + + @override + void dispose() { + _nameController.dispose(); + _yearController.dispose(); + _nameFocus.dispose(); + _yearFocus.dispose(); + _searchFocus.dispose(); + super.dispose(); + } + + Future _search() async { + if (_isSearching) return; + setState(() => _isSearching = true); + final results = await _client.findMatches( + widget.metadata.ratingKey, + title: _nameController.text.trim(), + year: _yearController.text.trim(), + ); + if (!mounted) return; + setState(() { + _results = results; + _isSearching = false; + }); + } + + Future _applyMatch(PlexMatchResult result) async { + if (_isApplying) return; + setState(() => _applyingGuid = result.guid); + final success = await _client.applyMatch( + widget.metadata.ratingKey, + guid: result.guid, + name: result.name, + year: result.year?.toString(), + ); + if (!mounted) return; + setState(() => _applyingGuid = null); + if (success) { + showSuccessSnackBar(context, t.matchScreen.matchApplied); + Navigator.pop(context, true); + } else { + showErrorSnackBar(context, t.matchScreen.matchFailed); + } + } + + @override + Widget build(BuildContext context) { + return FocusedScrollScaffold( + title: Text(widget.metadata.isUnmatched ? t.matchScreen.match : t.matchScreen.fixMatch), + slivers: [ + SliverPadding( + padding: const EdgeInsets.fromLTRB(16, 16, 16, 8), + sliver: SliverToBoxAdapter(child: _buildSearchForm(context)), + ), + if (_isSearching || _results == null) + const SliverFillRemaining(hasScrollBody: false, child: Center(child: CircularProgressIndicator())) + else if (_results!.isEmpty) + SliverFillRemaining( + hasScrollBody: false, + child: Center( + child: Text( + t.matchScreen.noMatchesFound, + style: Theme.of(context).textTheme.bodyLarge, + textAlign: TextAlign.center, + ), + ), + ) + else + SliverPadding( + padding: const EdgeInsets.fromLTRB(8, 0, 8, 24), + sliver: SliverList.builder( + itemCount: _results!.length, + itemBuilder: (context, index) => _buildResultTile(_results![index]), + ), + ), + ], + ); + } + + Widget _buildSearchForm(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Row( + children: [ + Expanded( + child: FocusableTextField( + controller: _nameController, + focusNode: _nameFocus, + decoration: pillInputDecoration( + context, + hintText: t.matchScreen.titleHint, + prefixIcon: const AppIcon(Symbols.search_rounded), + ), + textInputAction: TextInputAction.next, + onSubmitted: (_) => _yearFocus.requestFocus(), + onNavigateRight: _yearFocus.requestFocus, + onNavigateDown: _searchFocus.requestFocus, + ), + ), + const SizedBox(width: 12), + SizedBox( + width: 140, + child: FocusableTextField( + controller: _yearController, + focusNode: _yearFocus, + decoration: pillInputDecoration(context, hintText: t.matchScreen.yearHint), + keyboardType: TextInputType.number, + inputFormatters: [FilteringTextInputFormatter.digitsOnly, LengthLimitingTextInputFormatter(4)], + textInputAction: TextInputAction.search, + onSubmitted: (_) => _search(), + onNavigateLeft: _nameFocus.requestFocus, + onNavigateDown: _searchFocus.requestFocus, + ), + ), + ], + ), + const SizedBox(height: 12), + FocusableButton( + focusNode: _searchFocus, + useBackgroundFocus: true, + onPressed: _isSearching ? null : _search, + child: FilledButton.icon( + onPressed: _isSearching ? null : _search, + icon: const AppIcon(Symbols.search_rounded), + label: Text(t.matchScreen.search), + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 14), + shape: const StadiumBorder(), + ), + ), + ), + ], + ); + } + + Widget _buildResultTile(PlexMatchResult result) { + final isApplyingThis = _isApplying && _applyingGuid == result.guid; + final yearLabel = result.year?.toString() ?? ''; + final titleText = yearLabel.isEmpty ? result.name : '${result.name} (${result.year})'; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), + child: FocusableListTile( + enabled: !_isApplying, + onTap: () => _applyMatch(result), + leading: SizedBox( + width: 48, + height: 72, + child: ClipRRect( + borderRadius: BorderRadius.circular(4), + child: PlexOptimizedImage( + client: _client, + imagePath: result.thumb, + fit: BoxFit.cover, + fallbackIcon: Symbols.movie_rounded, + ), + ), + ), + title: Text(titleText, style: const TextStyle(fontWeight: FontWeight.w600)), + subtitle: result.summary != null && result.summary!.isNotEmpty + ? Text(result.summary!, maxLines: 2, overflow: TextOverflow.ellipsis) + : null, + trailing: isApplyingThis + ? const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2)) + : result.score != null + ? _ScoreChip(score: result.score!) + : null, + ), + ); + } +} + +class _ScoreChip extends StatelessWidget { + final int score; + const _ScoreChip({required this.score}); + + @override + Widget build(BuildContext context) { + final colorScheme = Theme.of(context).colorScheme; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration(color: colorScheme.secondaryContainer, borderRadius: BorderRadius.circular(100)), + child: Text( + '$score', + style: TextStyle(color: colorScheme.onSecondaryContainer, fontWeight: FontWeight.w600), + ), + ); + } +} diff --git a/lib/services/plex_client.dart b/lib/services/plex_client.dart index f963666f..981653b1 100644 --- a/lib/services/plex_client.dart +++ b/lib/services/plex_client.dart @@ -22,6 +22,7 @@ import '../models/plex_library.dart'; import '../models/plex_media_info.dart'; import '../models/plex_subtitle_search_result.dart'; import '../models/plex_media_version.dart'; +import '../models/plex_match_result.dart'; import '../models/plex_metadata.dart'; import '../utils/content_utils.dart'; import '../models/plex_playlist.dart'; @@ -1988,6 +1989,60 @@ class PlexClient { ); } + /// Search for match candidates for a media item. + Future> findMatches( + String ratingKey, { + String? title, + String? year, + String? agent, + String? language, + }) async { + final queryParams = {'manual': 1}; + if (title != null && title.isNotEmpty) queryParams['title'] = title; + if (year != null && year.isNotEmpty) queryParams['year'] = year; + if (agent != null && agent.isNotEmpty) queryParams['agent'] = agent; + if (language != null && language.isNotEmpty) queryParams['language'] = language; + + return _wrapListApiCall( + () => _getWithFailover('/library/metadata/$ratingKey/matches', queryParameters: queryParams), + (response) { + final container = _getMediaContainer(response); + if (container == null || container['SearchResult'] == null) return []; + return (container['SearchResult'] as List) + .map((json) => PlexMatchResult.fromJson(json as Map)) + .toList(); + }, + 'Failed to search for matches', + ); + } + + /// Apply a chosen match to a media item. + Future applyMatch(String ratingKey, {required String guid, String? name, String? year}) async { + final queryParams = {'guid': guid}; + if (name != null && name.isNotEmpty) queryParams['name'] = name; + if (year != null && year.isNotEmpty) queryParams['year'] = year; + + final result = await _wrapBoolApiCall( + () => _http.put('/library/metadata/$ratingKey/match', queryParameters: queryParams), + 'Failed to apply match', + ); + if (result) { + await _cache.deleteForItem(serverId, ratingKey); + } + return result; + } + + Future unmatchItem(String ratingKey) async { + final result = await _wrapBoolApiCall( + () => _http.put('/library/metadata/$ratingKey/unmatch'), + 'Failed to unmatch item', + ); + if (result) { + await _cache.deleteForItem(serverId, ratingKey); + } + return result; + } + /// Get available artwork (posters or backgrounds) for a media item Future>> getAvailableArtwork(String ratingKey, String element) async { try { diff --git a/lib/widgets/media_context_menu.dart b/lib/widgets/media_context_menu.dart index 9ead75ad..caf3d46d 100644 --- a/lib/widgets/media_context_menu.dart +++ b/lib/widgets/media_context_menu.dart @@ -27,6 +27,7 @@ import '../utils/focus_utils.dart'; import '../services/external_player_service.dart'; import '../focus/focusable_button.dart'; import '../focus/dpad_navigator.dart'; +import '../screens/match_screen.dart'; import '../screens/media_detail_screen.dart'; import '../screens/metadata_edit_screen.dart'; import '../utils/smart_deletion_handler.dart'; @@ -241,6 +242,19 @@ class MediaContextMenuState extends State { ); } + if (isAdmin && (mediaType == PlexMediaType.movie || mediaType == PlexMediaType.show)) { + menuActions.add( + _MenuAction( + value: 'match', + icon: Symbols.search_rounded, + label: metadata.isUnmatched ? t.matchScreen.match : t.matchScreen.fixMatch, + ), + ); + if (!metadata.isUnmatched) { + menuActions.add(_MenuAction(value: 'unmatch', icon: Symbols.link_off_rounded, label: t.matchScreen.unmatch)); + } + } + // Remove from Collection (only when viewing items within a collection) if (widget.collectionId != null) { menuActions.add( @@ -507,6 +521,18 @@ class MediaContextMenuState extends State { } break; + case 'match': + didNavigate = true; + if (context.mounted) { + await Navigator.push(context, MaterialPageRoute(builder: (context) => MatchScreen(metadata: metadata!))); + widget.onRefresh?.call(metadata!.ratingKey); + } + break; + + case 'unmatch': + await _handleUnmatch(context, metadata!); + break; + case 'remove_from_collection': await _handleRemoveFromCollection(context, metadata!); break; @@ -626,6 +652,33 @@ class MediaContextMenuState extends State { } } + Future _handleUnmatch(BuildContext context, PlexMetadata metadata) async { + final confirmed = await showConfirmDialog( + context, + title: t.matchScreen.unmatch, + message: t.matchScreen.unmatchConfirm, + confirmText: t.matchScreen.unmatch, + isDestructive: true, + ); + if (!confirmed || !context.mounted) return; + + final client = _getClientForItem(); + try { + final success = await client.unmatchItem(metadata.ratingKey); + if (!context.mounted) return; + if (success) { + showSuccessSnackBar(context, t.matchScreen.unmatchSuccess); + widget.onRefresh?.call(metadata.ratingKey); + } else { + showErrorSnackBar(context, t.matchScreen.unmatchFailed); + } + } catch (e) { + if (context.mounted) { + showErrorSnackBar(context, t.messages.errorLoading(error: e.toString())); + } + } + } + /// Navigate to a related item (series or season) Future _navigateToRelated( BuildContext context,