From 1665e4cd576d06e7be04ea13c264eacbd81e3bc3 Mon Sep 17 00:00:00 2001 From: zarzet Date: Sun, 15 Mar 2026 20:35:42 +0700 Subject: [PATCH] feat: selective auto-fill from online in Edit Metadata sheet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add 'Auto-fill from online' expandable section to the metadata editor that lets users choose exactly which fields to populate from online metadata search. Users can select individual fields via filter chips, use 'All' or 'Empty only' quick-select buttons, then tap 'Fetch & Fill' to search metadata providers and fill only the selected controllers. The search uses existing searchTracksWithMetadataProviders API with ISRC-preferring best-match selection. Extended metadata (genre, label, copyright) is fetched via Deezer extended metadata API when available. Cover art is downloaded from the match's cover_url. All results are previewed in the editor before saving — nothing is written to the file until the user taps Save. Add 21 new l10n keys (editMetadata* namespace) for all UI strings. --- lib/l10n/app_localizations.dart | 126 +++++++ lib/l10n/app_localizations_de.dart | 74 ++++ lib/l10n/app_localizations_en.dart | 74 ++++ lib/l10n/app_localizations_es.dart | 74 ++++ lib/l10n/app_localizations_fr.dart | 74 ++++ lib/l10n/app_localizations_hi.dart | 74 ++++ lib/l10n/app_localizations_id.dart | 74 ++++ lib/l10n/app_localizations_ja.dart | 74 ++++ lib/l10n/app_localizations_ko.dart | 74 ++++ lib/l10n/app_localizations_nl.dart | 74 ++++ lib/l10n/app_localizations_pt.dart | 74 ++++ lib/l10n/app_localizations_ru.dart | 74 ++++ lib/l10n/app_localizations_tr.dart | 74 ++++ lib/l10n/app_localizations_zh.dart | 74 ++++ lib/l10n/arb/app_en.arb | 89 +++++ lib/screens/track_metadata_screen.dart | 479 +++++++++++++++++++++++++ 16 files changed, 1656 insertions(+) diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index ae9a4cc0..01af801f 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -4817,6 +4817,132 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'{count, plural, =1{1 playlist} other{{count} playlists}}'** String playlistsCount(int count); + + /// Section title for selective online metadata auto-fill in the edit metadata sheet + /// + /// In en, this message translates to: + /// **'Auto-fill from online'** + String get editMetadataAutoFill; + + /// Description for the auto-fill section + /// + /// In en, this message translates to: + /// **'Select fields to fill automatically from online metadata'** + String get editMetadataAutoFillDesc; + + /// Button label to fetch online metadata and fill selected fields + /// + /// In en, this message translates to: + /// **'Fetch & Fill'** + String get editMetadataAutoFillFetch; + + /// Snackbar shown while searching for online metadata + /// + /// In en, this message translates to: + /// **'Searching online...'** + String get editMetadataAutoFillSearching; + + /// Snackbar when online metadata search returns no results + /// + /// In en, this message translates to: + /// **'No matching metadata found online'** + String get editMetadataAutoFillNoResults; + + /// Snackbar confirming how many fields were auto-filled + /// + /// In en, this message translates to: + /// **'Filled {count} {count, plural, =1{field} other{fields}} from online metadata'** + String editMetadataAutoFillDone(int count); + + /// Snackbar when user taps Fetch without selecting any fields + /// + /// In en, this message translates to: + /// **'Select at least one field to auto-fill'** + String get editMetadataAutoFillNoneSelected; + + /// Chip label for title field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Title'** + String get editMetadataFieldTitle; + + /// Chip label for artist field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Artist'** + String get editMetadataFieldArtist; + + /// Chip label for album field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Album'** + String get editMetadataFieldAlbum; + + /// Chip label for album artist field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Album Artist'** + String get editMetadataFieldAlbumArtist; + + /// Chip label for date field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Date'** + String get editMetadataFieldDate; + + /// Chip label for track number field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Track #'** + String get editMetadataFieldTrackNum; + + /// Chip label for disc number field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Disc #'** + String get editMetadataFieldDiscNum; + + /// Chip label for genre field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Genre'** + String get editMetadataFieldGenre; + + /// Chip label for ISRC field in auto-fill selector + /// + /// In en, this message translates to: + /// **'ISRC'** + String get editMetadataFieldIsrc; + + /// Chip label for label field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Label'** + String get editMetadataFieldLabel; + + /// Chip label for copyright field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Copyright'** + String get editMetadataFieldCopyright; + + /// Chip label for cover art field in auto-fill selector + /// + /// In en, this message translates to: + /// **'Cover Art'** + String get editMetadataFieldCover; + + /// Button to select all fields for auto-fill + /// + /// In en, this message translates to: + /// **'All'** + String get editMetadataSelectAll; + + /// Button to select only fields that are currently empty + /// + /// In en, this message translates to: + /// **'Empty only'** + String get editMetadataSelectEmpty; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index ce489e3d..c3ee5083 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -2826,4 +2826,78 @@ class AppLocalizationsDe extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index bd280e94..180810f3 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -2798,4 +2798,78 @@ class AppLocalizationsEn extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_es.dart b/lib/l10n/app_localizations_es.dart index 81ce15ff..cea3915b 100644 --- a/lib/l10n/app_localizations_es.dart +++ b/lib/l10n/app_localizations_es.dart @@ -2798,6 +2798,80 @@ class AppLocalizationsEs extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } /// The translations for Spanish Castilian, as used in Spain (`es_ES`). diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index b35b22d0..8270c7c2 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -2800,4 +2800,78 @@ class AppLocalizationsFr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_hi.dart b/lib/l10n/app_localizations_hi.dart index e36316e9..1767c438 100644 --- a/lib/l10n/app_localizations_hi.dart +++ b/lib/l10n/app_localizations_hi.dart @@ -2798,4 +2798,78 @@ class AppLocalizationsHi extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_id.dart b/lib/l10n/app_localizations_id.dart index 68bffbe5..67a9c2fc 100644 --- a/lib/l10n/app_localizations_id.dart +++ b/lib/l10n/app_localizations_id.dart @@ -2805,4 +2805,78 @@ class AppLocalizationsId extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_ja.dart b/lib/l10n/app_localizations_ja.dart index 8145c794..e406ba35 100644 --- a/lib/l10n/app_localizations_ja.dart +++ b/lib/l10n/app_localizations_ja.dart @@ -2785,4 +2785,78 @@ class AppLocalizationsJa extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_ko.dart b/lib/l10n/app_localizations_ko.dart index 66d21aec..b8085a70 100644 --- a/lib/l10n/app_localizations_ko.dart +++ b/lib/l10n/app_localizations_ko.dart @@ -2778,4 +2778,78 @@ class AppLocalizationsKo extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_nl.dart b/lib/l10n/app_localizations_nl.dart index e87906ac..c54e044a 100644 --- a/lib/l10n/app_localizations_nl.dart +++ b/lib/l10n/app_localizations_nl.dart @@ -2798,4 +2798,78 @@ class AppLocalizationsNl extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_pt.dart b/lib/l10n/app_localizations_pt.dart index 466572ce..ac0f577d 100644 --- a/lib/l10n/app_localizations_pt.dart +++ b/lib/l10n/app_localizations_pt.dart @@ -2798,6 +2798,80 @@ class AppLocalizationsPt extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } /// The translations for Portuguese, as used in Portugal (`pt_PT`). diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index dd460698..76814d38 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -2857,4 +2857,78 @@ class AppLocalizationsRu extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index 6e1a37f1..f24d9f88 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -2810,4 +2810,78 @@ class AppLocalizationsTr extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } diff --git a/lib/l10n/app_localizations_zh.dart b/lib/l10n/app_localizations_zh.dart index 9248d997..d9440cff 100644 --- a/lib/l10n/app_localizations_zh.dart +++ b/lib/l10n/app_localizations_zh.dart @@ -2798,6 +2798,80 @@ class AppLocalizationsZh extends AppLocalizations { ); return '$_temp0'; } + + @override + String get editMetadataAutoFill => 'Auto-fill from online'; + + @override + String get editMetadataAutoFillDesc => + 'Select fields to fill automatically from online metadata'; + + @override + String get editMetadataAutoFillFetch => 'Fetch & Fill'; + + @override + String get editMetadataAutoFillSearching => 'Searching online...'; + + @override + String get editMetadataAutoFillNoResults => + 'No matching metadata found online'; + + @override + String editMetadataAutoFillDone(int count) { + String _temp0 = intl.Intl.pluralLogic( + count, + locale: localeName, + other: 'fields', + one: 'field', + ); + return 'Filled $count $_temp0 from online metadata'; + } + + @override + String get editMetadataAutoFillNoneSelected => + 'Select at least one field to auto-fill'; + + @override + String get editMetadataFieldTitle => 'Title'; + + @override + String get editMetadataFieldArtist => 'Artist'; + + @override + String get editMetadataFieldAlbum => 'Album'; + + @override + String get editMetadataFieldAlbumArtist => 'Album Artist'; + + @override + String get editMetadataFieldDate => 'Date'; + + @override + String get editMetadataFieldTrackNum => 'Track #'; + + @override + String get editMetadataFieldDiscNum => 'Disc #'; + + @override + String get editMetadataFieldGenre => 'Genre'; + + @override + String get editMetadataFieldIsrc => 'ISRC'; + + @override + String get editMetadataFieldLabel => 'Label'; + + @override + String get editMetadataFieldCopyright => 'Copyright'; + + @override + String get editMetadataFieldCover => 'Cover Art'; + + @override + String get editMetadataSelectAll => 'All'; + + @override + String get editMetadataSelectEmpty => 'Empty only'; } /// The translations for Chinese, as used in China (`zh_CN`). diff --git a/lib/l10n/arb/app_en.arb b/lib/l10n/arb/app_en.arb index 3de4ec31..6da406d0 100644 --- a/lib/l10n/arb/app_en.arb +++ b/lib/l10n/arb/app_en.arb @@ -3706,5 +3706,94 @@ "type": "int" } } + }, + "editMetadataAutoFill": "Auto-fill from online", + "@editMetadataAutoFill": { + "description": "Section title for selective online metadata auto-fill in the edit metadata sheet" + }, + "editMetadataAutoFillDesc": "Select fields to fill automatically from online metadata", + "@editMetadataAutoFillDesc": { + "description": "Description for the auto-fill section" + }, + "editMetadataAutoFillFetch": "Fetch & Fill", + "@editMetadataAutoFillFetch": { + "description": "Button label to fetch online metadata and fill selected fields" + }, + "editMetadataAutoFillSearching": "Searching online...", + "@editMetadataAutoFillSearching": { + "description": "Snackbar shown while searching for online metadata" + }, + "editMetadataAutoFillNoResults": "No matching metadata found online", + "@editMetadataAutoFillNoResults": { + "description": "Snackbar when online metadata search returns no results" + }, + "editMetadataAutoFillDone": "Filled {count} {count, plural, =1{field} other{fields}} from online metadata", + "@editMetadataAutoFillDone": { + "description": "Snackbar confirming how many fields were auto-filled", + "placeholders": { + "count": { + "type": "int" + } + } + }, + "editMetadataAutoFillNoneSelected": "Select at least one field to auto-fill", + "@editMetadataAutoFillNoneSelected": { + "description": "Snackbar when user taps Fetch without selecting any fields" + }, + "editMetadataFieldTitle": "Title", + "@editMetadataFieldTitle": { + "description": "Chip label for title field in auto-fill selector" + }, + "editMetadataFieldArtist": "Artist", + "@editMetadataFieldArtist": { + "description": "Chip label for artist field in auto-fill selector" + }, + "editMetadataFieldAlbum": "Album", + "@editMetadataFieldAlbum": { + "description": "Chip label for album field in auto-fill selector" + }, + "editMetadataFieldAlbumArtist": "Album Artist", + "@editMetadataFieldAlbumArtist": { + "description": "Chip label for album artist field in auto-fill selector" + }, + "editMetadataFieldDate": "Date", + "@editMetadataFieldDate": { + "description": "Chip label for date field in auto-fill selector" + }, + "editMetadataFieldTrackNum": "Track #", + "@editMetadataFieldTrackNum": { + "description": "Chip label for track number field in auto-fill selector" + }, + "editMetadataFieldDiscNum": "Disc #", + "@editMetadataFieldDiscNum": { + "description": "Chip label for disc number field in auto-fill selector" + }, + "editMetadataFieldGenre": "Genre", + "@editMetadataFieldGenre": { + "description": "Chip label for genre field in auto-fill selector" + }, + "editMetadataFieldIsrc": "ISRC", + "@editMetadataFieldIsrc": { + "description": "Chip label for ISRC field in auto-fill selector" + }, + "editMetadataFieldLabel": "Label", + "@editMetadataFieldLabel": { + "description": "Chip label for label field in auto-fill selector" + }, + "editMetadataFieldCopyright": "Copyright", + "@editMetadataFieldCopyright": { + "description": "Chip label for copyright field in auto-fill selector" + }, + "editMetadataFieldCover": "Cover Art", + "@editMetadataFieldCover": { + "description": "Chip label for cover art field in auto-fill selector" + }, + "editMetadataSelectAll": "All", + "@editMetadataSelectAll": { + "description": "Button to select all fields for auto-fill" + }, + "editMetadataSelectEmpty": "Empty only", + "@editMetadataSelectEmpty": { + "description": "Button to select only fields that are currently empty" } } diff --git a/lib/screens/track_metadata_screen.dart b/lib/screens/track_metadata_screen.dart index cd37ee6a..989b6a47 100644 --- a/lib/screens/track_metadata_screen.dart +++ b/lib/screens/track_metadata_screen.dart @@ -3932,6 +3932,8 @@ class _EditMetadataSheet extends StatefulWidget { class _EditMetadataSheetState extends State<_EditMetadataSheet> { bool _saving = false; bool _showAdvanced = false; + bool _showAutoFill = false; + bool _fetching = false; String? _selectedCoverPath; String? _selectedCoverTempDir; String? _selectedCoverName; @@ -3939,6 +3941,25 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { String? _currentCoverTempDir; bool _loadingCurrentCover = false; + // Auto-fill field selection — which fields the user wants to fetch + final Set _autoFillFields = {}; + + // All auto-fillable fields and their mapping + static const _fieldDefs = { + 'title': 'title', + 'artist': 'artist', + 'album': 'album', + 'album_artist': 'album_artist', + 'date': 'date', + 'track_number': 'track_number', + 'disc_number': 'disc_number', + 'genre': 'genre', + 'isrc': 'isrc', + 'label': 'label', + 'copyright': 'copyright', + 'cover': 'cover', + }; + late final TextEditingController _titleCtrl; late final TextEditingController _artistCtrl; late final TextEditingController _albumCtrl; @@ -4132,6 +4153,286 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { } } + String _fieldLabel(String key) { + final l10n = context.l10n; + switch (key) { + case 'title': + return l10n.editMetadataFieldTitle; + case 'artist': + return l10n.editMetadataFieldArtist; + case 'album': + return l10n.editMetadataFieldAlbum; + case 'album_artist': + return l10n.editMetadataFieldAlbumArtist; + case 'date': + return l10n.editMetadataFieldDate; + case 'track_number': + return l10n.editMetadataFieldTrackNum; + case 'disc_number': + return l10n.editMetadataFieldDiscNum; + case 'genre': + return l10n.editMetadataFieldGenre; + case 'isrc': + return l10n.editMetadataFieldIsrc; + case 'label': + return l10n.editMetadataFieldLabel; + case 'copyright': + return l10n.editMetadataFieldCopyright; + case 'cover': + return l10n.editMetadataFieldCover; + default: + return key; + } + } + + TextEditingController? _controllerForKey(String key) { + switch (key) { + case 'title': + return _titleCtrl; + case 'artist': + return _artistCtrl; + case 'album': + return _albumCtrl; + case 'album_artist': + return _albumArtistCtrl; + case 'date': + return _dateCtrl; + case 'track_number': + return _trackNumCtrl; + case 'disc_number': + return _discNumCtrl; + case 'genre': + return _genreCtrl; + case 'isrc': + return _isrcCtrl; + case 'label': + return _labelCtrl; + case 'copyright': + return _copyrightCtrl; + default: + return null; + } + } + + void _selectAllFields() { + setState(() { + _autoFillFields.addAll(_fieldDefs.keys); + }); + } + + void _selectEmptyFields() { + setState(() { + _autoFillFields.clear(); + for (final key in _fieldDefs.keys) { + if (key == 'cover') { + if (!_hasValue(_currentCoverPath) && !_hasValue(_selectedCoverPath)) { + _autoFillFields.add(key); + } + continue; + } + final ctrl = _controllerForKey(key); + if (ctrl != null && ctrl.text.trim().isEmpty) { + _autoFillFields.add(key); + } + } + }); + } + + Future _fetchAndFill() async { + if (_autoFillFields.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.editMetadataAutoFillNoneSelected)), + ); + return; + } + + setState(() => _fetching = true); + + try { + // Build search query from current field values + final title = _titleCtrl.text.trim(); + final artist = _artistCtrl.text.trim(); + final album = _albumCtrl.text.trim(); + final queryParts = []; + if (title.isNotEmpty) queryParts.add(title); + if (artist.isNotEmpty) queryParts.add(artist); + if (album.isNotEmpty) queryParts.add(album); + + if (queryParts.isEmpty) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.editMetadataAutoFillNoResults), + ), + ); + } + return; + } + + final query = queryParts.join(' '); + final results = await PlatformBridge.searchTracksWithMetadataProviders( + query, + limit: 5, + ); + + if (!mounted) return; + + if (results.isEmpty) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(context.l10n.editMetadataAutoFillNoResults)), + ); + return; + } + + // Pick best match: prefer ISRC match, then first result + final currentIsrc = _isrcCtrl.text.trim().toUpperCase(); + Map? best; + if (currentIsrc.isNotEmpty) { + for (final r in results) { + final candidateIsrc = + (r['isrc']?.toString() ?? '').trim().toUpperCase(); + if (candidateIsrc == currentIsrc) { + best = r; + break; + } + } + } + best ??= results.first; + + // Extract metadata from best match + final enriched = { + 'title': (best['name'] ?? '').toString(), + 'artist': (best['artists'] ?? best['artist'] ?? '').toString(), + 'album': (best['album_name'] ?? best['album'] ?? '').toString(), + 'album_artist': (best['album_artist'] ?? '').toString(), + 'date': (best['release_date'] ?? '').toString(), + 'track_number': (best['track_number'] ?? '').toString(), + 'disc_number': (best['disc_number'] ?? '').toString(), + 'isrc': (best['isrc'] ?? '').toString(), + }; + + // Try to get extended metadata (genre, label, copyright) from Deezer + final trackId = + (best['spotify_id'] ?? best['id'] ?? '').toString(); + final source = (best['source'] ?? best['provider_id'] ?? '').toString(); + + if ((_autoFillFields.contains('genre') || + _autoFillFields.contains('label') || + _autoFillFields.contains('copyright')) && + trackId.isNotEmpty) { + try { + // If source is Deezer, fetch extended metadata directly + Map? extended; + if (source.toLowerCase().contains('deezer')) { + extended = await PlatformBridge.getDeezerExtendedMetadata(trackId); + } else { + // Try ISRC lookup via Deezer for genre/label/copyright + final isrcForLookup = enriched['isrc'] ?? ''; + if (isrcForLookup.isNotEmpty) { + try { + final deezerResult = await PlatformBridge.searchDeezerByISRC( + isrcForLookup, + ); + final deezerTrackId = + (deezerResult['id'] ?? deezerResult['track_id'] ?? '') + .toString(); + if (deezerTrackId.isNotEmpty) { + extended = await PlatformBridge.getDeezerExtendedMetadata( + deezerTrackId, + ); + } + } catch (_) {} + } + } + if (extended != null) { + enriched['genre'] = extended['genre'] ?? ''; + enriched['label'] = extended['label'] ?? ''; + enriched['copyright'] = extended['copyright'] ?? ''; + } + } catch (_) { + // Extended metadata is best-effort + } + } + + if (!mounted) return; + + // Apply selected fields to controllers + var filledCount = 0; + for (final key in _autoFillFields) { + if (key == 'cover') continue; // Handle cover separately below + final value = enriched[key]; + if (value != null && value.isNotEmpty && value != '0' && value != 'null') { + final ctrl = _controllerForKey(key); + if (ctrl != null) { + ctrl.text = value; + filledCount++; + } + } + } + + // Handle cover art download + if (_autoFillFields.contains('cover')) { + final coverUrl = + (best['cover_url'] ?? best['images'] ?? '').toString(); + if (coverUrl.isNotEmpty) { + try { + final tempDir = await Directory.systemTemp.createTemp( + 'autofill_cover_', + ); + final coverOutput = + '${tempDir.path}${Platform.pathSeparator}cover.jpg'; + final response = await HttpClient() + .getUrl(Uri.parse(coverUrl)) + .then((req) => req.close()); + final file = File(coverOutput); + final sink = file.openWrite(); + await response.pipe(sink); + if (await file.exists() && await file.length() > 0) { + await _cleanupSelectedCoverTemp(); + if (mounted) { + setState(() { + _selectedCoverPath = coverOutput; + _selectedCoverTempDir = tempDir.path; + _selectedCoverName = 'Online cover'; + }); + filledCount++; + } + } else { + try { + await tempDir.delete(recursive: true); + } catch (_) {} + } + } catch (_) { + // Cover download is best-effort + } + } + } + + if (mounted) { + setState(() {}); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + filledCount > 0 + ? context.l10n.editMetadataAutoFillDone(filledCount) + : context.l10n.editMetadataAutoFillNoResults, + ), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(context.l10n.snackbarError(e.toString())), + ), + ); + } + } finally { + if (mounted) setState(() => _fetching = false); + } + } + @override void initState() { super.initState(); @@ -4416,6 +4717,7 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { children: [ const SizedBox(height: 6), _buildCoverEditor(cs), + _buildAutoFillSection(cs), _field('Title', _titleCtrl), _field('Artist', _artistCtrl), _field('Album', _albumCtrl), @@ -4487,6 +4789,183 @@ class _EditMetadataSheetState extends State<_EditMetadataSheet> { ); } + Widget _buildAutoFillSection(ColorScheme cs) { + return Padding( + padding: const EdgeInsets.only(bottom: 12), + child: Container( + decoration: BoxDecoration( + color: cs.surfaceContainerHighest.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.5)), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + InkWell( + onTap: () => setState(() => _showAutoFill = !_showAutoFill), + borderRadius: BorderRadius.circular(12), + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 10, + ), + child: Row( + children: [ + Icon( + Icons.travel_explore, + size: 20, + color: cs.primary, + ), + const SizedBox(width: 8), + Expanded( + child: Text( + context.l10n.editMetadataAutoFill, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: cs.onSurface, + fontWeight: FontWeight.w600, + ), + ), + ), + Icon( + _showAutoFill ? Icons.expand_less : Icons.expand_more, + size: 20, + color: cs.onSurfaceVariant, + ), + ], + ), + ), + ), + if (_showAutoFill) ...[ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Text( + context.l10n.editMetadataAutoFillDesc, + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: cs.onSurfaceVariant, + ), + ), + ), + const SizedBox(height: 8), + // Quick select buttons + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + children: [ + _quickSelectButton( + label: context.l10n.editMetadataSelectAll, + onTap: _selectAllFields, + cs: cs, + ), + const SizedBox(width: 8), + _quickSelectButton( + label: context.l10n.editMetadataSelectEmpty, + onTap: _selectEmptyFields, + cs: cs, + ), + ], + ), + ), + const SizedBox(height: 8), + // Field chips + Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Wrap( + spacing: 6, + runSpacing: 4, + children: _fieldDefs.keys.map((key) { + final selected = _autoFillFields.contains(key); + return FilterChip( + label: Text(_fieldLabel(key)), + selected: selected, + onSelected: _fetching + ? null + : (val) { + setState(() { + if (val) { + _autoFillFields.add(key); + } else { + _autoFillFields.remove(key); + } + }); + }, + selectedColor: cs.primaryContainer, + checkmarkColor: cs.onPrimaryContainer, + labelStyle: Theme.of(context).textTheme.labelSmall + ?.copyWith( + color: selected + ? cs.onPrimaryContainer + : cs.onSurfaceVariant, + ), + visualDensity: VisualDensity.compact, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + ); + }).toList(), + ), + ), + const SizedBox(height: 10), + // Fetch button + Padding( + padding: const EdgeInsets.only( + left: 12, + right: 12, + bottom: 12, + ), + child: SizedBox( + width: double.infinity, + child: FilledButton.icon( + onPressed: + (_fetching || _saving || _autoFillFields.isEmpty) + ? null + : _fetchAndFill, + icon: _fetching + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: Colors.white, + ), + ) + : const Icon(Icons.auto_fix_high), + label: Text( + _fetching + ? context.l10n.editMetadataAutoFillSearching + : context.l10n.editMetadataAutoFillFetch, + ), + ), + ), + ), + ], + ], + ), + ), + ); + } + + Widget _quickSelectButton({ + required String label, + required VoidCallback onTap, + required ColorScheme cs, + }) { + return InkWell( + onTap: _fetching ? null : onTap, + borderRadius: BorderRadius.circular(16), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all(color: cs.outline.withValues(alpha: 0.5)), + ), + child: Text( + label, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: cs.primary, + ), + ), + ), + ); + } + Widget _buildCoverEditor(ColorScheme cs) { final hasSelectedCover = _hasValue(_selectedCoverPath); final hasCurrentCover = _hasValue(_currentCoverPath);