From 6d4d90e0fdafea138463c183f8732182aa4b13a9 Mon Sep 17 00:00:00 2001 From: mattsigal Date: Sat, 25 Jul 2026 01:36:05 -0700 Subject: [PATCH 1/2] Add Jellyfin Identify dialog and redirect Episode/Season Identify to parent TV Series - Add custom Identify dialog with search parameters, current metadata reference, and remote provider lookup - Add Identify action to item context menus and detail page admin controls - Redirect Identify action for TV Episode and TV Season items to target the parent TV Series - Fix FocusNode disposal crash on dialog dismissal by unfocusing FocusScope before pop - Update localization strings for Identify dialog components --- lib/l10n/app_en.arb | 15 +- lib/l10n/app_localizations.dart | 72 + lib/l10n/app_localizations_af.dart | 36 + lib/l10n/app_localizations_ar.dart | 36 + lib/l10n/app_localizations_be.dart | 36 + lib/l10n/app_localizations_bg.dart | 36 + lib/l10n/app_localizations_bn.dart | 36 + lib/l10n/app_localizations_ca.dart | 36 + lib/l10n/app_localizations_cs.dart | 36 + lib/l10n/app_localizations_cy.dart | 36 + lib/l10n/app_localizations_da.dart | 36 + lib/l10n/app_localizations_de.dart | 36 + lib/l10n/app_localizations_el.dart | 36 + lib/l10n/app_localizations_en.dart | 36 + lib/l10n/app_localizations_eo.dart | 36 + lib/l10n/app_localizations_es.dart | 36 + lib/l10n/app_localizations_et.dart | 36 + lib/l10n/app_localizations_fa.dart | 36 + lib/l10n/app_localizations_fi.dart | 36 + lib/l10n/app_localizations_fr.dart | 36 + lib/l10n/app_localizations_gl.dart | 36 + lib/l10n/app_localizations_he.dart | 36 + lib/l10n/app_localizations_hi.dart | 36 + lib/l10n/app_localizations_hr.dart | 36 + lib/l10n/app_localizations_hu.dart | 36 + lib/l10n/app_localizations_id.dart | 36 + lib/l10n/app_localizations_it.dart | 36 + lib/l10n/app_localizations_ja.dart | 36 + lib/l10n/app_localizations_kk.dart | 36 + lib/l10n/app_localizations_kn.dart | 36 + lib/l10n/app_localizations_ko.dart | 36 + lib/l10n/app_localizations_lt.dart | 36 + lib/l10n/app_localizations_lv.dart | 36 + lib/l10n/app_localizations_mk.dart | 36 + lib/l10n/app_localizations_ml.dart | 36 + lib/l10n/app_localizations_mn.dart | 36 + lib/l10n/app_localizations_nb.dart | 36 + lib/l10n/app_localizations_nl.dart | 36 + lib/l10n/app_localizations_pa.dart | 36 + lib/l10n/app_localizations_pl.dart | 36 + lib/l10n/app_localizations_pt.dart | 36 + lib/l10n/app_localizations_ro.dart | 36 + lib/l10n/app_localizations_ru.dart | 36 + lib/l10n/app_localizations_si.dart | 36 + lib/l10n/app_localizations_sk.dart | 36 + lib/l10n/app_localizations_sl.dart | 36 + lib/l10n/app_localizations_sq.dart | 36 + lib/l10n/app_localizations_sr.dart | 36 + lib/l10n/app_localizations_sv.dart | 36 + lib/l10n/app_localizations_sw.dart | 36 + lib/l10n/app_localizations_ta.dart | 36 + lib/l10n/app_localizations_te.dart | 36 + lib/l10n/app_localizations_th.dart | 36 + lib/l10n/app_localizations_tl.dart | 36 + lib/l10n/app_localizations_tr.dart | 36 + lib/l10n/app_localizations_ug.dart | 36 + lib/l10n/app_localizations_uk.dart | 36 + lib/l10n/app_localizations_vi.dart | 36 + lib/l10n/app_localizations_yue.dart | 36 + lib/l10n/app_localizations_zh.dart | 36 + .../metadata/admin_metadata_edit_screen.dart | 133 +- lib/ui/screens/detail/item_detail_screen.dart | 79 +- lib/ui/widgets/focus/context_action.dart | 27 + lib/ui/widgets/identify_dialog.dart | 1274 +++++++++++++++++ .../lib/src/api/jellyfin_admin_items_api.dart | 11 +- 65 files changed, 3571 insertions(+), 128 deletions(-) create mode 100644 lib/ui/widgets/identify_dialog.dart diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 996340bae..ece166ca1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -8945,5 +8945,18 @@ "watchlistUpdateFailed": "Couldn't update watchlist", "@watchlistUpdateFailed": { "description": "Snackbar shown when adding/removing a Seerr watchlist item fails" - } + }, + "adminSearchParameters": "Search Parameters", + "adminCurrentMetadata": "Current Metadata", + "adminLabelName": "Name", + "adminLabelYear": "Year", + "adminLabelImdbId": "IMDb Id", + "adminLabelTmdbMovieId": "TheMovieDb Movie Id", + "adminLabelTmdbBoxSetId": "TheMovieDb Box Set Id", + "adminLabelTvdbBoxSetId": "TheTVDB Box Set Id", + "adminLabelTvdbId": "TheTVDB Numerical Id", + "adminLabelTvdbSlug": "TheTVDB Slug Movie Id", + "adminReplaceImages": "Replace existing images", + "adminApply": "Apply", + "adminBackToSearch": "Back to Search Criteria" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 61554197e..cb72b6271 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -18021,6 +18021,78 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Couldn\'t update watchlist'** String get watchlistUpdateFailed; + + /// No description provided for @adminSearchParameters. + /// + /// In en, this message translates to: + /// **'Search Parameters'** + String get adminSearchParameters; + + /// No description provided for @adminCurrentMetadata. + /// + /// In en, this message translates to: + /// **'Current Metadata'** + String get adminCurrentMetadata; + + /// No description provided for @adminLabelName. + /// + /// In en, this message translates to: + /// **'Name'** + String get adminLabelName; + + /// No description provided for @adminLabelYear. + /// + /// In en, this message translates to: + /// **'Year'** + String get adminLabelYear; + + /// No description provided for @adminLabelImdbId. + /// + /// In en, this message translates to: + /// **'IMDb Id'** + String get adminLabelImdbId; + + /// No description provided for @adminLabelTmdbMovieId. + /// + /// In en, this message translates to: + /// **'TheMovieDb Movie Id'** + String get adminLabelTmdbMovieId; + + /// No description provided for @adminLabelTmdbBoxSetId. + /// + /// In en, this message translates to: + /// **'TheMovieDb Box Set Id'** + String get adminLabelTmdbBoxSetId; + + /// No description provided for @adminLabelTvdbBoxSetId. + /// + /// In en, this message translates to: + /// **'TheTVDB Box Set Id'** + String get adminLabelTvdbBoxSetId; + + /// No description provided for @adminLabelTvdbId. + /// + /// In en, this message translates to: + /// **'TheTVDB Numerical Id'** + String get adminLabelTvdbId; + + /// No description provided for @adminLabelTvdbSlug. + /// + /// In en, this message translates to: + /// **'TheTVDB Slug Movie Id'** + String get adminLabelTvdbSlug; + + /// No description provided for @adminReplaceImages. + /// + /// In en, this message translates to: + /// **'Replace existing images'** + String get adminReplaceImages; + + /// No description provided for @adminBackToSearch. + /// + /// In en, this message translates to: + /// **'Back to Search Criteria'** + String get adminBackToSearch; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_af.dart b/lib/l10n/app_localizations_af.dart index cea92de18..ce476da7c 100644 --- a/lib/l10n/app_localizations_af.dart +++ b/lib/l10n/app_localizations_af.dart @@ -10157,4 +10157,40 @@ class AppLocalizationsAf extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 464402a91..f34942040 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -10133,4 +10133,40 @@ class AppLocalizationsAr extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_be.dart b/lib/l10n/app_localizations_be.dart index fe5ae0f3c..05b937bba 100644 --- a/lib/l10n/app_localizations_be.dart +++ b/lib/l10n/app_localizations_be.dart @@ -10222,4 +10222,40 @@ class AppLocalizationsBe extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 877b27ab1..6915d6afd 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -10260,4 +10260,40 @@ class AppLocalizationsBg extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_bn.dart b/lib/l10n/app_localizations_bn.dart index f072b34d0..eba11111f 100644 --- a/lib/l10n/app_localizations_bn.dart +++ b/lib/l10n/app_localizations_bn.dart @@ -10131,4 +10131,40 @@ class AppLocalizationsBn extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_ca.dart b/lib/l10n/app_localizations_ca.dart index f5f97251f..ed88c38b5 100644 --- a/lib/l10n/app_localizations_ca.dart +++ b/lib/l10n/app_localizations_ca.dart @@ -10308,4 +10308,40 @@ class AppLocalizationsCa extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_cs.dart b/lib/l10n/app_localizations_cs.dart index 486d614b4..a7dcbabf4 100644 --- a/lib/l10n/app_localizations_cs.dart +++ b/lib/l10n/app_localizations_cs.dart @@ -10195,4 +10195,40 @@ class AppLocalizationsCs extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_cy.dart b/lib/l10n/app_localizations_cy.dart index 21d2ff062..d2701d34b 100644 --- a/lib/l10n/app_localizations_cy.dart +++ b/lib/l10n/app_localizations_cy.dart @@ -10212,4 +10212,40 @@ class AppLocalizationsCy extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_da.dart b/lib/l10n/app_localizations_da.dart index c043173e8..995b8baf5 100644 --- a/lib/l10n/app_localizations_da.dart +++ b/lib/l10n/app_localizations_da.dart @@ -10148,4 +10148,40 @@ class AppLocalizationsDa extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 41722688f..d0e86f1a1 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -10306,4 +10306,40 @@ class AppLocalizationsDe extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_el.dart b/lib/l10n/app_localizations_el.dart index 04a3f6290..5f105f260 100644 --- a/lib/l10n/app_localizations_el.dart +++ b/lib/l10n/app_localizations_el.dart @@ -10315,4 +10315,40 @@ class AppLocalizationsEl extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 85e81bf1d..0248ab48b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -10067,6 +10067,42 @@ class AppLocalizationsEn extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } /// The translations for English, as used in the United Kingdom (`en_GB`). diff --git a/lib/l10n/app_localizations_eo.dart b/lib/l10n/app_localizations_eo.dart index 34fc29747..0160d5416 100644 --- a/lib/l10n/app_localizations_eo.dart +++ b/lib/l10n/app_localizations_eo.dart @@ -10143,4 +10143,40 @@ class AppLocalizationsEo extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 75a95eebb..1cf518807 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -10272,6 +10272,42 @@ class AppLocalizationsEs extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } /// The translations for Spanish Castilian, as used in Latin America and the Caribbean (`es_419`). diff --git a/lib/l10n/app_localizations_et.dart b/lib/l10n/app_localizations_et.dart index dfd3c6ec3..932a000fa 100644 --- a/lib/l10n/app_localizations_et.dart +++ b/lib/l10n/app_localizations_et.dart @@ -10161,4 +10161,40 @@ class AppLocalizationsEt extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_fa.dart b/lib/l10n/app_localizations_fa.dart index 359112f03..b6f1718f4 100644 --- a/lib/l10n/app_localizations_fa.dart +++ b/lib/l10n/app_localizations_fa.dart @@ -10089,4 +10089,40 @@ class AppLocalizationsFa extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_fi.dart b/lib/l10n/app_localizations_fi.dart index f454dd7bb..47cd8bd1e 100644 --- a/lib/l10n/app_localizations_fi.dart +++ b/lib/l10n/app_localizations_fi.dart @@ -10179,4 +10179,40 @@ class AppLocalizationsFi extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index e0469db91..d025becfa 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -10284,4 +10284,40 @@ class AppLocalizationsFr extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_gl.dart b/lib/l10n/app_localizations_gl.dart index 9c6978ec6..f63817b67 100644 --- a/lib/l10n/app_localizations_gl.dart +++ b/lib/l10n/app_localizations_gl.dart @@ -10298,4 +10298,40 @@ class AppLocalizationsGl extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_he.dart b/lib/l10n/app_localizations_he.dart index 6e830d01c..b8b998d36 100644 --- a/lib/l10n/app_localizations_he.dart +++ b/lib/l10n/app_localizations_he.dart @@ -10010,4 +10010,40 @@ class AppLocalizationsHe extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index 95ebca0a3..eb6a88b77 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -10124,4 +10124,40 @@ class AppLocalizationsHi extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_hr.dart b/lib/l10n/app_localizations_hr.dart index f4494d26f..ee56c7575 100644 --- a/lib/l10n/app_localizations_hr.dart +++ b/lib/l10n/app_localizations_hr.dart @@ -10377,4 +10377,40 @@ class AppLocalizationsHr extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 551ab8eb2..9fafc6eac 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -10249,4 +10249,40 @@ class AppLocalizationsHu extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 81708da00..10155568f 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -10153,4 +10153,40 @@ class AppLocalizationsId extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index a257d9bcb..ecb3dd486 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -10238,4 +10238,40 @@ class AppLocalizationsIt extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 18f68442b..a957223fe 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -9837,4 +9837,40 @@ class AppLocalizationsJa extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_kk.dart b/lib/l10n/app_localizations_kk.dart index 7418bef61..a90ce159f 100644 --- a/lib/l10n/app_localizations_kk.dart +++ b/lib/l10n/app_localizations_kk.dart @@ -10199,4 +10199,40 @@ class AppLocalizationsKk extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_kn.dart b/lib/l10n/app_localizations_kn.dart index 89d4d6905..198b1f526 100644 --- a/lib/l10n/app_localizations_kn.dart +++ b/lib/l10n/app_localizations_kn.dart @@ -10223,4 +10223,40 @@ class AppLocalizationsKn extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index fb863366e..1d43cf769 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -9830,4 +9830,40 @@ class AppLocalizationsKo extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_lt.dart b/lib/l10n/app_localizations_lt.dart index 04ece38c6..ed1add099 100644 --- a/lib/l10n/app_localizations_lt.dart +++ b/lib/l10n/app_localizations_lt.dart @@ -10219,4 +10219,40 @@ class AppLocalizationsLt extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_lv.dart b/lib/l10n/app_localizations_lv.dart index a33032901..1155495c1 100644 --- a/lib/l10n/app_localizations_lv.dart +++ b/lib/l10n/app_localizations_lv.dart @@ -10216,4 +10216,40 @@ class AppLocalizationsLv extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_mk.dart b/lib/l10n/app_localizations_mk.dart index b1e53470c..b5b90ce2d 100644 --- a/lib/l10n/app_localizations_mk.dart +++ b/lib/l10n/app_localizations_mk.dart @@ -10231,4 +10231,40 @@ class AppLocalizationsMk extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_ml.dart b/lib/l10n/app_localizations_ml.dart index 7aa8898e6..1bac0891d 100644 --- a/lib/l10n/app_localizations_ml.dart +++ b/lib/l10n/app_localizations_ml.dart @@ -10276,4 +10276,40 @@ class AppLocalizationsMl extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_mn.dart b/lib/l10n/app_localizations_mn.dart index 1b91edf99..af5665a60 100644 --- a/lib/l10n/app_localizations_mn.dart +++ b/lib/l10n/app_localizations_mn.dart @@ -10173,4 +10173,40 @@ class AppLocalizationsMn extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_nb.dart b/lib/l10n/app_localizations_nb.dart index 3e0dd05a6..40e2d966f 100644 --- a/lib/l10n/app_localizations_nb.dart +++ b/lib/l10n/app_localizations_nb.dart @@ -10146,4 +10146,40 @@ class AppLocalizationsNb extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 730a6f1c5..380362b9e 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -10205,4 +10205,40 @@ class AppLocalizationsNl extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_pa.dart b/lib/l10n/app_localizations_pa.dart index 7dfe370cb..347433394 100644 --- a/lib/l10n/app_localizations_pa.dart +++ b/lib/l10n/app_localizations_pa.dart @@ -10112,4 +10112,40 @@ class AppLocalizationsPa extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index f8b2a6007..751a4ad34 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -10215,4 +10215,40 @@ class AppLocalizationsPl extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 2d12d203b..179483bb7 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -10238,6 +10238,42 @@ class AppLocalizationsPt extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } /// The translations for Portuguese, as used in Brazil (`pt_BR`). diff --git a/lib/l10n/app_localizations_ro.dart b/lib/l10n/app_localizations_ro.dart index 650f02300..24c497010 100644 --- a/lib/l10n/app_localizations_ro.dart +++ b/lib/l10n/app_localizations_ro.dart @@ -10242,4 +10242,40 @@ class AppLocalizationsRo extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 4acf9b05c..3c37c1448 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -10232,4 +10232,40 @@ class AppLocalizationsRu extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_si.dart b/lib/l10n/app_localizations_si.dart index 0479944a5..9d26e172f 100644 --- a/lib/l10n/app_localizations_si.dart +++ b/lib/l10n/app_localizations_si.dart @@ -10144,4 +10144,40 @@ class AppLocalizationsSi extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index afedf6f12..864cdf243 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -10224,4 +10224,40 @@ class AppLocalizationsSk extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index 9b9a32e99..a5e20157e 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -10218,4 +10218,40 @@ class AppLocalizationsSl extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_sq.dart b/lib/l10n/app_localizations_sq.dart index a8bef402e..cca7dbe19 100644 --- a/lib/l10n/app_localizations_sq.dart +++ b/lib/l10n/app_localizations_sq.dart @@ -10253,4 +10253,40 @@ class AppLocalizationsSq extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_sr.dart b/lib/l10n/app_localizations_sr.dart index 9842d88b2..1161dfa46 100644 --- a/lib/l10n/app_localizations_sr.dart +++ b/lib/l10n/app_localizations_sr.dart @@ -10365,4 +10365,40 @@ class AppLocalizationsSr extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index 51b0393ad..b47e60547 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -10162,4 +10162,40 @@ class AppLocalizationsSv extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_sw.dart b/lib/l10n/app_localizations_sw.dart index 9487465ae..6db89dd41 100644 --- a/lib/l10n/app_localizations_sw.dart +++ b/lib/l10n/app_localizations_sw.dart @@ -10237,4 +10237,40 @@ class AppLocalizationsSw extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_ta.dart b/lib/l10n/app_localizations_ta.dart index c522ce5d7..b6bc0cd61 100644 --- a/lib/l10n/app_localizations_ta.dart +++ b/lib/l10n/app_localizations_ta.dart @@ -10238,4 +10238,40 @@ class AppLocalizationsTa extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_te.dart b/lib/l10n/app_localizations_te.dart index 568aef1cd..0dc330aef 100644 --- a/lib/l10n/app_localizations_te.dart +++ b/lib/l10n/app_localizations_te.dart @@ -10224,4 +10224,40 @@ class AppLocalizationsTe extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_th.dart b/lib/l10n/app_localizations_th.dart index c6114384a..7624610b2 100644 --- a/lib/l10n/app_localizations_th.dart +++ b/lib/l10n/app_localizations_th.dart @@ -10075,4 +10075,40 @@ class AppLocalizationsTh extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_tl.dart b/lib/l10n/app_localizations_tl.dart index 3771164ac..2b92b1dad 100644 --- a/lib/l10n/app_localizations_tl.dart +++ b/lib/l10n/app_localizations_tl.dart @@ -10268,4 +10268,40 @@ class AppLocalizationsTl extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index d29ec263d..26296bb5b 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -10168,4 +10168,40 @@ class AppLocalizationsTr extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_ug.dart b/lib/l10n/app_localizations_ug.dart index fd65eb396..3de2472ed 100644 --- a/lib/l10n/app_localizations_ug.dart +++ b/lib/l10n/app_localizations_ug.dart @@ -10189,4 +10189,40 @@ class AppLocalizationsUg extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index fff5f7456..8239d1b52 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -10243,4 +10243,40 @@ class AppLocalizationsUk extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_vi.dart b/lib/l10n/app_localizations_vi.dart index 8520d6749..5e5a30f40 100644 --- a/lib/l10n/app_localizations_vi.dart +++ b/lib/l10n/app_localizations_vi.dart @@ -10156,4 +10156,40 @@ class AppLocalizationsVi extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } diff --git a/lib/l10n/app_localizations_yue.dart b/lib/l10n/app_localizations_yue.dart index b3eeca030..db95cd017 100644 --- a/lib/l10n/app_localizations_yue.dart +++ b/lib/l10n/app_localizations_yue.dart @@ -9749,6 +9749,42 @@ class AppLocalizationsYue extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } /// The translations for Yue Chinese Cantonese, as used in China (`yue_CN`). diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 50c2f9961..afd25c61c 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -9745,6 +9745,42 @@ class AppLocalizationsZh extends AppLocalizations { @override String get watchlistUpdateFailed => 'Couldn\'t update watchlist'; + + @override + String get adminSearchParameters => 'Search Parameters'; + + @override + String get adminCurrentMetadata => 'Current Metadata'; + + @override + String get adminLabelName => 'Name'; + + @override + String get adminLabelYear => 'Year'; + + @override + String get adminLabelImdbId => 'IMDb Id'; + + @override + String get adminLabelTmdbMovieId => 'TheMovieDb Movie Id'; + + @override + String get adminLabelTmdbBoxSetId => 'TheMovieDb Box Set Id'; + + @override + String get adminLabelTvdbBoxSetId => 'TheTVDB Box Set Id'; + + @override + String get adminLabelTvdbId => 'TheTVDB Numerical Id'; + + @override + String get adminLabelTvdbSlug => 'TheTVDB Slug Movie Id'; + + @override + String get adminReplaceImages => 'Replace existing images'; + + @override + String get adminBackToSearch => 'Back to Search Criteria'; } /// The translations for Chinese, using the Han script (`zh_Hant`). diff --git a/lib/ui/screens/admin/metadata/admin_metadata_edit_screen.dart b/lib/ui/screens/admin/metadata/admin_metadata_edit_screen.dart index 7ce492891..50af97068 100644 --- a/lib/ui/screens/admin/metadata/admin_metadata_edit_screen.dart +++ b/lib/ui/screens/admin/metadata/admin_metadata_edit_screen.dart @@ -10,6 +10,7 @@ import '../../../../l10n/app_localizations.dart'; import '../../../../util/image_mime.dart'; import '../../../../util/platform_detection.dart'; import '../../../widgets/adaptive/adaptive_dialog.dart'; +import '../../../widgets/identify_dialog.dart'; import '../../detail/modern/widgets/details_tab_bar.dart'; import '../widgets/admin_form_styles.dart'; @@ -524,128 +525,26 @@ class _AdminMetadataEditScreenState extends State { } Future _searchAndApplyRemote() async { - final l10n = AppLocalizations.of(context); final searchType = (_raw['Type'] ?? '').toString(); - if (searchType.isEmpty) return; - - final queryController = - TextEditingController(text: (_raw['Name'] ?? '').toString()); - final query = await showDialog( - context: context, - builder: (ctx) => AlertDialog.adaptive( - title: Text(l10n.adminMetadataIdentify), - content: TextField( - controller: queryController, - autofocus: true, - decoration: adminInputDecoration(label: l10n.name), - onSubmitted: (v) => Navigator.pop(ctx, v.trim()), - ), - actions: [ - adaptiveDialogAction( - onPressed: () => Navigator.pop(ctx), - child: Text(l10n.cancel), - ), - FilledButton( - onPressed: () => Navigator.pop(ctx, queryController.text.trim()), - child: Text(l10n.search), - ), - ], - ), - ); - queryController.dispose(); - - if (query == null || query.isEmpty || !mounted) return; - - final searchInfo = {'Name': query}; + final name = (_raw['Name'] ?? '').toString(); final year = int.tryParse((_raw['ProductionYear'] ?? '').toString()); - if (year != null) searchInfo['Year'] = year; - final providerIds = _raw['ProviderIds']; - if (providerIds is Map && providerIds.isNotEmpty) { - searchInfo['ProviderIds'] = Map.from(providerIds); - } - - try { - final results = await _api.searchRemote(searchType, { - 'SearchInfo': searchInfo, - 'ItemId': widget.itemId, - }); - - if (!mounted) return; - if (results.isEmpty) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(l10n.adminNoRemoteMatches)), - ); - return; - } + final path = _raw['Path']?.toString(); + final providerIds = _raw['ProviderIds'] is Map + ? Map.from(_raw['ProviderIds']) + : null; - final selected = await showDialog>( - context: context, - builder: (ctx) => AlertDialog.adaptive( - title: Text(l10n.adminRemoteResults), - content: SizedBox( - width: (MediaQuery.sizeOf(ctx).width - 32).clamp(280.0, 560.0), - height: (MediaQuery.sizeOf(ctx).height * 0.6).clamp(240.0, 520.0), - child: ListView.builder( - shrinkWrap: true, - itemCount: results.length, - itemBuilder: (context, index) { - final item = results[index]; - final name = (item['Name'] ?? l10n.unknown).toString(); - final resultYear = item['ProductionYear']?.toString(); - final overview = (item['Overview'] ?? '').toString(); - final provider = - (item['SearchProviderName'] ?? item['ProviderName'] ?? '') - .toString(); - final imageUrl = item['ImageUrl']?.toString(); - final subtitle = overview.isNotEmpty ? overview : provider; - return ListTile( - leading: imageUrl != null && imageUrl.isNotEmpty - ? ClipRRect( - borderRadius: BorderRadius.circular(4), - child: Image.network( - imageUrl, - width: 40, - height: 60, - fit: BoxFit.cover, - errorBuilder: (_, _, _) => - const Icon(Icons.movie_outlined), - ), - ) - : const Icon(Icons.movie_outlined), - title: Text(resultYear != null && resultYear.isNotEmpty - ? '$name ($resultYear)' - : name), - subtitle: subtitle.isEmpty - ? null - : Text(subtitle, - maxLines: 2, overflow: TextOverflow.ellipsis), - isThreeLine: overview.isNotEmpty, - onTap: () => Navigator.pop(ctx, item), - ); - }, - ), - ), - actions: [ - adaptiveDialogAction( - onPressed: () => Navigator.pop(ctx), - child: Text(l10n.cancel), - ), - ], - ), - ); + final applied = await IdentifyDialog.show( + context, + itemId: widget.itemId, + itemType: searchType, + itemName: name, + itemYear: year, + itemPath: path, + providerIds: providerIds, + ); - if (selected == null || !mounted) return; - await _api.applyRemoteSearchResult(widget.itemId, selected); - if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(l10n.adminRemoteMetadataApplied))); + if (applied == true && mounted) { await _load(); - } catch (e) { - if (!mounted) return; - ScaffoldMessenger.of( - context, - ).showSnackBar(SnackBar(content: Text(l10n.adminRemoteSearchFailed(e.toString())))); } } diff --git a/lib/ui/screens/detail/item_detail_screen.dart b/lib/ui/screens/detail/item_detail_screen.dart index ec682bff6..294619a7f 100644 --- a/lib/ui/screens/detail/item_detail_screen.dart +++ b/lib/ui/screens/detail/item_detail_screen.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:ui'; import '../../widgets/offline_aware_image.dart'; +import '../../widgets/identify_dialog.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -6572,10 +6573,14 @@ class DetailActionButtonsState extends State { onKeyEvent: (_, event) { if (isActivateKey(event)) { Navigator.of(dialogCtx).pop(); - ChangeArtworkDialog.show(context, item: item).then(( - changed, - ) { - if (changed == true) { + IdentifyDialog.show( + context, + itemId: item.id, + itemType: item.type, + itemName: item.name, + itemYear: item.productionYear, + ).then((applied) { + if (applied == true) { viewModel.load(); } }); @@ -6589,11 +6594,14 @@ class DetailActionButtonsState extends State { return InkWell( onTap: () async { Navigator.of(dialogCtx).pop(); - final changed = await ChangeArtworkDialog.show( + final applied = await IdentifyDialog.show( context, - item: item, + itemId: item.id, + itemType: item.type, + itemName: item.name, + itemYear: item.productionYear, ); - if (changed == true) { + if (applied == true) { viewModel.load(); } }, @@ -6608,10 +6616,10 @@ class DetailActionButtonsState extends State { ), child: Row( children: [ - const Icon(Icons.image, color: Colors.white70), + const Icon(Icons.search, color: Colors.white70), const SizedBox(width: 12), Text( - l10n.changeArtwork, + l10n.adminMetadataIdentify, style: const TextStyle(color: Colors.white), ), ], @@ -6668,6 +6676,59 @@ class DetailActionButtonsState extends State { }, ), ), + Focus( + onKeyEvent: (_, event) { + if (isActivateKey(event)) { + Navigator.of(dialogCtx).pop(); + ChangeArtworkDialog.show(context, item: item).then(( + changed, + ) { + if (changed == true) { + viewModel.load(); + } + }); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: Builder( + builder: (buttonCtx) { + final hasFocus = Focus.of(buttonCtx).hasFocus; + return InkWell( + onTap: () async { + Navigator.of(dialogCtx).pop(); + final changed = await ChangeArtworkDialog.show( + context, + item: item, + ); + if (changed == true) { + viewModel.load(); + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + decoration: BoxDecoration( + color: hasFocus ? Colors.white12 : Colors.transparent, + borderRadius: AppRadius.circular(8), + ), + child: Row( + children: [ + const Icon(Icons.image, color: Colors.white70), + const SizedBox(width: 12), + Text( + l10n.changeArtwork, + style: const TextStyle(color: Colors.white), + ), + ], + ), + ), + ); + }, + ), + ), if (item.canDelete) Focus( onKeyEvent: (_, event) { diff --git a/lib/ui/widgets/focus/context_action.dart b/lib/ui/widgets/focus/context_action.dart index ab8f06fb3..f3ec77631 100644 --- a/lib/ui/widgets/focus/context_action.dart +++ b/lib/ui/widgets/focus/context_action.dart @@ -13,6 +13,7 @@ import '../../navigation/destinations.dart'; import '../add_to_collection_dialog.dart'; import '../add_to_playlist_dialog.dart'; import '../change_artwork_dialog.dart'; +import '../identify_dialog.dart'; class ItemContextAction { final IconData icon; @@ -202,6 +203,32 @@ List contextActionsFor( } }, )); + + actions.add(ItemContextAction( + icon: Icons.search_outlined, + label: l10n.adminMetadataIdentify, + onSelect: () async { + if (!context.mounted) return; + final isTVChild = item.type == 'Episode' || item.type == 'Season'; + final hasSeriesId = item.seriesId != null && item.seriesId!.isNotEmpty; + final targetItemId = (isTVChild && hasSeriesId) ? item.seriesId! : item.id; + final targetItemType = (isTVChild && hasSeriesId) ? 'Series' : item.type; + final targetItemName = (isTVChild && hasSeriesId) + ? (item.seriesName ?? item.name) + : item.name; + + final applied = await IdentifyDialog.show( + context, + itemId: targetItemId, + itemType: targetItemType, + itemName: targetItemName, + itemYear: isTVChild ? null : item.productionYear, + ); + if (applied == true) { + onChanged?.call(); + } + }, + )); } } diff --git a/lib/ui/widgets/identify_dialog.dart b/lib/ui/widgets/identify_dialog.dart new file mode 100644 index 000000000..4ee7adb10 --- /dev/null +++ b/lib/ui/widgets/identify_dialog.dart @@ -0,0 +1,1274 @@ +import 'package:custom_tv_text_field/custom_tv_text_field.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get_it/get_it.dart'; +import 'package:moonfin_design/moonfin_design.dart'; +import 'package:server_core/server_core.dart'; + +import '../../l10n/app_localizations.dart'; +import '../../preference/user_preferences.dart'; +import '../../util/platform_detection.dart'; +import 'adaptive/adaptive_dialog.dart'; +import 'focus/focusable_button.dart'; + +class IdentifyDialog extends StatefulWidget { + final String itemId; + final String? itemType; + final String? itemName; + final int? itemYear; + final String? itemPath; + final Map? providerIds; + + const IdentifyDialog({ + super.key, + required this.itemId, + this.itemType, + this.itemName, + this.itemYear, + this.itemPath, + this.providerIds, + }); + + static Future show( + BuildContext context, { + required String itemId, + String? itemType, + String? itemName, + int? itemYear, + String? itemPath, + Map? providerIds, + }) { + return showDialog( + context: context, + builder: (ctx) => IdentifyDialog( + itemId: itemId, + itemType: itemType, + itemName: itemName, + itemYear: itemYear, + itemPath: itemPath, + providerIds: providerIds, + ), + ); + } + + @override + State createState() => _IdentifyDialogState(); +} + +class _IdentifyDialogState extends State { + late final AdminItemsApi _adminApi; + late final ItemsApi _itemsApi; + late final UserPreferences _prefs; + + bool _loadingItem = false; + bool _searching = false; + bool _applying = false; + + late String _targetItemId; + String? _path; + String _searchType = 'Movie'; + + List>? _searchResults; + + // Reference Metadata Values (Current Item) + String? _refName; + String? _refYear; + String? _refImdb; + String? _refTmdbMovie; + String? _refTmdbBoxSet; + String? _refTvdbBoxSet; + String? _refTvdbId; + String? _refTvdbSlug; + + // Editable Search Parameter Controllers (Blank by default) + late final TextEditingController _nameController; + late final TextEditingController _yearController; + late final TextEditingController _imdbController; + late final TextEditingController _tmdbMovieController; + late final TextEditingController _tmdbBoxSetController; + late final TextEditingController _tvdbBoxSetController; + late final TextEditingController _tvdbIdController; + late final TextEditingController _tvdbSlugController; + + // FocusNodes and ScrollController for TV Navigation + final ScrollController _scrollController = ScrollController(); + + final FocusNode _closeButtonFocusNode = FocusNode(debugLabel: 'identify-close-btn'); + final FocusNode _cancelButtonFocusNode = FocusNode(debugLabel: 'identify-cancel-btn'); + final FocusNode _searchButtonFocusNode = FocusNode(debugLabel: 'identify-search-btn'); + + final List _fieldFocusNodes = + List.generate(8, (i) => FocusNode(debugLabel: 'identify-field-$i')); + final List _arrowFocusNodes = + List.generate(8, (i) => FocusNode(debugLabel: 'identify-arrow-$i')); + final List> _tvFieldKeys = + List.generate(8, (_) => GlobalKey()); + + List? _resultFocusNodes; + + @override + void initState() { + super.initState(); + final client = GetIt.instance(); + _adminApi = client.adminItemsApi; + _itemsApi = client.itemsApi; + _prefs = GetIt.instance(); + + _targetItemId = widget.itemId; + final isTVChild = widget.itemType == 'Episode' || widget.itemType == 'Season'; + _searchType = isTVChild ? 'Series' : (widget.itemType ?? 'Movie'); + _path = widget.itemPath; + + final initialProviders = widget.providerIds ?? const {}; + + if (!isTVChild) { + _refName = widget.itemName; + _refYear = widget.itemYear?.toString(); + _refImdb = initialProviders['Imdb']?.toString(); + _refTmdbMovie = initialProviders['Tmdb']?.toString(); + _refTmdbBoxSet = initialProviders['TmdbBoxSet']?.toString(); + _refTvdbBoxSet = initialProviders['TvdbBoxSet']?.toString(); + _refTvdbId = initialProviders['Tvdb']?.toString(); + _refTvdbSlug = initialProviders['TvdbSlug']?.toString(); + } + + // Search controllers start BLANK by default + _nameController = TextEditingController(text: ''); + _yearController = TextEditingController(text: ''); + _imdbController = TextEditingController(text: ''); + _tmdbMovieController = TextEditingController(text: ''); + _tmdbBoxSetController = TextEditingController(text: ''); + _tvdbBoxSetController = TextEditingController(text: ''); + _tvdbIdController = TextEditingController(text: ''); + _tvdbSlugController = TextEditingController(text: ''); + + // Add listeners to auto-scroll focused rows into the visible viewport on TV + for (int i = 0; i < 8; i++) { + final idx = i; + _fieldFocusNodes[idx].addListener(() { + if (_fieldFocusNodes[idx].hasFocus) { + _scrollToIndex(idx); + } + }); + _arrowFocusNodes[idx].addListener(() { + if (_arrowFocusNodes[idx].hasFocus) { + _scrollToIndex(idx); + } + }); + } + + _fetchItemDetailsIfNeeded(); + } + + void _closeDialog([bool? result]) { + FocusScope.of(context).unfocus(); + if (mounted) { + Navigator.of(context).pop(result); + } + } + + @override + void dispose() { + _nameController.dispose(); + _yearController.dispose(); + _imdbController.dispose(); + _tmdbMovieController.dispose(); + _tmdbBoxSetController.dispose(); + _tvdbBoxSetController.dispose(); + _tvdbIdController.dispose(); + _tvdbSlugController.dispose(); + + _scrollController.dispose(); + + _closeButtonFocusNode.unfocus(); + _closeButtonFocusNode.dispose(); + + _cancelButtonFocusNode.unfocus(); + _cancelButtonFocusNode.dispose(); + + _searchButtonFocusNode.unfocus(); + _searchButtonFocusNode.dispose(); + + for (final fn in _fieldFocusNodes) { + fn.unfocus(); + fn.dispose(); + } + for (final fn in _arrowFocusNodes) { + fn.unfocus(); + fn.dispose(); + } + _clearResultFocusNodes(); + super.dispose(); + } + + void _clearResultFocusNodes() { + if (_resultFocusNodes != null) { + for (final fn in _resultFocusNodes!) { + fn.unfocus(); + fn.dispose(); + } + _resultFocusNodes = null; + } + } + + void _setupResultFocusNodes(int count) { + _clearResultFocusNodes(); + _resultFocusNodes = + List.generate(count, (i) => FocusNode(debugLabel: 'identify-result-$i')); + for (int i = 0; i < count; i++) { + final idx = i; + _resultFocusNodes![idx].addListener(() { + if (_resultFocusNodes != null && + idx < _resultFocusNodes!.length && + _resultFocusNodes![idx].hasFocus) { + _scrollToResultIndex(idx); + } + }); + } + } + + void _scrollToIndex(int index) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final fn = _fieldFocusNodes[index]; + final fnCtx = fn.context ?? _arrowFocusNodes[index].context; + if (fnCtx != null && fnCtx.mounted) { + Scrollable.ensureVisible( + fnCtx, + alignment: 0.35, + duration: const Duration(milliseconds: 150), + curve: Curves.easeOutCubic, + ); + } + }); + } + + void _scrollToResultIndex(int index) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted || _resultFocusNodes == null || index >= _resultFocusNodes!.length) return; + final fnCtx = _resultFocusNodes![index].context; + if (fnCtx != null && fnCtx.mounted) { + Scrollable.ensureVisible( + fnCtx, + alignment: 0.35, + duration: const Duration(milliseconds: 150), + curve: Curves.easeOutCubic, + ); + } + }); + } + + Future _fetchItemDetailsIfNeeded() async { + setState(() => _loadingItem = true); + try { + var raw = await _itemsApi.getItem(widget.itemId); + + if ((raw['Type'] == 'Episode' || raw['Type'] == 'Season') && + raw['SeriesId'] != null && + raw['SeriesId'].toString().isNotEmpty) { + _targetItemId = raw['SeriesId'].toString(); + _searchType = 'Series'; + try { + raw = await _itemsApi.getItem(_targetItemId); + } catch (_) {} + } + + if (!mounted) return; + setState(() { + _refName = raw['Name']?.toString(); + _refYear = raw['ProductionYear']?.toString(); + if (raw['Path'] != null && raw['Path'].toString().isNotEmpty) { + _path = raw['Path'].toString(); + } + if (raw['Type'] != null && raw['Type'].toString().isNotEmpty) { + _searchType = raw['Type'].toString(); + } + final pIds = raw['ProviderIds']; + if (pIds is Map) { + _refImdb = pIds['Imdb']?.toString(); + _refTmdbMovie = pIds['Tmdb']?.toString(); + _refTmdbBoxSet = pIds['TmdbBoxSet']?.toString(); + _refTvdbBoxSet = pIds['TvdbBoxSet']?.toString(); + _refTvdbId = pIds['Tvdb']?.toString(); + _refTvdbSlug = pIds['TvdbSlug']?.toString(); + } + }); + } catch (_) { + } finally { + if (mounted) { + setState(() => _loadingItem = false); + } + } + } + + Future _performSearch() async { + final l10n = AppLocalizations.of(context); + setState(() { + _searching = true; + _searchResults = null; + }); + + final nameVal = _nameController.text.trim(); + final yearVal = int.tryParse(_yearController.text.trim()); + + final providerIds = {}; + + var imdbVal = _imdbController.text.trim(); + if (imdbVal.isNotEmpty) { + if (RegExp(r'^\d+$').hasMatch(imdbVal)) { + imdbVal = 'tt$imdbVal'; + } + providerIds['Imdb'] = imdbVal; + providerIds['IMDb'] = imdbVal; + providerIds['imdb'] = imdbVal; + } + + final tmdbVal = _tmdbMovieController.text.trim(); + if (tmdbVal.isNotEmpty) { + providerIds['Tmdb'] = tmdbVal; + providerIds['TMDB'] = tmdbVal; + providerIds['tmdb'] = tmdbVal; + } + + final tmdbBoxSetVal = _tmdbBoxSetController.text.trim(); + if (tmdbBoxSetVal.isNotEmpty) { + providerIds['TmdbBoxSet'] = tmdbBoxSetVal; + providerIds['TMDBBoxSet'] = tmdbBoxSetVal; + } + + final tvdbBoxSetVal = _tvdbBoxSetController.text.trim(); + if (tvdbBoxSetVal.isNotEmpty) { + providerIds['TvdbBoxSet'] = tvdbBoxSetVal; + providerIds['TVDBBoxSet'] = tvdbBoxSetVal; + } + + final tvdbVal = _tvdbIdController.text.trim(); + if (tvdbVal.isNotEmpty) { + providerIds['Tvdb'] = tvdbVal; + providerIds['TVDB'] = tvdbVal; + providerIds['tvdb'] = tvdbVal; + } + + final tvdbSlugVal = _tvdbSlugController.text.trim(); + if (tvdbSlugVal.isNotEmpty) { + providerIds['TvdbSlug'] = tvdbSlugVal; + providerIds['TVDBSlug'] = tvdbSlugVal; + } + + final searchInfo = { + 'Name': nameVal, + 'Year': ?yearVal, + if (providerIds.isNotEmpty) 'ProviderIds': providerIds, + }; + + debugPrint('--> [IdentifyRemoteSearch] Executing search for $_searchType | ItemId: $_targetItemId'); + debugPrint('--> [IdentifyRemoteSearch] SearchInfo: $searchInfo'); + + try { + List> results = await _adminApi.searchRemote(_searchType, { + 'SearchInfo': searchInfo, + 'ItemId': _targetItemId, + 'IncludeDisabledProviders': false, + }); + + // Fallback 1: If TMDB/IMDb is present, try setting explicit SearchProviderName + if (results.isEmpty && (providerIds.containsKey('Tmdb') || providerIds.containsKey('Imdb'))) { + final altInfoProvider = { + ...searchInfo, + 'SearchProviderName': 'TheMovieDb', + }; + debugPrint('--> [IdentifyRemoteSearch] Retry with SearchProviderName: TheMovieDb'); + results = await _adminApi.searchRemote(_searchType, { + 'SearchInfo': altInfoProvider, + 'ItemId': _targetItemId, + 'IncludeDisabledProviders': false, + }); + } + + // Fallback 2: Try ProviderIds alone without Name + if (results.isEmpty && providerIds.isNotEmpty && nameVal.isNotEmpty) { + final altInfoA = { + 'Name': '', + 'Year': ?yearVal, + 'ProviderIds': providerIds, + }; + debugPrint('--> [IdentifyRemoteSearch] Retry with ProviderIds alone (empty Name)'); + results = await _adminApi.searchRemote(_searchType, { + 'SearchInfo': altInfoA, + 'ItemId': _targetItemId, + 'IncludeDisabledProviders': false, + }); + } + + // Fallback 3: Try Name (+ Year) alone + if (results.isEmpty && nameVal.isNotEmpty) { + final altInfoB = { + 'Name': nameVal, + 'Year': ?yearVal, + }; + debugPrint('--> [IdentifyRemoteSearch] Retry with Name + Year alone'); + results = await _adminApi.searchRemote(_searchType, { + 'SearchInfo': altInfoB, + 'ItemId': _targetItemId, + 'IncludeDisabledProviders': false, + }); + } + + debugPrint('<-- [IdentifyRemoteSearch] Results received: ${results.length}'); + if (!mounted) return; + + if (results.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.adminNoRemoteMatches)), + ); + setState(() { + _searching = false; + }); + } else { + _setupResultFocusNodes(results.length); + setState(() { + _searchResults = results; + _searching = false; + }); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && _resultFocusNodes != null && _resultFocusNodes!.isNotEmpty) { + _resultFocusNodes![0].requestFocus(); + } + }); + } + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.adminRemoteSearchFailed('$e'))), + ); + setState(() => _searching = false); + } + } + + Future _applyResult(Map result) async { + final l10n = AppLocalizations.of(context); + bool replaceAllImages = true; + + final confirmed = await showDialog( + context: context, + builder: (ctx) => StatefulBuilder( + builder: (dialogCtx, setDialogState) => AlertDialog.adaptive( + title: Text(l10n.adminRemoteResults), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + (result['Name'] ?? l10n.unknown).toString(), + style: Theme.of(ctx).textTheme.titleMedium, + ), + const SizedBox(height: 16), + FocusableButton( + onPressed: () { + setDialogState(() { + replaceAllImages = !replaceAllImages; + }); + }, + borderRadius: 8, + padding: const EdgeInsets.symmetric(vertical: 4, horizontal: 4), + child: Row( + children: [ + Checkbox( + value: replaceAllImages, + onChanged: (val) { + setDialogState(() { + replaceAllImages = val ?? true; + }); + }, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + l10n.adminReplaceImages, + style: Theme.of(ctx).textTheme.bodyMedium, + ), + ), + ], + ), + ), + ], + ), + actions: [ + adaptiveDialogAction( + onPressed: () => Navigator.pop(ctx, false), + child: Text(l10n.cancel), + ), + FilledButton( + onPressed: () => Navigator.pop(ctx, true), + child: Text(l10n.adminApply), + ), + ], + ), + ), + ); + + if (confirmed != true || !mounted) return; + + setState(() => _applying = true); + try { + await _adminApi.applyRemoteSearchResult(_targetItemId, result); + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.adminRemoteMetadataApplied)), + ); + _closeDialog(true); + } catch (e) { + if (!mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(l10n.adminRemoteSearchFailed('$e'))), + ); + } finally { + if (mounted) { + setState(() => _applying = false); + } + } + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context); + final width = (MediaQuery.sizeOf(context).width - 32).clamp(340.0, 780.0); + final height = (MediaQuery.sizeOf(context).height * 0.92).clamp(420.0, 840.0); + + return Dialog( + backgroundColor: theme.colorScheme.surface, + shape: RoundedRectangleBorder( + borderRadius: AppRadius.circular(16), + ), + child: SizedBox( + width: width, + height: height, + child: Padding( + padding: const EdgeInsets.fromLTRB(20, 14, 20, 10), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Dialog Header + Row( + children: [ + if (_searchResults != null) + IconButton( + icon: const Icon(Icons.arrow_back), + onPressed: () { + _clearResultFocusNodes(); + setState(() => _searchResults = null); + }, + tooltip: l10n.adminBackToSearch, + ), + Expanded( + child: Text( + _searchResults == null + ? l10n.adminMetadataIdentify + : l10n.adminRemoteResults, + style: theme.textTheme.headlineSmall, + ), + ), + // Requirement 1 & 2: Pronounced X focus & Down navigation & Select/Enter pop + Focus( + focusNode: _closeButtonFocusNode, + onKeyEvent: (_, event) { + if (event is KeyDownEvent) { + final key = event.logicalKey; + if (key == LogicalKeyboardKey.select || + key == LogicalKeyboardKey.enter || + key == LogicalKeyboardKey.gameButtonA || + key == LogicalKeyboardKey.space) { + _closeDialog(false); + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowDown) { + if (_searchResults != null && + _resultFocusNodes != null && + _resultFocusNodes!.isNotEmpty) { + _resultFocusNodes![0].requestFocus(); + } else { + _arrowFocusNodes[0].requestFocus(); + } + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: Builder( + builder: (btnCtx) { + final hasFocus = Focus.of(btnCtx).hasFocus; + return GestureDetector( + onTap: () => _closeDialog(false), + child: InkWell( + onTap: () => _closeDialog(false), + borderRadius: AppRadius.circular(20), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: hasFocus + ? theme.colorScheme.primary.withValues(alpha: 0.25) + : Colors.transparent, + border: Border.all( + color: hasFocus + ? theme.colorScheme.primary + : Colors.transparent, + width: 2.5, + ), + boxShadow: hasFocus + ? [ + BoxShadow( + color: theme.colorScheme.primary + .withValues(alpha: 0.6), + blurRadius: 10, + spreadRadius: 2, + ) + ] + : null, + ), + child: Icon( + Icons.close, + color: hasFocus + ? theme.colorScheme.primary + : theme.colorScheme.onSurface, + size: 20, + ), + ), + ), + ); + }, + ), + ), + ], + ), + const Divider(height: 12), + + // Dialog Body + Expanded( + child: _loadingItem || _applying + ? const Center(child: CircularProgressIndicator()) + : _searchResults != null + ? _buildResultsList(context) + : _buildSearchForm(context), + ), + + // Requirement 5: Compact Button Bar + if (_searchResults == null) ...[ + Padding( + padding: const EdgeInsets.only(top: 4, bottom: 2), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Focus( + focusNode: _cancelButtonFocusNode, + onKeyEvent: (_, event) { + if (event is KeyDownEvent) { + final key = event.logicalKey; + if (key == LogicalKeyboardKey.select || + key == LogicalKeyboardKey.enter || + key == LogicalKeyboardKey.gameButtonA || + key == LogicalKeyboardKey.space) { + _closeDialog(false); + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowUp) { + _fieldFocusNodes[7].requestFocus(); + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowRight) { + _searchButtonFocusNode.requestFocus(); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: Builder( + builder: (btnCtx) { + final hasFocus = Focus.of(btnCtx).hasFocus; + return TextButton( + onPressed: () => _closeDialog(false), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), + minimumSize: const Size(60, 30), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: hasFocus + ? theme.colorScheme.primary.withValues(alpha: 0.15) + : null, + side: hasFocus + ? BorderSide(color: theme.colorScheme.primary, width: 1.5) + : null, + ), + child: Text(l10n.cancel), + ); + }, + ), + ), + const SizedBox(width: 10), + Focus( + focusNode: _searchButtonFocusNode, + onKeyEvent: (_, event) { + if (event is KeyDownEvent) { + final key = event.logicalKey; + if (key == LogicalKeyboardKey.select || + key == LogicalKeyboardKey.enter || + key == LogicalKeyboardKey.gameButtonA || + key == LogicalKeyboardKey.space) { + if (!_searching) _performSearch(); + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowUp) { + _arrowFocusNodes[7].requestFocus(); + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowLeft) { + _cancelButtonFocusNode.requestFocus(); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: Builder( + builder: (btnCtx) { + final hasFocus = Focus.of(btnCtx).hasFocus; + return FilledButton.icon( + onPressed: _searching ? null : _performSearch, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 4), + minimumSize: const Size(70, 30), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: hasFocus + ? const BorderSide(color: Colors.white, width: 2.0) + : null, + ), + icon: _searching + ? const SizedBox( + width: 14, + height: 14, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.search, size: 16), + label: Text(l10n.search), + ); + }, + ), + ), + ], + ), + ), + ], + ], + ), + ), + ), + ); + } + + Widget _buildSearchForm(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context); + return SingleChildScrollView( + controller: _scrollController, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_path != null && _path!.isNotEmpty) ...[ + Container( + width: double.infinity, + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: AppRadius.circular(8), + ), + child: SelectableText( + 'Path: $_path', + style: theme.textTheme.bodySmall?.copyWith( + fontFamily: 'monospace', + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ), + const SizedBox(height: 12), + ], + + // Column Headers + Padding( + padding: const EdgeInsets.only(bottom: 10), + child: Row( + children: [ + Expanded( + flex: 3, + child: Text( + l10n.adminSearchParameters, + style: theme.textTheme.titleSmall?.copyWith( + color: theme.colorScheme.primary, + fontWeight: FontWeight.bold, + ), + ), + ), + const SizedBox(width: 48), // Arrow button spacing + Expanded( + flex: 2, + child: Text( + l10n.adminCurrentMetadata, + style: theme.textTheme.titleSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + fontWeight: FontWeight.bold, + ), + ), + ), + ], + ), + ), + + _buildFieldRow( + index: 0, + label: l10n.adminLabelName, + controller: _nameController, + referenceValue: _refName, + ), + _buildFieldRow( + index: 1, + label: l10n.adminLabelYear, + controller: _yearController, + referenceValue: _refYear, + keyboardType: TextInputType.number, + ), + _buildFieldRow( + index: 2, + label: l10n.adminLabelImdbId, + controller: _imdbController, + referenceValue: _refImdb, + ), + _buildFieldRow( + index: 3, + label: l10n.adminLabelTmdbMovieId, + controller: _tmdbMovieController, + referenceValue: _refTmdbMovie, + ), + _buildFieldRow( + index: 4, + label: l10n.adminLabelTmdbBoxSetId, + controller: _tmdbBoxSetController, + referenceValue: _refTmdbBoxSet, + ), + _buildFieldRow( + index: 5, + label: l10n.adminLabelTvdbBoxSetId, + controller: _tvdbBoxSetController, + referenceValue: _refTvdbBoxSet, + ), + _buildFieldRow( + index: 6, + label: l10n.adminLabelTvdbId, + controller: _tvdbIdController, + referenceValue: _refTvdbId, + ), + _buildFieldRow( + index: 7, + label: l10n.adminLabelTvdbSlug, + controller: _tvdbSlugController, + referenceValue: _refTvdbSlug, + ), + ], + ), + ); + } + + Widget _buildFieldRow({ + required int index, + required String label, + required TextEditingController controller, + required String? referenceValue, + TextInputType? keyboardType, + }) { + final theme = Theme.of(context); + final hasReference = referenceValue != null && referenceValue.trim().isNotEmpty; + final fieldFocusNode = _fieldFocusNodes[index]; + final arrowFocusNode = _arrowFocusNodes[index]; + final tvFieldKey = _tvFieldKeys[index]; + + return Padding( + padding: const EdgeInsets.only(bottom: 10.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + // Left Column: Search Input Field (Uses CustomTVTextField on TV) + Expanded( + flex: 3, + child: Focus( + focusNode: fieldFocusNode, + onKeyEvent: (_, event) { + if (event is KeyDownEvent) { + final key = event.logicalKey; + if (key == LogicalKeyboardKey.select || + key == LogicalKeyboardKey.enter || + key == LogicalKeyboardKey.gameButtonA || + key == LogicalKeyboardKey.space) { + if (!fieldFocusNode.hasFocus) { + fieldFocusNode.requestFocus(); + } + tvFieldKey.currentState?.openKeyboard(); + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowRight) { + arrowFocusNode.requestFocus(); + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowDown) { + if (index < 7) { + _fieldFocusNodes[index + 1].requestFocus(); + } else { + _cancelButtonFocusNode.requestFocus(); + } + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowUp) { + if (index > 0) { + _fieldFocusNodes[index - 1].requestFocus(); + } else { + _closeButtonFocusNode.requestFocus(); + } + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: ListenableBuilder( + listenable: fieldFocusNode, + builder: (ctx, _) { + if (PlatformDetection.isTV) { + final preferSystemIme = + _prefs.get(UserPreferences.preferSystemImeKeyboard); + return GestureDetector( + onTap: () { + if (!fieldFocusNode.hasFocus) { + fieldFocusNode.requestFocus(); + } + tvFieldKey.currentState?.openKeyboard(); + }, + child: CustomTVTextField( + key: tvFieldKey, + controller: controller, + isFocused: fieldFocusNode.hasFocus, + inputPurpose: InputPurpose.text, + keyboardType: keyboardType == TextInputType.number + ? KeyboardType.numeric + : KeyboardType.alphabetic, + preferSystemIme: preferSystemIme, + hint: label, + filled: true, + fillColor: theme.colorScheme.surfaceContainerHighest + .withValues(alpha: 0.4), + borderColor: theme.colorScheme.outlineVariant + .withValues(alpha: 0.4), + focusedBorderColor: theme.colorScheme.primary, + hintStyle: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant + .withValues(alpha: 0.6), + ), + textStyle: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface, + ), + popParentOnKeyboardClose: false, + ), + ); + } + + return TextField( + controller: controller, + keyboardType: keyboardType, + style: theme.textTheme.bodyMedium, + decoration: InputDecoration( + labelText: label, + border: const OutlineInputBorder(), + isDense: true, + ), + ); + }, + ), + ), + ), + const SizedBox(width: 6), + + // Pull Action Button with explicit Up/Down/Left/Right TV D-pad linking & Select/Enter activation + Focus( + focusNode: arrowFocusNode, + onKeyEvent: (_, event) { + if (event is KeyDownEvent) { + final key = event.logicalKey; + if (key == LogicalKeyboardKey.select || + key == LogicalKeyboardKey.enter || + key == LogicalKeyboardKey.gameButtonA || + key == LogicalKeyboardKey.space) { + if (hasReference) { + setState(() { + controller.text = referenceValue.trim(); + }); + } + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowUp) { + if (index == 0) { + _closeButtonFocusNode.requestFocus(); + } else { + _arrowFocusNodes[index - 1].requestFocus(); + } + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowDown) { + if (index < 7) { + _arrowFocusNodes[index + 1].requestFocus(); + } else { + _searchButtonFocusNode.requestFocus(); + } + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowRight) { + _searchButtonFocusNode.requestFocus(); + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowLeft) { + fieldFocusNode.requestFocus(); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: Builder( + builder: (btnCtx) { + final hasFocus = Focus.of(btnCtx).hasFocus; + return InkWell( + onTap: hasReference + ? () { + setState(() { + controller.text = referenceValue.trim(); + }); + } + : null, + borderRadius: AppRadius.circular(20), + child: Container( + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: hasFocus + ? theme.colorScheme.primary.withValues(alpha: 0.25) + : Colors.transparent, + border: Border.all( + color: hasFocus + ? theme.colorScheme.primary + : Colors.transparent, + width: 2, + ), + ), + child: Icon( + Icons.west, + size: 18, + color: hasFocus + ? theme.colorScheme.primary + : (hasReference + ? theme.colorScheme.primary.withValues(alpha: 0.8) + : theme.colorScheme.onSurfaceVariant + .withValues(alpha: 0.3)), + ), + ), + ); + }, + ), + ), + + const SizedBox(width: 6), + + // Right Column: Non-editable reference text (Borderless reference badge) + Expanded( + flex: 2, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10), + decoration: BoxDecoration( + color: theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.18), + borderRadius: AppRadius.circular(6), + border: Border( + left: BorderSide( + color: hasReference + ? theme.colorScheme.primary.withValues(alpha: 0.4) + : Colors.transparent, + width: 2.5, + ), + ), + ), + child: Text( + hasReference ? referenceValue.trim() : '—', + style: theme.textTheme.bodyMedium?.copyWith( + color: hasReference + ? theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.85) + : theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.3), + fontStyle: hasReference ? FontStyle.normal : FontStyle.italic, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ); + } + + Widget _buildResultsList(BuildContext context) { + final theme = Theme.of(context); + final results = _searchResults!; + + return ListView.builder( + controller: _scrollController, + itemCount: results.length, + itemBuilder: (context, index) { + final item = results[index]; + final name = (item['Name'] ?? '').toString(); + final year = item['ProductionYear']?.toString(); + final overview = (item['Overview'] ?? '').toString(); + final provider = + (item['SearchProviderName'] ?? item['ProviderName'] ?? '').toString(); + final imageUrl = item['ImageUrl']?.toString(); + final resultFocusNode = _resultFocusNodes != null && index < _resultFocusNodes!.length + ? _resultFocusNodes![index] + : null; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Focus( + focusNode: resultFocusNode, + onKeyEvent: (_, event) { + if (event is KeyDownEvent) { + final key = event.logicalKey; + if (key == LogicalKeyboardKey.select || + key == LogicalKeyboardKey.enter || + key == LogicalKeyboardKey.gameButtonA || + key == LogicalKeyboardKey.space) { + _applyResult(item); + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowUp) { + if (index == 0) { + _closeButtonFocusNode.requestFocus(); + } else if (_resultFocusNodes != null && index > 0) { + _resultFocusNodes![index - 1].requestFocus(); + } + return KeyEventResult.handled; + } + if (key == LogicalKeyboardKey.arrowDown) { + if (_resultFocusNodes != null && index < _resultFocusNodes!.length - 1) { + _resultFocusNodes![index + 1].requestFocus(); + } + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: Builder( + builder: (cardCtx) { + final hasFocus = Focus.of(cardCtx).hasFocus; + return Card( + margin: EdgeInsets.zero, + color: hasFocus + ? theme.colorScheme.primaryContainer.withValues(alpha: 0.4) + : theme.colorScheme.surfaceContainerHighest.withValues(alpha: 0.3), + shape: RoundedRectangleBorder( + borderRadius: AppRadius.circular(10), + side: BorderSide( + color: hasFocus + ? theme.colorScheme.primary + : Colors.transparent, + width: 2, + ), + ), + child: InkWell( + onTap: () => _applyResult(item), + borderRadius: AppRadius.circular(10), + child: Padding( + padding: const EdgeInsets.all(12), + child: Row( + children: [ + ClipRRect( + borderRadius: AppRadius.circular(6), + child: imageUrl != null && imageUrl.isNotEmpty + ? Image.network( + imageUrl, + width: 50, + height: 75, + fit: BoxFit.cover, + errorBuilder: (_, _, _) => Container( + width: 50, + height: 75, + color: theme.colorScheme.surfaceContainerHighest, + child: const Icon(Icons.movie_outlined), + ), + ) + : Container( + width: 50, + height: 75, + color: theme.colorScheme.surfaceContainerHighest, + child: const Icon(Icons.movie_outlined), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + year != null && year.isNotEmpty ? '$name ($year)' : name, + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + color: hasFocus + ? theme.colorScheme.primary + : theme.colorScheme.onSurface, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (provider.isNotEmpty) ...[ + const SizedBox(height: 4), + Text( + provider, + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.primary, + ), + ), + ], + if (overview.isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + overview, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ], + ), + ), + Icon( + Icons.chevron_right, + color: hasFocus + ? theme.colorScheme.primary + : theme.colorScheme.onSurfaceVariant, + ), + ], + ), + ), + ), + ); + }, + ), + ), + ); + }, + ); + } +} diff --git a/packages/server_jellyfin/lib/src/api/jellyfin_admin_items_api.dart b/packages/server_jellyfin/lib/src/api/jellyfin_admin_items_api.dart index 8548e0494..153707518 100644 --- a/packages/server_jellyfin/lib/src/api/jellyfin_admin_items_api.dart +++ b/packages/server_jellyfin/lib/src/api/jellyfin_admin_items_api.dart @@ -76,9 +76,18 @@ class JellyfinAdminItemsApi implements AdminItemsApi { String searchType, Map query, ) async { + final normalized = switch (searchType.toLowerCase()) { + 'movie' => 'Movie', + 'series' => 'Series', + 'boxset' => 'BoxSet', + 'person' => 'Person', + 'musicalbum' => 'MusicAlbum', + 'musicartist' => 'MusicArtist', + _ => searchType, + }; return _requestWithFallback>>( methods: const ['POST'], - paths: ['/Items/RemoteSearch/$searchType'], + paths: ['/Items/RemoteSearch/$normalized', '/Items/RemoteSearch/$searchType'], data: query, convert: _asList, ); From 3be762b1fc99af9648b6f2490dd76228654ff1d3 Mon Sep 17 00:00:00 2001 From: RadicalMuffinMan <103554043+RadicalMuffinMan@users.noreply.github.com> Date: Sat, 25 Jul 2026 23:19:38 -0400 Subject: [PATCH 2/2] made dialog usable off TV, fixed the apply options, and dropped the duplicate arb entry --- lib/l10n/app_en.arb | 37 +- lib/l10n/app_localizations.dart | 28 +- lib/l10n/app_localizations_af.dart | 3 - lib/l10n/app_localizations_ar.dart | 3 - lib/l10n/app_localizations_be.dart | 3 - lib/l10n/app_localizations_bg.dart | 3 - lib/l10n/app_localizations_bn.dart | 3 - lib/l10n/app_localizations_ca.dart | 3 - lib/l10n/app_localizations_cs.dart | 3 - lib/l10n/app_localizations_cy.dart | 3 - lib/l10n/app_localizations_da.dart | 3 - lib/l10n/app_localizations_de.dart | 3 - lib/l10n/app_localizations_el.dart | 3 - lib/l10n/app_localizations_en.dart | 3 - lib/l10n/app_localizations_eo.dart | 3 - lib/l10n/app_localizations_es.dart | 3 - lib/l10n/app_localizations_et.dart | 3 - lib/l10n/app_localizations_fa.dart | 3 - lib/l10n/app_localizations_fi.dart | 3 - lib/l10n/app_localizations_fr.dart | 3 - lib/l10n/app_localizations_gl.dart | 3 - lib/l10n/app_localizations_he.dart | 3 - lib/l10n/app_localizations_hi.dart | 3 - lib/l10n/app_localizations_hr.dart | 3 - lib/l10n/app_localizations_hu.dart | 3 - lib/l10n/app_localizations_id.dart | 3 - lib/l10n/app_localizations_it.dart | 3 - lib/l10n/app_localizations_ja.dart | 3 - lib/l10n/app_localizations_kk.dart | 3 - lib/l10n/app_localizations_kn.dart | 3 - lib/l10n/app_localizations_ko.dart | 3 - lib/l10n/app_localizations_lt.dart | 3 - lib/l10n/app_localizations_lv.dart | 3 - lib/l10n/app_localizations_mk.dart | 3 - lib/l10n/app_localizations_ml.dart | 3 - lib/l10n/app_localizations_mn.dart | 3 - lib/l10n/app_localizations_nb.dart | 3 - lib/l10n/app_localizations_nl.dart | 3 - lib/l10n/app_localizations_pa.dart | 3 - lib/l10n/app_localizations_pl.dart | 3 - lib/l10n/app_localizations_pt.dart | 3 - lib/l10n/app_localizations_ro.dart | 3 - lib/l10n/app_localizations_ru.dart | 3 - lib/l10n/app_localizations_si.dart | 3 - lib/l10n/app_localizations_sk.dart | 3 - lib/l10n/app_localizations_sl.dart | 3 - lib/l10n/app_localizations_sq.dart | 3 - lib/l10n/app_localizations_sr.dart | 3 - lib/l10n/app_localizations_sv.dart | 3 - lib/l10n/app_localizations_sw.dart | 3 - lib/l10n/app_localizations_ta.dart | 3 - lib/l10n/app_localizations_te.dart | 3 - lib/l10n/app_localizations_th.dart | 3 - lib/l10n/app_localizations_tl.dart | 3 - lib/l10n/app_localizations_tr.dart | 3 - lib/l10n/app_localizations_ug.dart | 3 - lib/l10n/app_localizations_uk.dart | 3 - lib/l10n/app_localizations_vi.dart | 3 - lib/l10n/app_localizations_yue.dart | 3 - lib/l10n/app_localizations_zh.dart | 3 - .../metadata/admin_metadata_edit_screen.dart | 4 +- lib/ui/screens/detail/item_detail_screen.dart | 112 +-- lib/ui/widgets/focus/context_action.dart | 72 +- lib/ui/widgets/identify_dialog.dart | 792 +++++++++--------- .../lib/src/api/admin_items_api.dart | 8 +- .../lib/src/api/jellyfin_admin_items_api.dart | 6 +- 66 files changed, 554 insertions(+), 679 deletions(-) diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index ece166ca1..1291a6dcd 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -8947,16 +8947,47 @@ "description": "Snackbar shown when adding/removing a Seerr watchlist item fails" }, "adminSearchParameters": "Search Parameters", + "@adminSearchParameters": { + "description": "Column header above the editable fields used to search metadata providers in the Identify dialog" + }, "adminCurrentMetadata": "Current Metadata", - "adminLabelName": "Name", + "@adminCurrentMetadata": { + "description": "Column header above the read-only values currently stored on the item in the Identify dialog" + }, "adminLabelYear": "Year", + "@adminLabelYear": { + "description": "Label for the release year field in the Identify dialog" + }, "adminLabelImdbId": "IMDb Id", + "@adminLabelImdbId": { + "description": "Label for the IMDb identifier field in the Identify dialog. IMDb is a brand name and is normally left untranslated" + }, "adminLabelTmdbMovieId": "TheMovieDb Movie Id", + "@adminLabelTmdbMovieId": { + "description": "Label for the TheMovieDb movie identifier field in the Identify dialog. TheMovieDb is a brand name and is normally left untranslated" + }, "adminLabelTmdbBoxSetId": "TheMovieDb Box Set Id", + "@adminLabelTmdbBoxSetId": { + "description": "Label for the TheMovieDb collection identifier field in the Identify dialog. TheMovieDb is a brand name and is normally left untranslated" + }, "adminLabelTvdbBoxSetId": "TheTVDB Box Set Id", + "@adminLabelTvdbBoxSetId": { + "description": "Label for the TheTVDB collection identifier field in the Identify dialog. TheTVDB is a brand name and is normally left untranslated" + }, "adminLabelTvdbId": "TheTVDB Numerical Id", + "@adminLabelTvdbId": { + "description": "Label for the numeric TheTVDB identifier field in the Identify dialog. TheTVDB is a brand name and is normally left untranslated" + }, "adminLabelTvdbSlug": "TheTVDB Slug Movie Id", + "@adminLabelTvdbSlug": { + "description": "Label for the TheTVDB slug identifier field in the Identify dialog. TheTVDB is a brand name and is normally left untranslated" + }, "adminReplaceImages": "Replace existing images", - "adminApply": "Apply", - "adminBackToSearch": "Back to Search Criteria" + "@adminReplaceImages": { + "description": "Checkbox label in the confirmation dialog shown before applying an identified metadata match" + }, + "adminBackToSearch": "Back to Search Criteria", + "@adminBackToSearch": { + "description": "Tooltip on the button that returns from the Identify results list to the search form" + } } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index cb72b6271..b118a80b4 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -18022,73 +18022,67 @@ abstract class AppLocalizations { /// **'Couldn\'t update watchlist'** String get watchlistUpdateFailed; - /// No description provided for @adminSearchParameters. + /// Column header above the editable fields used to search metadata providers in the Identify dialog /// /// In en, this message translates to: /// **'Search Parameters'** String get adminSearchParameters; - /// No description provided for @adminCurrentMetadata. + /// Column header above the read-only values currently stored on the item in the Identify dialog /// /// In en, this message translates to: /// **'Current Metadata'** String get adminCurrentMetadata; - /// No description provided for @adminLabelName. - /// - /// In en, this message translates to: - /// **'Name'** - String get adminLabelName; - - /// No description provided for @adminLabelYear. + /// Label for the release year field in the Identify dialog /// /// In en, this message translates to: /// **'Year'** String get adminLabelYear; - /// No description provided for @adminLabelImdbId. + /// Label for the IMDb identifier field in the Identify dialog. IMDb is a brand name and is normally left untranslated /// /// In en, this message translates to: /// **'IMDb Id'** String get adminLabelImdbId; - /// No description provided for @adminLabelTmdbMovieId. + /// Label for the TheMovieDb movie identifier field in the Identify dialog. TheMovieDb is a brand name and is normally left untranslated /// /// In en, this message translates to: /// **'TheMovieDb Movie Id'** String get adminLabelTmdbMovieId; - /// No description provided for @adminLabelTmdbBoxSetId. + /// Label for the TheMovieDb collection identifier field in the Identify dialog. TheMovieDb is a brand name and is normally left untranslated /// /// In en, this message translates to: /// **'TheMovieDb Box Set Id'** String get adminLabelTmdbBoxSetId; - /// No description provided for @adminLabelTvdbBoxSetId. + /// Label for the TheTVDB collection identifier field in the Identify dialog. TheTVDB is a brand name and is normally left untranslated /// /// In en, this message translates to: /// **'TheTVDB Box Set Id'** String get adminLabelTvdbBoxSetId; - /// No description provided for @adminLabelTvdbId. + /// Label for the numeric TheTVDB identifier field in the Identify dialog. TheTVDB is a brand name and is normally left untranslated /// /// In en, this message translates to: /// **'TheTVDB Numerical Id'** String get adminLabelTvdbId; - /// No description provided for @adminLabelTvdbSlug. + /// Label for the TheTVDB slug identifier field in the Identify dialog. TheTVDB is a brand name and is normally left untranslated /// /// In en, this message translates to: /// **'TheTVDB Slug Movie Id'** String get adminLabelTvdbSlug; - /// No description provided for @adminReplaceImages. + /// Checkbox label in the confirmation dialog shown before applying an identified metadata match /// /// In en, this message translates to: /// **'Replace existing images'** String get adminReplaceImages; - /// No description provided for @adminBackToSearch. + /// Tooltip on the button that returns from the Identify results list to the search form /// /// In en, this message translates to: /// **'Back to Search Criteria'** diff --git a/lib/l10n/app_localizations_af.dart b/lib/l10n/app_localizations_af.dart index ce476da7c..64afd6d95 100644 --- a/lib/l10n/app_localizations_af.dart +++ b/lib/l10n/app_localizations_af.dart @@ -10164,9 +10164,6 @@ class AppLocalizationsAf extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index f34942040..3608235f2 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -10140,9 +10140,6 @@ class AppLocalizationsAr extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_be.dart b/lib/l10n/app_localizations_be.dart index 05b937bba..57fdac86f 100644 --- a/lib/l10n/app_localizations_be.dart +++ b/lib/l10n/app_localizations_be.dart @@ -10229,9 +10229,6 @@ class AppLocalizationsBe extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_bg.dart b/lib/l10n/app_localizations_bg.dart index 6915d6afd..0032b386d 100644 --- a/lib/l10n/app_localizations_bg.dart +++ b/lib/l10n/app_localizations_bg.dart @@ -10267,9 +10267,6 @@ class AppLocalizationsBg extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_bn.dart b/lib/l10n/app_localizations_bn.dart index eba11111f..7d45c0ec4 100644 --- a/lib/l10n/app_localizations_bn.dart +++ b/lib/l10n/app_localizations_bn.dart @@ -10138,9 +10138,6 @@ class AppLocalizationsBn extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_ca.dart b/lib/l10n/app_localizations_ca.dart index ed88c38b5..d5493de59 100644 --- a/lib/l10n/app_localizations_ca.dart +++ b/lib/l10n/app_localizations_ca.dart @@ -10315,9 +10315,6 @@ class AppLocalizationsCa extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_cs.dart b/lib/l10n/app_localizations_cs.dart index a7dcbabf4..faa0e03f8 100644 --- a/lib/l10n/app_localizations_cs.dart +++ b/lib/l10n/app_localizations_cs.dart @@ -10202,9 +10202,6 @@ class AppLocalizationsCs extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_cy.dart b/lib/l10n/app_localizations_cy.dart index d2701d34b..8cad6f022 100644 --- a/lib/l10n/app_localizations_cy.dart +++ b/lib/l10n/app_localizations_cy.dart @@ -10219,9 +10219,6 @@ class AppLocalizationsCy extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_da.dart b/lib/l10n/app_localizations_da.dart index 995b8baf5..e0c91c845 100644 --- a/lib/l10n/app_localizations_da.dart +++ b/lib/l10n/app_localizations_da.dart @@ -10155,9 +10155,6 @@ class AppLocalizationsDa extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index d0e86f1a1..b6f33ed63 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -10313,9 +10313,6 @@ class AppLocalizationsDe extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_el.dart b/lib/l10n/app_localizations_el.dart index 5f105f260..0de82327e 100644 --- a/lib/l10n/app_localizations_el.dart +++ b/lib/l10n/app_localizations_el.dart @@ -10322,9 +10322,6 @@ class AppLocalizationsEl extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 0248ab48b..c6971f9b3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -10074,9 +10074,6 @@ class AppLocalizationsEn extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_eo.dart b/lib/l10n/app_localizations_eo.dart index 0160d5416..33e4e1e8f 100644 --- a/lib/l10n/app_localizations_eo.dart +++ b/lib/l10n/app_localizations_eo.dart @@ -10150,9 +10150,6 @@ class AppLocalizationsEo extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 1cf518807..e8fe7d239 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -10279,9 +10279,6 @@ class AppLocalizationsEs extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_et.dart b/lib/l10n/app_localizations_et.dart index 932a000fa..2305332c0 100644 --- a/lib/l10n/app_localizations_et.dart +++ b/lib/l10n/app_localizations_et.dart @@ -10168,9 +10168,6 @@ class AppLocalizationsEt extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_fa.dart b/lib/l10n/app_localizations_fa.dart index b6f1718f4..ae4817aa4 100644 --- a/lib/l10n/app_localizations_fa.dart +++ b/lib/l10n/app_localizations_fa.dart @@ -10096,9 +10096,6 @@ class AppLocalizationsFa extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_fi.dart b/lib/l10n/app_localizations_fi.dart index 47cd8bd1e..acecdc208 100644 --- a/lib/l10n/app_localizations_fi.dart +++ b/lib/l10n/app_localizations_fi.dart @@ -10186,9 +10186,6 @@ class AppLocalizationsFi extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index d025becfa..acaf9bc44 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -10291,9 +10291,6 @@ class AppLocalizationsFr extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_gl.dart b/lib/l10n/app_localizations_gl.dart index f63817b67..94e1b7cff 100644 --- a/lib/l10n/app_localizations_gl.dart +++ b/lib/l10n/app_localizations_gl.dart @@ -10305,9 +10305,6 @@ class AppLocalizationsGl extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_he.dart b/lib/l10n/app_localizations_he.dart index b8b998d36..702ab22e9 100644 --- a/lib/l10n/app_localizations_he.dart +++ b/lib/l10n/app_localizations_he.dart @@ -10017,9 +10017,6 @@ class AppLocalizationsHe extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index eb6a88b77..2e064e507 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -10131,9 +10131,6 @@ class AppLocalizationsHi extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_hr.dart b/lib/l10n/app_localizations_hr.dart index ee56c7575..1bdfb5b6d 100644 --- a/lib/l10n/app_localizations_hr.dart +++ b/lib/l10n/app_localizations_hr.dart @@ -10384,9 +10384,6 @@ class AppLocalizationsHr extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_hu.dart b/lib/l10n/app_localizations_hu.dart index 9fafc6eac..4a1d45a61 100644 --- a/lib/l10n/app_localizations_hu.dart +++ b/lib/l10n/app_localizations_hu.dart @@ -10256,9 +10256,6 @@ class AppLocalizationsHu extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 10155568f..49e4149c1 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -10160,9 +10160,6 @@ class AppLocalizationsId extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_it.dart b/lib/l10n/app_localizations_it.dart index ecb3dd486..487141dfd 100644 --- a/lib/l10n/app_localizations_it.dart +++ b/lib/l10n/app_localizations_it.dart @@ -10245,9 +10245,6 @@ class AppLocalizationsIt extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index a957223fe..2ca9576e9 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -9844,9 +9844,6 @@ class AppLocalizationsJa extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_kk.dart b/lib/l10n/app_localizations_kk.dart index a90ce159f..2ac1cd121 100644 --- a/lib/l10n/app_localizations_kk.dart +++ b/lib/l10n/app_localizations_kk.dart @@ -10206,9 +10206,6 @@ class AppLocalizationsKk extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_kn.dart b/lib/l10n/app_localizations_kn.dart index 198b1f526..a2666436c 100644 --- a/lib/l10n/app_localizations_kn.dart +++ b/lib/l10n/app_localizations_kn.dart @@ -10230,9 +10230,6 @@ class AppLocalizationsKn extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 1d43cf769..3b86fedd1 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -9837,9 +9837,6 @@ class AppLocalizationsKo extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_lt.dart b/lib/l10n/app_localizations_lt.dart index ed1add099..f6b0bb5f5 100644 --- a/lib/l10n/app_localizations_lt.dart +++ b/lib/l10n/app_localizations_lt.dart @@ -10226,9 +10226,6 @@ class AppLocalizationsLt extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_lv.dart b/lib/l10n/app_localizations_lv.dart index 1155495c1..7901a55d0 100644 --- a/lib/l10n/app_localizations_lv.dart +++ b/lib/l10n/app_localizations_lv.dart @@ -10223,9 +10223,6 @@ class AppLocalizationsLv extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_mk.dart b/lib/l10n/app_localizations_mk.dart index b5b90ce2d..3b49af12e 100644 --- a/lib/l10n/app_localizations_mk.dart +++ b/lib/l10n/app_localizations_mk.dart @@ -10238,9 +10238,6 @@ class AppLocalizationsMk extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_ml.dart b/lib/l10n/app_localizations_ml.dart index 1bac0891d..edad1a00e 100644 --- a/lib/l10n/app_localizations_ml.dart +++ b/lib/l10n/app_localizations_ml.dart @@ -10283,9 +10283,6 @@ class AppLocalizationsMl extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_mn.dart b/lib/l10n/app_localizations_mn.dart index af5665a60..595186831 100644 --- a/lib/l10n/app_localizations_mn.dart +++ b/lib/l10n/app_localizations_mn.dart @@ -10180,9 +10180,6 @@ class AppLocalizationsMn extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_nb.dart b/lib/l10n/app_localizations_nb.dart index 40e2d966f..5f851cc49 100644 --- a/lib/l10n/app_localizations_nb.dart +++ b/lib/l10n/app_localizations_nb.dart @@ -10153,9 +10153,6 @@ class AppLocalizationsNb extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index 380362b9e..77e488121 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -10212,9 +10212,6 @@ class AppLocalizationsNl extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_pa.dart b/lib/l10n/app_localizations_pa.dart index 347433394..3b50ec6ee 100644 --- a/lib/l10n/app_localizations_pa.dart +++ b/lib/l10n/app_localizations_pa.dart @@ -10119,9 +10119,6 @@ class AppLocalizationsPa extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_pl.dart b/lib/l10n/app_localizations_pl.dart index 751a4ad34..389cae952 100644 --- a/lib/l10n/app_localizations_pl.dart +++ b/lib/l10n/app_localizations_pl.dart @@ -10222,9 +10222,6 @@ class AppLocalizationsPl extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 179483bb7..968905b90 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -10245,9 +10245,6 @@ class AppLocalizationsPt extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_ro.dart b/lib/l10n/app_localizations_ro.dart index 24c497010..68c987880 100644 --- a/lib/l10n/app_localizations_ro.dart +++ b/lib/l10n/app_localizations_ro.dart @@ -10249,9 +10249,6 @@ class AppLocalizationsRo extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index 3c37c1448..bfbda2aa6 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -10239,9 +10239,6 @@ class AppLocalizationsRu extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_si.dart b/lib/l10n/app_localizations_si.dart index 9d26e172f..229b6ff15 100644 --- a/lib/l10n/app_localizations_si.dart +++ b/lib/l10n/app_localizations_si.dart @@ -10151,9 +10151,6 @@ class AppLocalizationsSi extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_sk.dart b/lib/l10n/app_localizations_sk.dart index 864cdf243..28dbaa94c 100644 --- a/lib/l10n/app_localizations_sk.dart +++ b/lib/l10n/app_localizations_sk.dart @@ -10231,9 +10231,6 @@ class AppLocalizationsSk extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_sl.dart b/lib/l10n/app_localizations_sl.dart index a5e20157e..024b0066c 100644 --- a/lib/l10n/app_localizations_sl.dart +++ b/lib/l10n/app_localizations_sl.dart @@ -10225,9 +10225,6 @@ class AppLocalizationsSl extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_sq.dart b/lib/l10n/app_localizations_sq.dart index cca7dbe19..aba3ae80b 100644 --- a/lib/l10n/app_localizations_sq.dart +++ b/lib/l10n/app_localizations_sq.dart @@ -10260,9 +10260,6 @@ class AppLocalizationsSq extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_sr.dart b/lib/l10n/app_localizations_sr.dart index 1161dfa46..42e3ad10c 100644 --- a/lib/l10n/app_localizations_sr.dart +++ b/lib/l10n/app_localizations_sr.dart @@ -10372,9 +10372,6 @@ class AppLocalizationsSr extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_sv.dart b/lib/l10n/app_localizations_sv.dart index b47e60547..23eb5ee15 100644 --- a/lib/l10n/app_localizations_sv.dart +++ b/lib/l10n/app_localizations_sv.dart @@ -10169,9 +10169,6 @@ class AppLocalizationsSv extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_sw.dart b/lib/l10n/app_localizations_sw.dart index 6db89dd41..8dcf587ca 100644 --- a/lib/l10n/app_localizations_sw.dart +++ b/lib/l10n/app_localizations_sw.dart @@ -10244,9 +10244,6 @@ class AppLocalizationsSw extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_ta.dart b/lib/l10n/app_localizations_ta.dart index b6bc0cd61..47fb9f8aa 100644 --- a/lib/l10n/app_localizations_ta.dart +++ b/lib/l10n/app_localizations_ta.dart @@ -10245,9 +10245,6 @@ class AppLocalizationsTa extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_te.dart b/lib/l10n/app_localizations_te.dart index 0dc330aef..9399fc8fe 100644 --- a/lib/l10n/app_localizations_te.dart +++ b/lib/l10n/app_localizations_te.dart @@ -10231,9 +10231,6 @@ class AppLocalizationsTe extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_th.dart b/lib/l10n/app_localizations_th.dart index 7624610b2..236c3011b 100644 --- a/lib/l10n/app_localizations_th.dart +++ b/lib/l10n/app_localizations_th.dart @@ -10082,9 +10082,6 @@ class AppLocalizationsTh extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_tl.dart b/lib/l10n/app_localizations_tl.dart index 2b92b1dad..02d4d2b2a 100644 --- a/lib/l10n/app_localizations_tl.dart +++ b/lib/l10n/app_localizations_tl.dart @@ -10275,9 +10275,6 @@ class AppLocalizationsTl extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 26296bb5b..976624c71 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -10175,9 +10175,6 @@ class AppLocalizationsTr extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_ug.dart b/lib/l10n/app_localizations_ug.dart index 3de2472ed..ddd20d894 100644 --- a/lib/l10n/app_localizations_ug.dart +++ b/lib/l10n/app_localizations_ug.dart @@ -10196,9 +10196,6 @@ class AppLocalizationsUg extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_uk.dart b/lib/l10n/app_localizations_uk.dart index 8239d1b52..993866bd5 100644 --- a/lib/l10n/app_localizations_uk.dart +++ b/lib/l10n/app_localizations_uk.dart @@ -10250,9 +10250,6 @@ class AppLocalizationsUk extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_vi.dart b/lib/l10n/app_localizations_vi.dart index 5e5a30f40..959d4fc01 100644 --- a/lib/l10n/app_localizations_vi.dart +++ b/lib/l10n/app_localizations_vi.dart @@ -10163,9 +10163,6 @@ class AppLocalizationsVi extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_yue.dart b/lib/l10n/app_localizations_yue.dart index db95cd017..669621924 100644 --- a/lib/l10n/app_localizations_yue.dart +++ b/lib/l10n/app_localizations_yue.dart @@ -9756,9 +9756,6 @@ class AppLocalizationsYue extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index afd25c61c..aa0de0214 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -9752,9 +9752,6 @@ class AppLocalizationsZh extends AppLocalizations { @override String get adminCurrentMetadata => 'Current Metadata'; - @override - String get adminLabelName => 'Name'; - @override String get adminLabelYear => 'Year'; diff --git a/lib/ui/screens/admin/metadata/admin_metadata_edit_screen.dart b/lib/ui/screens/admin/metadata/admin_metadata_edit_screen.dart index 50af97068..74e82d19a 100644 --- a/lib/ui/screens/admin/metadata/admin_metadata_edit_screen.dart +++ b/lib/ui/screens/admin/metadata/admin_metadata_edit_screen.dart @@ -536,7 +536,9 @@ class _AdminMetadataEditScreenState extends State { final applied = await IdentifyDialog.show( context, itemId: widget.itemId, - itemType: searchType, + // An empty type means unknown, so let the dialog resolve it from the item + // rather than posting to /Items/RemoteSearch/ with no type at all. + itemType: searchType.isEmpty ? null : searchType, itemName: name, itemYear: year, itemPath: path, diff --git a/lib/ui/screens/detail/item_detail_screen.dart b/lib/ui/screens/detail/item_detail_screen.dart index 294619a7f..fa429f7ac 100644 --- a/lib/ui/screens/detail/item_detail_screen.dart +++ b/lib/ui/screens/detail/item_detail_screen.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import '../../widgets/offline_aware_image.dart'; import '../../widgets/identify_dialog.dart'; +import '../../widgets/focus/context_action.dart' show canIdentifyItemType; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -6569,66 +6570,67 @@ class DetailActionButtonsState extends State { content: Column( mainAxisSize: MainAxisSize.min, children: [ - Focus( - onKeyEvent: (_, event) { - if (isActivateKey(event)) { - Navigator.of(dialogCtx).pop(); - IdentifyDialog.show( - context, - itemId: item.id, - itemType: item.type, - itemName: item.name, - itemYear: item.productionYear, - ).then((applied) { - if (applied == true) { - viewModel.load(); - } - }); - return KeyEventResult.handled; - } - return KeyEventResult.ignored; - }, - child: Builder( - builder: (buttonCtx) { - final hasFocus = Focus.of(buttonCtx).hasFocus; - return InkWell( - onTap: () async { - Navigator.of(dialogCtx).pop(); - final applied = await IdentifyDialog.show( - context, - itemId: item.id, - itemType: item.type, - itemName: item.name, - itemYear: item.productionYear, - ); + if (canIdentifyItemType(item.type)) + Focus( + onKeyEvent: (_, event) { + if (isActivateKey(event)) { + Navigator.of(dialogCtx).pop(); + IdentifyDialog.show( + context, + itemId: item.id, + itemType: item.type, + itemName: item.name, + itemYear: item.productionYear, + ).then((applied) { if (applied == true) { viewModel.load(); } - }, - child: Container( - padding: const EdgeInsets.symmetric( - vertical: 12, - horizontal: 16, - ), - decoration: BoxDecoration( - color: hasFocus ? Colors.white12 : Colors.transparent, - borderRadius: AppRadius.circular(8), - ), - child: Row( - children: [ - const Icon(Icons.search, color: Colors.white70), - const SizedBox(width: 12), - Text( - l10n.adminMetadataIdentify, - style: const TextStyle(color: Colors.white), - ), - ], - ), - ), - ); + }); + return KeyEventResult.handled; + } + return KeyEventResult.ignored; }, + child: Builder( + builder: (buttonCtx) { + final hasFocus = Focus.of(buttonCtx).hasFocus; + return InkWell( + onTap: () async { + Navigator.of(dialogCtx).pop(); + final applied = await IdentifyDialog.show( + context, + itemId: item.id, + itemType: item.type, + itemName: item.name, + itemYear: item.productionYear, + ); + if (applied == true) { + viewModel.load(); + } + }, + child: Container( + padding: const EdgeInsets.symmetric( + vertical: 12, + horizontal: 16, + ), + decoration: BoxDecoration( + color: hasFocus ? Colors.white12 : Colors.transparent, + borderRadius: AppRadius.circular(8), + ), + child: Row( + children: [ + const Icon(Icons.search, color: Colors.white70), + const SizedBox(width: 12), + Text( + l10n.adminMetadataIdentify, + style: const TextStyle(color: Colors.white), + ), + ], + ), + ), + ); + }, + ), ), - ), if (!PlatformDetection.isTV) Focus( onKeyEvent: (_, event) { diff --git a/lib/ui/widgets/focus/context_action.dart b/lib/ui/widgets/focus/context_action.dart index f3ec77631..d768469a8 100644 --- a/lib/ui/widgets/focus/context_action.dart +++ b/lib/ui/widgets/focus/context_action.dart @@ -27,6 +27,25 @@ class ItemContextAction { }); } +/// Item types the server exposes a `/Items/RemoteSearch/{type}` endpoint for. +/// Episodes and Seasons qualify because Identify redirects them to their +/// parent series. Anything else would just 404 with a raw error toast. +const _identifiableTypes = { + 'Movie', + 'Series', + 'Season', + 'Episode', + 'BoxSet', + 'Person', + 'MusicAlbum', + 'MusicArtist', + 'Book', + 'Trailer', + 'MusicVideo', +}; + +bool canIdentifyItemType(String? type) => _identifiableTypes.contains(type); + List contextActionsFor( BuildContext context, AggregatedItem item, { @@ -204,31 +223,36 @@ List contextActionsFor( }, )); - actions.add(ItemContextAction( - icon: Icons.search_outlined, - label: l10n.adminMetadataIdentify, - onSelect: () async { - if (!context.mounted) return; - final isTVChild = item.type == 'Episode' || item.type == 'Season'; - final hasSeriesId = item.seriesId != null && item.seriesId!.isNotEmpty; - final targetItemId = (isTVChild && hasSeriesId) ? item.seriesId! : item.id; - final targetItemType = (isTVChild && hasSeriesId) ? 'Series' : item.type; - final targetItemName = (isTVChild && hasSeriesId) - ? (item.seriesName ?? item.name) - : item.name; + if (canIdentifyItemType(type)) { + actions.add(ItemContextAction( + icon: Icons.search_outlined, + label: l10n.adminMetadataIdentify, + onSelect: () async { + if (!context.mounted) return; + final isTVChild = item.type == 'Episode' || item.type == 'Season'; + final hasSeriesId = + item.seriesId != null && item.seriesId!.isNotEmpty; + final targetItemId = + (isTVChild && hasSeriesId) ? item.seriesId! : item.id; + final targetItemType = + (isTVChild && hasSeriesId) ? 'Series' : item.type; + final targetItemName = (isTVChild && hasSeriesId) + ? (item.seriesName ?? item.name) + : item.name; - final applied = await IdentifyDialog.show( - context, - itemId: targetItemId, - itemType: targetItemType, - itemName: targetItemName, - itemYear: isTVChild ? null : item.productionYear, - ); - if (applied == true) { - onChanged?.call(); - } - }, - )); + final applied = await IdentifyDialog.show( + context, + itemId: targetItemId, + itemType: targetItemType, + itemName: targetItemName, + itemYear: isTVChild ? null : item.productionYear, + ); + if (applied == true) { + onChanged?.call(); + } + }, + )); + } } } diff --git a/lib/ui/widgets/identify_dialog.dart b/lib/ui/widgets/identify_dialog.dart index 4ee7adb10..c0bfc9ca7 100644 --- a/lib/ui/widgets/identify_dialog.dart +++ b/lib/ui/widgets/identify_dialog.dart @@ -1,15 +1,16 @@ import 'package:custom_tv_text_field/custom_tv_text_field.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:get_it/get_it.dart'; import 'package:moonfin_design/moonfin_design.dart'; import 'package:server_core/server_core.dart'; import '../../l10n/app_localizations.dart'; import '../../preference/user_preferences.dart'; +import '../../util/focus/dpad_keys.dart'; import '../../util/platform_detection.dart'; import 'adaptive/adaptive_dialog.dart'; import 'focus/focusable_button.dart'; +import 'overlay_sheet.dart'; class IdentifyDialog extends StatefulWidget { final String itemId; @@ -38,7 +39,7 @@ class IdentifyDialog extends StatefulWidget { String? itemPath, Map? providerIds, }) { - return showDialog( + return showFocusRestoringDialog( context: context, builder: (ctx) => IdentifyDialog( itemId: itemId, @@ -56,11 +57,16 @@ class IdentifyDialog extends StatefulWidget { } class _IdentifyDialogState extends State { + /// Every focus node list and the d-pad wrap-around logic is sized off this. + static const int _fieldCount = 8; + static const int _lastFieldIndex = _fieldCount - 1; + late final AdminItemsApi _adminApi; late final ItemsApi _itemsApi; late final UserPreferences _prefs; bool _loadingItem = false; + String? _loadError; bool _searching = false; bool _applying = false; @@ -70,7 +76,7 @@ class _IdentifyDialogState extends State { List>? _searchResults; - // Reference Metadata Values (Current Item) + // What the item currently has, shown next to each search field. String? _refName; String? _refYear; String? _refImdb; @@ -80,32 +86,33 @@ class _IdentifyDialogState extends State { String? _refTvdbId; String? _refTvdbSlug; - // Editable Search Parameter Controllers (Blank by default) - late final TextEditingController _nameController; - late final TextEditingController _yearController; - late final TextEditingController _imdbController; - late final TextEditingController _tmdbMovieController; - late final TextEditingController _tmdbBoxSetController; - late final TextEditingController _tvdbBoxSetController; - late final TextEditingController _tvdbIdController; - late final TextEditingController _tvdbSlugController; - - // FocusNodes and ScrollController for TV Navigation + // The search fields start blank so a search only uses what the admin types. + final TextEditingController _nameController = TextEditingController(); + final TextEditingController _yearController = TextEditingController(); + final TextEditingController _imdbController = TextEditingController(); + final TextEditingController _tmdbMovieController = TextEditingController(); + final TextEditingController _tmdbBoxSetController = TextEditingController(); + final TextEditingController _tvdbBoxSetController = TextEditingController(); + final TextEditingController _tvdbIdController = TextEditingController(); + final TextEditingController _tvdbSlugController = TextEditingController(); + final ScrollController _scrollController = ScrollController(); final FocusNode _closeButtonFocusNode = FocusNode(debugLabel: 'identify-close-btn'); final FocusNode _cancelButtonFocusNode = FocusNode(debugLabel: 'identify-cancel-btn'); final FocusNode _searchButtonFocusNode = FocusNode(debugLabel: 'identify-search-btn'); - final List _fieldFocusNodes = - List.generate(8, (i) => FocusNode(debugLabel: 'identify-field-$i')); - final List _arrowFocusNodes = - List.generate(8, (i) => FocusNode(debugLabel: 'identify-arrow-$i')); + final List _fieldFocusNodes = List.generate( + _fieldCount, (i) => FocusNode(debugLabel: 'identify-field-$i')); + final List _arrowFocusNodes = List.generate( + _fieldCount, (i) => FocusNode(debugLabel: 'identify-arrow-$i')); final List> _tvFieldKeys = - List.generate(8, (_) => GlobalKey()); + List.generate(_fieldCount, (_) => GlobalKey()); List? _resultFocusNodes; + bool get _isTV => PlatformDetection.isTV; + @override void initState() { super.initState(); @@ -115,8 +122,11 @@ class _IdentifyDialogState extends State { _prefs = GetIt.instance(); _targetItemId = widget.itemId; - final isTVChild = widget.itemType == 'Episode' || widget.itemType == 'Season'; - _searchType = isTVChild ? 'Series' : (widget.itemType ?? 'Movie'); + final itemType = widget.itemType?.trim(); + final isTVChild = itemType == 'Episode' || itemType == 'Season'; + _searchType = isTVChild + ? 'Series' + : (itemType == null || itemType.isEmpty ? 'Movie' : itemType); _path = widget.itemPath; final initialProviders = widget.providerIds ?? const {}; @@ -132,18 +142,8 @@ class _IdentifyDialogState extends State { _refTvdbSlug = initialProviders['TvdbSlug']?.toString(); } - // Search controllers start BLANK by default - _nameController = TextEditingController(text: ''); - _yearController = TextEditingController(text: ''); - _imdbController = TextEditingController(text: ''); - _tmdbMovieController = TextEditingController(text: ''); - _tmdbBoxSetController = TextEditingController(text: ''); - _tvdbBoxSetController = TextEditingController(text: ''); - _tvdbIdController = TextEditingController(text: ''); - _tvdbSlugController = TextEditingController(text: ''); - - // Add listeners to auto-scroll focused rows into the visible viewport on TV - for (int i = 0; i < 8; i++) { + // Keep the focused row on screen while the d-pad walks down the form. + for (int i = 0; i < _fieldCount; i++) { final idx = i; _fieldFocusNodes[idx].addListener(() { if (_fieldFocusNodes[idx].hasFocus) { @@ -157,14 +157,13 @@ class _IdentifyDialogState extends State { }); } - _fetchItemDetailsIfNeeded(); + _loadItemDetails(); } void _closeDialog([bool? result]) { + if (!mounted) return; FocusScope.of(context).unfocus(); - if (mounted) { - Navigator.of(context).pop(result); - } + Navigator.of(context).pop(result); } @override @@ -197,22 +196,41 @@ class _IdentifyDialogState extends State { fn.unfocus(); fn.dispose(); } - _clearResultFocusNodes(); + _disposeNodes(_resultFocusNodes); + _resultFocusNodes = null; super.dispose(); } - void _clearResultFocusNodes() { - if (_resultFocusNodes != null) { - for (final fn in _resultFocusNodes!) { - fn.unfocus(); - fn.dispose(); - } - _resultFocusNodes = null; + static void _disposeNodes(List? nodes) { + if (nodes == null) return; + for (final fn in nodes) { + fn.unfocus(); + fn.dispose(); } } + /// Disposes the result focus nodes only after the frame that takes the result + /// cards off screen. Disposing them inline would leave the still mounted + /// [Focus] widgets holding dead nodes, which throws on the next detach. + void _releaseResultFocusNodes() { + final stale = _resultFocusNodes; + _resultFocusNodes = null; + if (stale == null) return; + WidgetsBinding.instance.addPostFrameCallback((_) => _disposeNodes(stale)); + } + + void _backToSearchForm() { + setState(() => _searchResults = null); + _releaseResultFocusNodes(); + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && _isTV) { + _fieldFocusNodes.first.requestFocus(); + } + }); + } + void _setupResultFocusNodes(int count) { - _clearResultFocusNodes(); + _releaseResultFocusNodes(); _resultFocusNodes = List.generate(count, (i) => FocusNode(debugLabel: 'identify-result-$i')); for (int i = 0; i < count; i++) { @@ -258,19 +276,24 @@ class _IdentifyDialogState extends State { }); } - Future _fetchItemDetailsIfNeeded() async { - setState(() => _loadingItem = true); + Future _loadItemDetails() async { + setState(() { + _loadingItem = true; + _loadError = null; + }); try { var raw = await _itemsApi.getItem(widget.itemId); if ((raw['Type'] == 'Episode' || raw['Type'] == 'Season') && raw['SeriesId'] != null && raw['SeriesId'].toString().isNotEmpty) { - _targetItemId = raw['SeriesId'].toString(); - _searchType = 'Series'; - try { - raw = await _itemsApi.getItem(_targetItemId); - } catch (_) {} + final seriesId = raw['SeriesId'].toString(); + // Episodes and seasons are identified through their series, but only + // switch targets once that series has actually loaded, otherwise the + // series id would end up paired with episode metadata. + final series = await _itemsApi.getItem(seriesId); + _targetItemId = seriesId; + raw = series; } if (!mounted) return; @@ -293,7 +316,12 @@ class _IdentifyDialogState extends State { _refTvdbSlug = pIds['TvdbSlug']?.toString(); } }); - } catch (_) { + } catch (e) { + // Searching by hand still works without reference values, so say what + // went wrong instead of leaving the current metadata column blank. + if (mounted) { + setState(() => _loadError = '$e'); + } } finally { if (mounted) { setState(() => _loadingItem = false); @@ -301,6 +329,18 @@ class _IdentifyDialogState extends State { } } + Future>> _searchRemote( + Map searchInfo, { + String? providerName, + }) { + return _adminApi.searchRemote(_searchType, { + 'SearchInfo': searchInfo, + 'ItemId': _targetItemId, + 'IncludeDisabledProviders': false, + 'SearchProviderName': ?providerName, + }); + } + Future _performSearch() async { final l10n = AppLocalizations.of(context); setState(() { @@ -311,6 +351,7 @@ class _IdentifyDialogState extends State { final nameVal = _nameController.text.trim(); final yearVal = int.tryParse(_yearController.text.trim()); + // Provider ids are matched case insensitively, so one casing each is enough. final providerIds = {}; var imdbVal = _imdbController.text.trim(); @@ -319,40 +360,31 @@ class _IdentifyDialogState extends State { imdbVal = 'tt$imdbVal'; } providerIds['Imdb'] = imdbVal; - providerIds['IMDb'] = imdbVal; - providerIds['imdb'] = imdbVal; } final tmdbVal = _tmdbMovieController.text.trim(); if (tmdbVal.isNotEmpty) { providerIds['Tmdb'] = tmdbVal; - providerIds['TMDB'] = tmdbVal; - providerIds['tmdb'] = tmdbVal; } final tmdbBoxSetVal = _tmdbBoxSetController.text.trim(); if (tmdbBoxSetVal.isNotEmpty) { providerIds['TmdbBoxSet'] = tmdbBoxSetVal; - providerIds['TMDBBoxSet'] = tmdbBoxSetVal; } final tvdbBoxSetVal = _tvdbBoxSetController.text.trim(); if (tvdbBoxSetVal.isNotEmpty) { providerIds['TvdbBoxSet'] = tvdbBoxSetVal; - providerIds['TVDBBoxSet'] = tvdbBoxSetVal; } final tvdbVal = _tvdbIdController.text.trim(); if (tvdbVal.isNotEmpty) { providerIds['Tvdb'] = tvdbVal; - providerIds['TVDB'] = tvdbVal; - providerIds['tvdb'] = tvdbVal; } final tvdbSlugVal = _tvdbSlugController.text.trim(); if (tvdbSlugVal.isNotEmpty) { providerIds['TvdbSlug'] = tvdbSlugVal; - providerIds['TVDBSlug'] = tvdbSlugVal; } final searchInfo = { @@ -361,60 +393,33 @@ class _IdentifyDialogState extends State { if (providerIds.isNotEmpty) 'ProviderIds': providerIds, }; - debugPrint('--> [IdentifyRemoteSearch] Executing search for $_searchType | ItemId: $_targetItemId'); - debugPrint('--> [IdentifyRemoteSearch] SearchInfo: $searchInfo'); - try { - List> results = await _adminApi.searchRemote(_searchType, { - 'SearchInfo': searchInfo, - 'ItemId': _targetItemId, - 'IncludeDisabledProviders': false, - }); + List> results = await _searchRemote(searchInfo); - // Fallback 1: If TMDB/IMDb is present, try setting explicit SearchProviderName - if (results.isEmpty && (providerIds.containsKey('Tmdb') || providerIds.containsKey('Imdb'))) { - final altInfoProvider = { - ...searchInfo, - 'SearchProviderName': 'TheMovieDb', - }; - debugPrint('--> [IdentifyRemoteSearch] Retry with SearchProviderName: TheMovieDb'); - results = await _adminApi.searchRemote(_searchType, { - 'SearchInfo': altInfoProvider, - 'ItemId': _targetItemId, - 'IncludeDisabledProviders': false, - }); + // Pin the search to TheMovieDb when a TMDB or IMDb id was given. Note + // that SearchProviderName belongs on the query, not inside SearchInfo. + if (results.isEmpty && + (providerIds.containsKey('Tmdb') || providerIds.containsKey('Imdb'))) { + results = await _searchRemote(searchInfo, providerName: 'TheMovieDb'); } - // Fallback 2: Try ProviderIds alone without Name + // Retry on the ids alone, in case the typed name blocked the match. if (results.isEmpty && providerIds.isNotEmpty && nameVal.isNotEmpty) { - final altInfoA = { + results = await _searchRemote({ 'Name': '', 'Year': ?yearVal, 'ProviderIds': providerIds, - }; - debugPrint('--> [IdentifyRemoteSearch] Retry with ProviderIds alone (empty Name)'); - results = await _adminApi.searchRemote(_searchType, { - 'SearchInfo': altInfoA, - 'ItemId': _targetItemId, - 'IncludeDisabledProviders': false, }); } - // Fallback 3: Try Name (+ Year) alone + // Last resort, drop the ids and search on name and year only. if (results.isEmpty && nameVal.isNotEmpty) { - final altInfoB = { + results = await _searchRemote({ 'Name': nameVal, 'Year': ?yearVal, - }; - debugPrint('--> [IdentifyRemoteSearch] Retry with Name + Year alone'); - results = await _adminApi.searchRemote(_searchType, { - 'SearchInfo': altInfoB, - 'ItemId': _targetItemId, - 'IncludeDisabledProviders': false, }); } - debugPrint('<-- [IdentifyRemoteSearch] Results received: ${results.length}'); if (!mounted) return; if (results.isEmpty) { @@ -446,10 +451,11 @@ class _IdentifyDialogState extends State { } Future _applyResult(Map result) async { + if (_applying) return; final l10n = AppLocalizations.of(context); bool replaceAllImages = true; - final confirmed = await showDialog( + final confirmed = await showFocusRestoringDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (dialogCtx, setDialogState) => AlertDialog.adaptive( @@ -507,11 +513,15 @@ class _IdentifyDialogState extends State { ), ); - if (confirmed != true || !mounted) return; + if (confirmed != true || !mounted || _applying) return; setState(() => _applying = true); try { - await _adminApi.applyRemoteSearchResult(_targetItemId, result); + await _adminApi.applyRemoteSearchResult( + _targetItemId, + result, + replaceAllImages: replaceAllImages, + ); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(l10n.adminRemoteMetadataApplied)), @@ -549,16 +559,12 @@ class _IdentifyDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - // Dialog Header Row( children: [ if (_searchResults != null) IconButton( icon: const Icon(Icons.arrow_back), - onPressed: () { - _clearResultFocusNodes(); - setState(() => _searchResults = null); - }, + onPressed: _backToSearchForm, tooltip: l10n.adminBackToSearch, ), Expanded( @@ -569,72 +575,64 @@ class _IdentifyDialogState extends State { style: theme.textTheme.headlineSmall, ), ), - // Requirement 1 & 2: Pronounced X focus & Down navigation & Select/Enter pop Focus( focusNode: _closeButtonFocusNode, onKeyEvent: (_, event) { - if (event is KeyDownEvent) { - final key = event.logicalKey; - if (key == LogicalKeyboardKey.select || - key == LogicalKeyboardKey.enter || - key == LogicalKeyboardKey.gameButtonA || - key == LogicalKeyboardKey.space) { - _closeDialog(false); - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowDown) { - if (_searchResults != null && - _resultFocusNodes != null && - _resultFocusNodes!.isNotEmpty) { - _resultFocusNodes![0].requestFocus(); - } else { - _arrowFocusNodes[0].requestFocus(); - } - return KeyEventResult.handled; + if (!event.isActionable) return KeyEventResult.ignored; + final key = event.logicalKey; + if (key.isSelectKey) { + _closeDialog(false); + return KeyEventResult.handled; + } + if (_isTV && key.isDownKey) { + if (_searchResults != null && + _resultFocusNodes != null && + _resultFocusNodes!.isNotEmpty) { + _resultFocusNodes![0].requestFocus(); + } else { + _fieldFocusNodes[0].requestFocus(); } + return KeyEventResult.handled; } return KeyEventResult.ignored; }, child: Builder( builder: (btnCtx) { final hasFocus = Focus.of(btnCtx).hasFocus; - return GestureDetector( + return InkWell( onTap: () => _closeDialog(false), - child: InkWell( - onTap: () => _closeDialog(false), - borderRadius: AppRadius.circular(20), - child: AnimatedContainer( - duration: const Duration(milliseconds: 150), - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - shape: BoxShape.circle, - color: hasFocus - ? theme.colorScheme.primary.withValues(alpha: 0.25) - : Colors.transparent, - border: Border.all( - color: hasFocus - ? theme.colorScheme.primary - : Colors.transparent, - width: 2.5, - ), - boxShadow: hasFocus - ? [ - BoxShadow( - color: theme.colorScheme.primary - .withValues(alpha: 0.6), - blurRadius: 10, - spreadRadius: 2, - ) - ] - : null, - ), - child: Icon( - Icons.close, + borderRadius: AppRadius.circular(20), + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + padding: const EdgeInsets.all(6), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: hasFocus + ? theme.colorScheme.primary.withValues(alpha: 0.25) + : Colors.transparent, + border: Border.all( color: hasFocus ? theme.colorScheme.primary - : theme.colorScheme.onSurface, - size: 20, + : Colors.transparent, + width: 2.5, ), + boxShadow: hasFocus + ? [ + BoxShadow( + color: theme.colorScheme.primary + .withValues(alpha: 0.6), + blurRadius: 10, + spreadRadius: 2, + ) + ] + : null, + ), + child: Icon( + Icons.close, + color: hasFocus + ? theme.colorScheme.primary + : theme.colorScheme.onSurface, + size: 20, ), ), ); @@ -645,7 +643,6 @@ class _IdentifyDialogState extends State { ), const Divider(height: 12), - // Dialog Body Expanded( child: _loadingItem || _applying ? const Center(child: CircularProgressIndicator()) @@ -654,7 +651,6 @@ class _IdentifyDialogState extends State { : _buildSearchForm(context), ), - // Requirement 5: Compact Button Bar if (_searchResults == null) ...[ Padding( padding: const EdgeInsets.only(top: 4, bottom: 2), @@ -664,23 +660,22 @@ class _IdentifyDialogState extends State { Focus( focusNode: _cancelButtonFocusNode, onKeyEvent: (_, event) { - if (event is KeyDownEvent) { - final key = event.logicalKey; - if (key == LogicalKeyboardKey.select || - key == LogicalKeyboardKey.enter || - key == LogicalKeyboardKey.gameButtonA || - key == LogicalKeyboardKey.space) { - _closeDialog(false); - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowUp) { - _fieldFocusNodes[7].requestFocus(); - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowRight) { - _searchButtonFocusNode.requestFocus(); - return KeyEventResult.handled; - } + if (!event.isActionable) { + return KeyEventResult.ignored; + } + final key = event.logicalKey; + if (key.isSelectKey) { + _closeDialog(false); + return KeyEventResult.handled; + } + if (!_isTV) return KeyEventResult.ignored; + if (key.isUpKey) { + _fieldFocusNodes[_lastFieldIndex].requestFocus(); + return KeyEventResult.handled; + } + if (key.isRightKey) { + _searchButtonFocusNode.requestFocus(); + return KeyEventResult.handled; } return KeyEventResult.ignored; }, @@ -709,23 +704,22 @@ class _IdentifyDialogState extends State { Focus( focusNode: _searchButtonFocusNode, onKeyEvent: (_, event) { - if (event is KeyDownEvent) { - final key = event.logicalKey; - if (key == LogicalKeyboardKey.select || - key == LogicalKeyboardKey.enter || - key == LogicalKeyboardKey.gameButtonA || - key == LogicalKeyboardKey.space) { - if (!_searching) _performSearch(); - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowUp) { - _arrowFocusNodes[7].requestFocus(); - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowLeft) { - _cancelButtonFocusNode.requestFocus(); - return KeyEventResult.handled; - } + if (!event.isActionable) { + return KeyEventResult.ignored; + } + final key = event.logicalKey; + if (key.isSelectKey) { + if (!_searching) _performSearch(); + return KeyEventResult.handled; + } + if (!_isTV) return KeyEventResult.ignored; + if (key.isUpKey) { + _arrowFocusNodes[_lastFieldIndex].requestFocus(); + return KeyEventResult.handled; + } + if (key.isLeftKey) { + _cancelButtonFocusNode.requestFocus(); + return KeyEventResult.handled; } return KeyEventResult.ignored; }, @@ -768,14 +762,54 @@ class _IdentifyDialogState extends State { ); } + /// The form rows in d-pad order. Position matters because the focus nodes are + /// keyed off it. + List<_IdentifyField> _searchFields(AppLocalizations l10n) => [ + _IdentifyField(l10n.name, _nameController, _refName), + _IdentifyField(l10n.adminLabelYear, _yearController, _refYear, + keyboardType: TextInputType.number), + _IdentifyField(l10n.adminLabelImdbId, _imdbController, _refImdb), + _IdentifyField( + l10n.adminLabelTmdbMovieId, _tmdbMovieController, _refTmdbMovie), + _IdentifyField( + l10n.adminLabelTmdbBoxSetId, _tmdbBoxSetController, _refTmdbBoxSet), + _IdentifyField( + l10n.adminLabelTvdbBoxSetId, _tvdbBoxSetController, _refTvdbBoxSet), + _IdentifyField(l10n.adminLabelTvdbId, _tvdbIdController, _refTvdbId), + _IdentifyField( + l10n.adminLabelTvdbSlug, _tvdbSlugController, _refTvdbSlug), + ]; + Widget _buildSearchForm(BuildContext context) { final theme = Theme.of(context); final l10n = AppLocalizations.of(context); + final fields = _searchFields(l10n); + assert(fields.length == _fieldCount); return SingleChildScrollView( controller: _scrollController, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ + if (_loadError != null) ...[ + Row( + children: [ + Icon(Icons.warning_amber_rounded, + size: 18, color: theme.colorScheme.error), + const SizedBox(width: 8), + Expanded( + child: Text( + l10n.adminMetadataLoadFailed(_loadError!), + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.error, + ), + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + const SizedBox(height: 12), + ], if (_path != null && _path!.isNotEmpty) ...[ Container( width: double.infinity, @@ -785,7 +819,7 @@ class _IdentifyDialogState extends State { borderRadius: AppRadius.circular(8), ), child: SelectableText( - 'Path: $_path', + '${l10n.path}: $_path', style: theme.textTheme.bodySmall?.copyWith( fontFamily: 'monospace', color: theme.colorScheme.onSurfaceVariant, @@ -795,7 +829,6 @@ class _IdentifyDialogState extends State { const SizedBox(height: 12), ], - // Column Headers Padding( padding: const EdgeInsets.only(bottom: 10), child: Row( @@ -810,7 +843,8 @@ class _IdentifyDialogState extends State { ), ), ), - const SizedBox(width: 48), // Arrow button spacing + // Matches the arrow column: 6px gap + 30px button + 6px gap. + const SizedBox(width: 42), Expanded( flex: 2, child: Text( @@ -825,217 +859,166 @@ class _IdentifyDialogState extends State { ), ), - _buildFieldRow( - index: 0, - label: l10n.adminLabelName, - controller: _nameController, - referenceValue: _refName, - ), - _buildFieldRow( - index: 1, - label: l10n.adminLabelYear, - controller: _yearController, - referenceValue: _refYear, - keyboardType: TextInputType.number, - ), - _buildFieldRow( - index: 2, - label: l10n.adminLabelImdbId, - controller: _imdbController, - referenceValue: _refImdb, - ), - _buildFieldRow( - index: 3, - label: l10n.adminLabelTmdbMovieId, - controller: _tmdbMovieController, - referenceValue: _refTmdbMovie, - ), - _buildFieldRow( - index: 4, - label: l10n.adminLabelTmdbBoxSetId, - controller: _tmdbBoxSetController, - referenceValue: _refTmdbBoxSet, - ), - _buildFieldRow( - index: 5, - label: l10n.adminLabelTvdbBoxSetId, - controller: _tvdbBoxSetController, - referenceValue: _refTvdbBoxSet, - ), - _buildFieldRow( - index: 6, - label: l10n.adminLabelTvdbId, - controller: _tvdbIdController, - referenceValue: _refTvdbId, - ), - _buildFieldRow( - index: 7, - label: l10n.adminLabelTvdbSlug, - controller: _tvdbSlugController, - referenceValue: _refTvdbSlug, - ), + for (var i = 0; i < fields.length; i++) _buildFieldRow(i, fields[i]), ], ), ); } - Widget _buildFieldRow({ - required int index, - required String label, - required TextEditingController controller, - required String? referenceValue, - TextInputType? keyboardType, - }) { + Widget _buildFieldRow(int index, _IdentifyField field) { final theme = Theme.of(context); - final hasReference = referenceValue != null && referenceValue.trim().isNotEmpty; + final label = field.label; + final controller = field.controller; + final keyboardType = field.keyboardType; + final reference = field.reference?.trim(); + final hasReference = reference != null && reference.isNotEmpty; final fieldFocusNode = _fieldFocusNodes[index]; final arrowFocusNode = _arrowFocusNodes[index]; final tvFieldKey = _tvFieldKeys[index]; + void pullReference() { + if (hasReference) setState(() => controller.text = reference); + } return Padding( padding: const EdgeInsets.only(bottom: 10.0), child: Row( crossAxisAlignment: CrossAxisAlignment.center, children: [ - // Left Column: Search Input Field (Uses CustomTVTextField on TV) + // Only TV gets the d-pad wrapper. Anywhere else a handler that + // swallows space and the arrows would stop them reaching the text + // input, so the field would refuse spaces and the caret couldn't move. Expanded( flex: 3, - child: Focus( - focusNode: fieldFocusNode, - onKeyEvent: (_, event) { - if (event is KeyDownEvent) { - final key = event.logicalKey; - if (key == LogicalKeyboardKey.select || - key == LogicalKeyboardKey.enter || - key == LogicalKeyboardKey.gameButtonA || - key == LogicalKeyboardKey.space) { - if (!fieldFocusNode.hasFocus) { - fieldFocusNode.requestFocus(); - } - tvFieldKey.currentState?.openKeyboard(); - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowRight) { - arrowFocusNode.requestFocus(); - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowDown) { - if (index < 7) { - _fieldFocusNodes[index + 1].requestFocus(); - } else { - _cancelButtonFocusNode.requestFocus(); - } - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowUp) { - if (index > 0) { - _fieldFocusNodes[index - 1].requestFocus(); - } else { - _closeButtonFocusNode.requestFocus(); - } - return KeyEventResult.handled; - } - } - return KeyEventResult.ignored; - }, - child: ListenableBuilder( - listenable: fieldFocusNode, - builder: (ctx, _) { - if (PlatformDetection.isTV) { - final preferSystemIme = - _prefs.get(UserPreferences.preferSystemImeKeyboard); - return GestureDetector( - onTap: () { + child: _isTV + ? Focus( + focusNode: fieldFocusNode, + onKeyEvent: (_, event) { + if (!event.isActionable) return KeyEventResult.ignored; + final key = event.logicalKey; + if (key.isSelectKey) { if (!fieldFocusNode.hasFocus) { fieldFocusNode.requestFocus(); } tvFieldKey.currentState?.openKeyboard(); + return KeyEventResult.handled; + } + if (key.isRightKey) { + arrowFocusNode.requestFocus(); + return KeyEventResult.handled; + } + if (key.isDownKey) { + if (index < _lastFieldIndex) { + _fieldFocusNodes[index + 1].requestFocus(); + } else { + _cancelButtonFocusNode.requestFocus(); + } + return KeyEventResult.handled; + } + if (key.isUpKey) { + if (index > 0) { + _fieldFocusNodes[index - 1].requestFocus(); + } else { + _closeButtonFocusNode.requestFocus(); + } + return KeyEventResult.handled; + } + return KeyEventResult.ignored; + }, + child: ListenableBuilder( + listenable: fieldFocusNode, + builder: (ctx, _) { + final preferSystemIme = + _prefs.get(UserPreferences.preferSystemImeKeyboard); + return GestureDetector( + onTap: () { + if (!fieldFocusNode.hasFocus) { + fieldFocusNode.requestFocus(); + } + tvFieldKey.currentState?.openKeyboard(); + }, + child: CustomTVTextField( + key: tvFieldKey, + controller: controller, + isFocused: fieldFocusNode.hasFocus, + inputPurpose: InputPurpose.text, + keyboardType: keyboardType == TextInputType.number + ? KeyboardType.numeric + : KeyboardType.alphabetic, + preferSystemIme: preferSystemIme, + hint: label, + filled: true, + fillColor: theme.colorScheme.surfaceContainerHighest + .withValues(alpha: 0.4), + borderColor: theme.colorScheme.outlineVariant + .withValues(alpha: 0.4), + focusedBorderColor: theme.colorScheme.primary, + hintStyle: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant + .withValues(alpha: 0.6), + ), + textStyle: theme.textTheme.bodyMedium?.copyWith( + color: theme.colorScheme.onSurface, + ), + popParentOnKeyboardClose: false, + ), + ); }, - child: CustomTVTextField( - key: tvFieldKey, - controller: controller, - isFocused: fieldFocusNode.hasFocus, - inputPurpose: InputPurpose.text, - keyboardType: keyboardType == TextInputType.number - ? KeyboardType.numeric - : KeyboardType.alphabetic, - preferSystemIme: preferSystemIme, - hint: label, - filled: true, - fillColor: theme.colorScheme.surfaceContainerHighest - .withValues(alpha: 0.4), - borderColor: theme.colorScheme.outlineVariant - .withValues(alpha: 0.4), - focusedBorderColor: theme.colorScheme.primary, - hintStyle: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurfaceVariant - .withValues(alpha: 0.6), - ), - textStyle: theme.textTheme.bodyMedium?.copyWith( - color: theme.colorScheme.onSurface, - ), - popParentOnKeyboardClose: false, - ), - ); - } - - return TextField( + ), + ) + : TextField( controller: controller, + focusNode: fieldFocusNode, keyboardType: keyboardType, + textInputAction: TextInputAction.search, + onSubmitted: (_) { + if (!_searching) _performSearch(); + }, style: theme.textTheme.bodyMedium, decoration: InputDecoration( labelText: label, border: const OutlineInputBorder(), isDense: true, ), - ); - }, - ), - ), + ), ), const SizedBox(width: 6), - // Pull Action Button with explicit Up/Down/Left/Right TV D-pad linking & Select/Enter activation + // Copies the current value on the right into the field on the left. Focus( focusNode: arrowFocusNode, onKeyEvent: (_, event) { - if (event is KeyDownEvent) { - final key = event.logicalKey; - if (key == LogicalKeyboardKey.select || - key == LogicalKeyboardKey.enter || - key == LogicalKeyboardKey.gameButtonA || - key == LogicalKeyboardKey.space) { - if (hasReference) { - setState(() { - controller.text = referenceValue.trim(); - }); - } - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowUp) { - if (index == 0) { - _closeButtonFocusNode.requestFocus(); - } else { - _arrowFocusNodes[index - 1].requestFocus(); - } - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowDown) { - if (index < 7) { - _arrowFocusNodes[index + 1].requestFocus(); - } else { - _searchButtonFocusNode.requestFocus(); - } - return KeyEventResult.handled; + if (!event.isActionable) return KeyEventResult.ignored; + final key = event.logicalKey; + if (key.isSelectKey) { + pullReference(); + return KeyEventResult.handled; + } + // Normal traversal already reaches this button off TV, so the + // arrow keys are left alone there. + if (!_isTV) return KeyEventResult.ignored; + if (key.isUpKey) { + if (index == 0) { + _closeButtonFocusNode.requestFocus(); + } else { + _arrowFocusNodes[index - 1].requestFocus(); } - if (key == LogicalKeyboardKey.arrowRight) { + return KeyEventResult.handled; + } + if (key.isDownKey) { + if (index < _lastFieldIndex) { + _arrowFocusNodes[index + 1].requestFocus(); + } else { _searchButtonFocusNode.requestFocus(); - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowLeft) { - fieldFocusNode.requestFocus(); - return KeyEventResult.handled; } + return KeyEventResult.handled; + } + if (key.isRightKey) { + _searchButtonFocusNode.requestFocus(); + return KeyEventResult.handled; + } + if (key.isLeftKey) { + fieldFocusNode.requestFocus(); + return KeyEventResult.handled; } return KeyEventResult.ignored; }, @@ -1043,13 +1026,7 @@ class _IdentifyDialogState extends State { builder: (btnCtx) { final hasFocus = Focus.of(btnCtx).hasFocus; return InkWell( - onTap: hasReference - ? () { - setState(() { - controller.text = referenceValue.trim(); - }); - } - : null, + onTap: hasReference ? pullReference : null, borderRadius: AppRadius.circular(20), child: Container( padding: const EdgeInsets.all(6), @@ -1083,7 +1060,6 @@ class _IdentifyDialogState extends State { const SizedBox(width: 6), - // Right Column: Non-editable reference text (Borderless reference badge) Expanded( flex: 2, child: Container( @@ -1101,7 +1077,7 @@ class _IdentifyDialogState extends State { ), ), child: Text( - hasReference ? referenceValue.trim() : '—', + hasReference ? reference : '-', style: theme.textTheme.bodyMedium?.copyWith( color: hasReference ? theme.colorScheme.onSurfaceVariant.withValues(alpha: 0.85) @@ -1142,29 +1118,27 @@ class _IdentifyDialogState extends State { child: Focus( focusNode: resultFocusNode, onKeyEvent: (_, event) { - if (event is KeyDownEvent) { - final key = event.logicalKey; - if (key == LogicalKeyboardKey.select || - key == LogicalKeyboardKey.enter || - key == LogicalKeyboardKey.gameButtonA || - key == LogicalKeyboardKey.space) { - _applyResult(item); - return KeyEventResult.handled; - } - if (key == LogicalKeyboardKey.arrowUp) { - if (index == 0) { - _closeButtonFocusNode.requestFocus(); - } else if (_resultFocusNodes != null && index > 0) { - _resultFocusNodes![index - 1].requestFocus(); - } - return KeyEventResult.handled; + if (!event.isActionable) return KeyEventResult.ignored; + final key = event.logicalKey; + if (key.isSelectKey) { + _applyResult(item); + return KeyEventResult.handled; + } + if (!_isTV) return KeyEventResult.ignored; + if (key.isUpKey) { + if (index == 0) { + _closeButtonFocusNode.requestFocus(); + } else if (_resultFocusNodes != null && index > 0) { + _resultFocusNodes![index - 1].requestFocus(); } - if (key == LogicalKeyboardKey.arrowDown) { - if (_resultFocusNodes != null && index < _resultFocusNodes!.length - 1) { - _resultFocusNodes![index + 1].requestFocus(); - } - return KeyEventResult.handled; + return KeyEventResult.handled; + } + if (key.isDownKey) { + if (_resultFocusNodes != null && + index < _resultFocusNodes!.length - 1) { + _resultFocusNodes![index + 1].requestFocus(); } + return KeyEventResult.handled; } return KeyEventResult.ignored; }, @@ -1272,3 +1246,19 @@ class _IdentifyDialogState extends State { ); } } + +/// One row of the search form, pairing an editable field with the value the +/// item already carries. +class _IdentifyField { + final String label; + final TextEditingController controller; + final String? reference; + final TextInputType? keyboardType; + + const _IdentifyField( + this.label, + this.controller, + this.reference, { + this.keyboardType, + }); +} diff --git a/packages/server_core/lib/src/api/admin_items_api.dart b/packages/server_core/lib/src/api/admin_items_api.dart index ccc176a84..9b667db3c 100644 --- a/packages/server_core/lib/src/api/admin_items_api.dart +++ b/packages/server_core/lib/src/api/admin_items_api.dart @@ -21,10 +21,14 @@ abstract class AdminItemsApi { Future>> searchRemotePerson( Map query, ); + /// Applies a result returned by [searchRemote] to [itemId]. When + /// [replaceAllImages] is false the item keeps its existing artwork and only + /// textual metadata is replaced. Future applyRemoteSearchResult( String itemId, - Map result, - ); + Map result, { + bool replaceAllImages = true, + }); Future> getRemoteImages( String itemId, { required ImageType imageType, diff --git a/packages/server_jellyfin/lib/src/api/jellyfin_admin_items_api.dart b/packages/server_jellyfin/lib/src/api/jellyfin_admin_items_api.dart index 153707518..23d8fca28 100644 --- a/packages/server_jellyfin/lib/src/api/jellyfin_admin_items_api.dart +++ b/packages/server_jellyfin/lib/src/api/jellyfin_admin_items_api.dart @@ -102,14 +102,16 @@ class JellyfinAdminItemsApi implements AdminItemsApi { @override Future applyRemoteSearchResult( String itemId, - Map result, - ) async { + Map result, { + bool replaceAllImages = true, + }) async { await _requestWithFallback( methods: const ['POST'], paths: [ '/Items/RemoteSearch/Apply/$itemId', '/Items/$itemId/RemoteSearch/Apply', ], + queryParameters: {'replaceAllImages': replaceAllImages}, data: result, convert: _ignoreResponse, );