feat: selective auto-fill from online in Edit Metadata sheet

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.
This commit is contained in:
zarzet
2026-03-15 20:35:42 +07:00
parent 42f0267277
commit 1665e4cd57
16 changed files with 1656 additions and 0 deletions
+126
View File
@@ -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
+74
View File
@@ -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';
}
+74
View File
@@ -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';
}
+74
View File
@@ -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`).
+74
View File
@@ -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';
}
+74
View File
@@ -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';
}
+74
View File
@@ -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';
}
+74
View File
@@ -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';
}
+74
View File
@@ -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';
}
+74
View File
@@ -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';
}
+74
View File
@@ -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`).
+74
View File
@@ -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';
}
+74
View File
@@ -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';
}
+74
View File
@@ -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`).
+89
View File
@@ -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"
}
}
+479
View File
@@ -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<String> _autoFillFields = {};
// All auto-fillable fields and their mapping
static const _fieldDefs = <String, String>{
'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<void> _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 = <String>[];
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<String, dynamic>? 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 = <String, String>{
'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<String, String>? 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);