feat(metadata): review batch tag enrichment #511

This commit is contained in:
zarzet
2026-08-11 16:40:06 +07:00
parent 28538395c2
commit bec9e33c94
24 changed files with 1698 additions and 265 deletions
+78
View File
@@ -4270,6 +4270,84 @@ abstract class AppLocalizations {
/// **'Select All'**
String get trackReEnrichSelectAll;
/// Batch metadata mode that only writes ISRC
///
/// In en, this message translates to:
/// **'ISRC only'**
String get trackReEnrichModeIsrc;
/// Explanation for the ISRC-only batch metadata mode
///
/// In en, this message translates to:
/// **'Find and add the recording identifier without changing other tags'**
String get trackReEnrichModeIsrcSubtitle;
/// Batch metadata mode that fills only empty tags
///
/// In en, this message translates to:
/// **'Fill missing tags'**
String get trackReEnrichModeMissing;
/// Explanation for the fill-missing batch metadata mode
///
/// In en, this message translates to:
/// **'Keep existing values and fill only fields that are empty'**
String get trackReEnrichModeMissingSubtitle;
/// Batch metadata mode that replaces selected tag groups
///
/// In en, this message translates to:
/// **'Update selected tags'**
String get trackReEnrichModeReplace;
/// Explanation for the selected-tag batch metadata mode
///
/// In en, this message translates to:
/// **'Choose which existing values may be replaced by online metadata'**
String get trackReEnrichModeReplaceSubtitle;
/// Heading above batch re-enrich field checkboxes
///
/// In en, this message translates to:
/// **'Tags to update'**
String get trackReEnrichFieldsTitle;
/// Button that searches metadata and opens the batch change review
///
/// In en, this message translates to:
/// **'Review changes'**
String get trackReEnrichReview;
/// Title of the batch metadata review sheet
///
/// In en, this message translates to:
/// **'Review metadata changes'**
String get trackReEnrichReviewTitle;
/// Summary shown above proposed batch metadata changes
///
/// In en, this message translates to:
/// **'{changeCount} proposed changes across {trackCount} tracks'**
String trackReEnrichReviewSubtitle(int changeCount, int trackCount);
/// Message when batch metadata preview has no proposed changes
///
/// In en, this message translates to:
/// **'No metadata changes were found for the selected tracks.'**
String get trackReEnrichNoChanges;
/// Confirmation button in the batch metadata review sheet
///
/// In en, this message translates to:
/// **'Apply changes'**
String get trackReEnrichApplyChanges;
/// Proposed value when lyrics will be refreshed during re-enrich
///
/// In en, this message translates to:
/// **'Refresh from online'**
String get trackReEnrichRefreshOnline;
/// Menu action - edit embedded metadata
///
/// In en, this message translates to:
+45
View File
@@ -2431,6 +2431,51 @@ class AppLocalizationsDe extends AppLocalizations {
@override
String get trackReEnrichSelectAll => 'Alles Auswählen';
@override
String get trackReEnrichModeIsrc => 'ISRC only';
@override
String get trackReEnrichModeIsrcSubtitle =>
'Find and add the recording identifier without changing other tags';
@override
String get trackReEnrichModeMissing => 'Fill missing tags';
@override
String get trackReEnrichModeMissingSubtitle =>
'Keep existing values and fill only fields that are empty';
@override
String get trackReEnrichModeReplace => 'Update selected tags';
@override
String get trackReEnrichModeReplaceSubtitle =>
'Choose which existing values may be replaced by online metadata';
@override
String get trackReEnrichFieldsTitle => 'Tags to update';
@override
String get trackReEnrichReview => 'Review changes';
@override
String get trackReEnrichReviewTitle => 'Review metadata changes';
@override
String trackReEnrichReviewSubtitle(int changeCount, int trackCount) {
return '$changeCount proposed changes across $trackCount tracks';
}
@override
String get trackReEnrichNoChanges =>
'No metadata changes were found for the selected tracks.';
@override
String get trackReEnrichApplyChanges => 'Apply changes';
@override
String get trackReEnrichRefreshOnline => 'Refresh from online';
@override
String get trackEditMetadata => 'Metadaten bearbeiten';
+45
View File
@@ -2403,6 +2403,51 @@ class AppLocalizationsEn extends AppLocalizations {
@override
String get trackReEnrichSelectAll => 'Select All';
@override
String get trackReEnrichModeIsrc => 'ISRC only';
@override
String get trackReEnrichModeIsrcSubtitle =>
'Find and add the recording identifier without changing other tags';
@override
String get trackReEnrichModeMissing => 'Fill missing tags';
@override
String get trackReEnrichModeMissingSubtitle =>
'Keep existing values and fill only fields that are empty';
@override
String get trackReEnrichModeReplace => 'Update selected tags';
@override
String get trackReEnrichModeReplaceSubtitle =>
'Choose which existing values may be replaced by online metadata';
@override
String get trackReEnrichFieldsTitle => 'Tags to update';
@override
String get trackReEnrichReview => 'Review changes';
@override
String get trackReEnrichReviewTitle => 'Review metadata changes';
@override
String trackReEnrichReviewSubtitle(int changeCount, int trackCount) {
return '$changeCount proposed changes across $trackCount tracks';
}
@override
String get trackReEnrichNoChanges =>
'No metadata changes were found for the selected tracks.';
@override
String get trackReEnrichApplyChanges => 'Apply changes';
@override
String get trackReEnrichRefreshOnline => 'Refresh from online';
@override
String get trackEditMetadata => 'Edit Metadata';
+45
View File
@@ -2403,6 +2403,51 @@ class AppLocalizationsEs extends AppLocalizations {
@override
String get trackReEnrichSelectAll => 'Select All';
@override
String get trackReEnrichModeIsrc => 'ISRC only';
@override
String get trackReEnrichModeIsrcSubtitle =>
'Find and add the recording identifier without changing other tags';
@override
String get trackReEnrichModeMissing => 'Fill missing tags';
@override
String get trackReEnrichModeMissingSubtitle =>
'Keep existing values and fill only fields that are empty';
@override
String get trackReEnrichModeReplace => 'Update selected tags';
@override
String get trackReEnrichModeReplaceSubtitle =>
'Choose which existing values may be replaced by online metadata';
@override
String get trackReEnrichFieldsTitle => 'Tags to update';
@override
String get trackReEnrichReview => 'Review changes';
@override
String get trackReEnrichReviewTitle => 'Review metadata changes';
@override
String trackReEnrichReviewSubtitle(int changeCount, int trackCount) {
return '$changeCount proposed changes across $trackCount tracks';
}
@override
String get trackReEnrichNoChanges =>
'No metadata changes were found for the selected tracks.';
@override
String get trackReEnrichApplyChanges => 'Apply changes';
@override
String get trackReEnrichRefreshOnline => 'Refresh from online';
@override
String get trackEditMetadata => 'Edit Metadata';
+45
View File
@@ -2461,6 +2461,51 @@ class AppLocalizationsFr extends AppLocalizations {
@override
String get trackReEnrichSelectAll => 'Tout sélectionner';
@override
String get trackReEnrichModeIsrc => 'ISRC only';
@override
String get trackReEnrichModeIsrcSubtitle =>
'Find and add the recording identifier without changing other tags';
@override
String get trackReEnrichModeMissing => 'Fill missing tags';
@override
String get trackReEnrichModeMissingSubtitle =>
'Keep existing values and fill only fields that are empty';
@override
String get trackReEnrichModeReplace => 'Update selected tags';
@override
String get trackReEnrichModeReplaceSubtitle =>
'Choose which existing values may be replaced by online metadata';
@override
String get trackReEnrichFieldsTitle => 'Tags to update';
@override
String get trackReEnrichReview => 'Review changes';
@override
String get trackReEnrichReviewTitle => 'Review metadata changes';
@override
String trackReEnrichReviewSubtitle(int changeCount, int trackCount) {
return '$changeCount proposed changes across $trackCount tracks';
}
@override
String get trackReEnrichNoChanges =>
'No metadata changes were found for the selected tracks.';
@override
String get trackReEnrichApplyChanges => 'Apply changes';
@override
String get trackReEnrichRefreshOnline => 'Refresh from online';
@override
String get trackEditMetadata => 'Modifier les métadonnées';
+46 -1
View File
@@ -2385,7 +2385,7 @@ class AppLocalizationsId extends AppLocalizations {
String get trackSaveLyricsProgress => 'Saving lyrics...';
@override
String get trackReEnrich => 'Re-enrich';
String get trackReEnrich => 'Perkaya ulang';
@override
String get trackReEnrichOnlineSubtitle =>
@@ -2412,6 +2412,51 @@ class AppLocalizationsId extends AppLocalizations {
@override
String get trackReEnrichSelectAll => 'Select All';
@override
String get trackReEnrichModeIsrc => 'ISRC saja';
@override
String get trackReEnrichModeIsrcSubtitle =>
'Cari dan tambahkan pengenal rekaman tanpa mengubah tag lain';
@override
String get trackReEnrichModeMissing => 'Isi tag yang kosong';
@override
String get trackReEnrichModeMissingSubtitle =>
'Pertahankan nilai yang ada dan isi hanya kolom yang kosong';
@override
String get trackReEnrichModeReplace => 'Perbarui tag yang dipilih';
@override
String get trackReEnrichModeReplaceSubtitle =>
'Pilih nilai yang boleh diganti oleh metadata online';
@override
String get trackReEnrichFieldsTitle => 'Tag yang diperbarui';
@override
String get trackReEnrichReview => 'Tinjau perubahan';
@override
String get trackReEnrichReviewTitle => 'Tinjau perubahan metadata';
@override
String trackReEnrichReviewSubtitle(int changeCount, int trackCount) {
return '$changeCount perubahan diusulkan untuk $trackCount trek';
}
@override
String get trackReEnrichNoChanges =>
'Tidak ada perubahan metadata yang ditemukan untuk trek yang dipilih.';
@override
String get trackReEnrichApplyChanges => 'Terapkan perubahan';
@override
String get trackReEnrichRefreshOnline => 'Perbarui dari online';
@override
String get trackEditMetadata => 'Edit Metadata';
+45
View File
@@ -2392,6 +2392,51 @@ class AppLocalizationsJa extends AppLocalizations {
@override
String get trackReEnrichSelectAll => 'Select All';
@override
String get trackReEnrichModeIsrc => 'ISRC only';
@override
String get trackReEnrichModeIsrcSubtitle =>
'Find and add the recording identifier without changing other tags';
@override
String get trackReEnrichModeMissing => 'Fill missing tags';
@override
String get trackReEnrichModeMissingSubtitle =>
'Keep existing values and fill only fields that are empty';
@override
String get trackReEnrichModeReplace => 'Update selected tags';
@override
String get trackReEnrichModeReplaceSubtitle =>
'Choose which existing values may be replaced by online metadata';
@override
String get trackReEnrichFieldsTitle => 'Tags to update';
@override
String get trackReEnrichReview => 'Review changes';
@override
String get trackReEnrichReviewTitle => 'Review metadata changes';
@override
String trackReEnrichReviewSubtitle(int changeCount, int trackCount) {
return '$changeCount proposed changes across $trackCount tracks';
}
@override
String get trackReEnrichNoChanges =>
'No metadata changes were found for the selected tracks.';
@override
String get trackReEnrichApplyChanges => 'Apply changes';
@override
String get trackReEnrichRefreshOnline => 'Refresh from online';
@override
String get trackEditMetadata => 'メタデータを編集';
+45
View File
@@ -2340,6 +2340,51 @@ class AppLocalizationsKo extends AppLocalizations {
@override
String get trackReEnrichSelectAll => '모두 선택';
@override
String get trackReEnrichModeIsrc => 'ISRC only';
@override
String get trackReEnrichModeIsrcSubtitle =>
'Find and add the recording identifier without changing other tags';
@override
String get trackReEnrichModeMissing => 'Fill missing tags';
@override
String get trackReEnrichModeMissingSubtitle =>
'Keep existing values and fill only fields that are empty';
@override
String get trackReEnrichModeReplace => 'Update selected tags';
@override
String get trackReEnrichModeReplaceSubtitle =>
'Choose which existing values may be replaced by online metadata';
@override
String get trackReEnrichFieldsTitle => 'Tags to update';
@override
String get trackReEnrichReview => 'Review changes';
@override
String get trackReEnrichReviewTitle => 'Review metadata changes';
@override
String trackReEnrichReviewSubtitle(int changeCount, int trackCount) {
return '$changeCount proposed changes across $trackCount tracks';
}
@override
String get trackReEnrichNoChanges =>
'No metadata changes were found for the selected tracks.';
@override
String get trackReEnrichApplyChanges => 'Apply changes';
@override
String get trackReEnrichRefreshOnline => 'Refresh from online';
@override
String get trackEditMetadata => '메타데이터 편집';
+45
View File
@@ -2403,6 +2403,51 @@ class AppLocalizationsPt extends AppLocalizations {
@override
String get trackReEnrichSelectAll => 'Select All';
@override
String get trackReEnrichModeIsrc => 'ISRC only';
@override
String get trackReEnrichModeIsrcSubtitle =>
'Find and add the recording identifier without changing other tags';
@override
String get trackReEnrichModeMissing => 'Fill missing tags';
@override
String get trackReEnrichModeMissingSubtitle =>
'Keep existing values and fill only fields that are empty';
@override
String get trackReEnrichModeReplace => 'Update selected tags';
@override
String get trackReEnrichModeReplaceSubtitle =>
'Choose which existing values may be replaced by online metadata';
@override
String get trackReEnrichFieldsTitle => 'Tags to update';
@override
String get trackReEnrichReview => 'Review changes';
@override
String get trackReEnrichReviewTitle => 'Review metadata changes';
@override
String trackReEnrichReviewSubtitle(int changeCount, int trackCount) {
return '$changeCount proposed changes across $trackCount tracks';
}
@override
String get trackReEnrichNoChanges =>
'No metadata changes were found for the selected tracks.';
@override
String get trackReEnrichApplyChanges => 'Apply changes';
@override
String get trackReEnrichRefreshOnline => 'Refresh from online';
@override
String get trackEditMetadata => 'Edit Metadata';
+45
View File
@@ -2432,6 +2432,51 @@ class AppLocalizationsRu extends AppLocalizations {
@override
String get trackReEnrichSelectAll => 'Выбрать всё';
@override
String get trackReEnrichModeIsrc => 'ISRC only';
@override
String get trackReEnrichModeIsrcSubtitle =>
'Find and add the recording identifier without changing other tags';
@override
String get trackReEnrichModeMissing => 'Fill missing tags';
@override
String get trackReEnrichModeMissingSubtitle =>
'Keep existing values and fill only fields that are empty';
@override
String get trackReEnrichModeReplace => 'Update selected tags';
@override
String get trackReEnrichModeReplaceSubtitle =>
'Choose which existing values may be replaced by online metadata';
@override
String get trackReEnrichFieldsTitle => 'Tags to update';
@override
String get trackReEnrichReview => 'Review changes';
@override
String get trackReEnrichReviewTitle => 'Review metadata changes';
@override
String trackReEnrichReviewSubtitle(int changeCount, int trackCount) {
return '$changeCount proposed changes across $trackCount tracks';
}
@override
String get trackReEnrichNoChanges =>
'No metadata changes were found for the selected tracks.';
@override
String get trackReEnrichApplyChanges => 'Apply changes';
@override
String get trackReEnrichRefreshOnline => 'Refresh from online';
@override
String get trackEditMetadata => 'Редактировать метаданные';
+45
View File
@@ -2429,6 +2429,51 @@ class AppLocalizationsTr extends AppLocalizations {
@override
String get trackReEnrichSelectAll => 'Tümünü Seç';
@override
String get trackReEnrichModeIsrc => 'ISRC only';
@override
String get trackReEnrichModeIsrcSubtitle =>
'Find and add the recording identifier without changing other tags';
@override
String get trackReEnrichModeMissing => 'Fill missing tags';
@override
String get trackReEnrichModeMissingSubtitle =>
'Keep existing values and fill only fields that are empty';
@override
String get trackReEnrichModeReplace => 'Update selected tags';
@override
String get trackReEnrichModeReplaceSubtitle =>
'Choose which existing values may be replaced by online metadata';
@override
String get trackReEnrichFieldsTitle => 'Tags to update';
@override
String get trackReEnrichReview => 'Review changes';
@override
String get trackReEnrichReviewTitle => 'Review metadata changes';
@override
String trackReEnrichReviewSubtitle(int changeCount, int trackCount) {
return '$changeCount proposed changes across $trackCount tracks';
}
@override
String get trackReEnrichNoChanges =>
'No metadata changes were found for the selected tracks.';
@override
String get trackReEnrichApplyChanges => 'Apply changes';
@override
String get trackReEnrichRefreshOnline => 'Refresh from online';
@override
String get trackEditMetadata => 'Edit Metadata';
+45
View File
@@ -2436,6 +2436,51 @@ class AppLocalizationsUk extends AppLocalizations {
@override
String get trackReEnrichSelectAll => 'Вибрати все';
@override
String get trackReEnrichModeIsrc => 'ISRC only';
@override
String get trackReEnrichModeIsrcSubtitle =>
'Find and add the recording identifier without changing other tags';
@override
String get trackReEnrichModeMissing => 'Fill missing tags';
@override
String get trackReEnrichModeMissingSubtitle =>
'Keep existing values and fill only fields that are empty';
@override
String get trackReEnrichModeReplace => 'Update selected tags';
@override
String get trackReEnrichModeReplaceSubtitle =>
'Choose which existing values may be replaced by online metadata';
@override
String get trackReEnrichFieldsTitle => 'Tags to update';
@override
String get trackReEnrichReview => 'Review changes';
@override
String get trackReEnrichReviewTitle => 'Review metadata changes';
@override
String trackReEnrichReviewSubtitle(int changeCount, int trackCount) {
return '$changeCount proposed changes across $trackCount tracks';
}
@override
String get trackReEnrichNoChanges =>
'No metadata changes were found for the selected tracks.';
@override
String get trackReEnrichApplyChanges => 'Apply changes';
@override
String get trackReEnrichRefreshOnline => 'Refresh from online';
@override
String get trackEditMetadata => 'Редагувати метадані';
+56
View File
@@ -3161,6 +3161,62 @@
"@trackReEnrichSelectAll": {
"description": "Select all fields checkbox in re-enrich"
},
"trackReEnrichModeIsrc": "ISRC only",
"@trackReEnrichModeIsrc": {
"description": "Batch metadata mode that only writes ISRC"
},
"trackReEnrichModeIsrcSubtitle": "Find and add the recording identifier without changing other tags",
"@trackReEnrichModeIsrcSubtitle": {
"description": "Explanation for the ISRC-only batch metadata mode"
},
"trackReEnrichModeMissing": "Fill missing tags",
"@trackReEnrichModeMissing": {
"description": "Batch metadata mode that fills only empty tags"
},
"trackReEnrichModeMissingSubtitle": "Keep existing values and fill only fields that are empty",
"@trackReEnrichModeMissingSubtitle": {
"description": "Explanation for the fill-missing batch metadata mode"
},
"trackReEnrichModeReplace": "Update selected tags",
"@trackReEnrichModeReplace": {
"description": "Batch metadata mode that replaces selected tag groups"
},
"trackReEnrichModeReplaceSubtitle": "Choose which existing values may be replaced by online metadata",
"@trackReEnrichModeReplaceSubtitle": {
"description": "Explanation for the selected-tag batch metadata mode"
},
"trackReEnrichFieldsTitle": "Tags to update",
"@trackReEnrichFieldsTitle": {
"description": "Heading above batch re-enrich field checkboxes"
},
"trackReEnrichReview": "Review changes",
"@trackReEnrichReview": {
"description": "Button that searches metadata and opens the batch change review"
},
"trackReEnrichReviewTitle": "Review metadata changes",
"@trackReEnrichReviewTitle": {
"description": "Title of the batch metadata review sheet"
},
"trackReEnrichReviewSubtitle": "{changeCount} proposed changes across {trackCount} tracks",
"@trackReEnrichReviewSubtitle": {
"description": "Summary shown above proposed batch metadata changes",
"placeholders": {
"changeCount": {"type": "int"},
"trackCount": {"type": "int"}
}
},
"trackReEnrichNoChanges": "No metadata changes were found for the selected tracks.",
"@trackReEnrichNoChanges": {
"description": "Message when batch metadata preview has no proposed changes"
},
"trackReEnrichApplyChanges": "Apply changes",
"@trackReEnrichApplyChanges": {
"description": "Confirmation button in the batch metadata review sheet"
},
"trackReEnrichRefreshOnline": "Refresh from online",
"@trackReEnrichRefreshOnline": {
"description": "Proposed value when lyrics will be refreshed during re-enrich"
},
"trackEditMetadata": "Edit Metadata",
"@trackEditMetadata": {
"description": "Menu action - edit embedded metadata"
+20 -1
View File
@@ -717,7 +717,26 @@
}
}
},
"trackReEnrich": "Re-enrich",
"trackReEnrich": "Perkaya ulang",
"trackReEnrichModeIsrc": "ISRC saja",
"trackReEnrichModeIsrcSubtitle": "Cari dan tambahkan pengenal rekaman tanpa mengubah tag lain",
"trackReEnrichModeMissing": "Isi tag yang kosong",
"trackReEnrichModeMissingSubtitle": "Pertahankan nilai yang ada dan isi hanya kolom yang kosong",
"trackReEnrichModeReplace": "Perbarui tag yang dipilih",
"trackReEnrichModeReplaceSubtitle": "Pilih nilai yang boleh diganti oleh metadata online",
"trackReEnrichFieldsTitle": "Tag yang diperbarui",
"trackReEnrichReview": "Tinjau perubahan",
"trackReEnrichReviewTitle": "Tinjau perubahan metadata",
"trackReEnrichReviewSubtitle": "{changeCount} perubahan diusulkan untuk {trackCount} trek",
"@trackReEnrichReviewSubtitle": {
"placeholders": {
"changeCount": {"type": "int"},
"trackCount": {"type": "int"}
}
},
"trackReEnrichNoChanges": "Tidak ada perubahan metadata yang ditemukan untuk trek yang dipilih.",
"trackReEnrichApplyChanges": "Terapkan perubahan",
"trackReEnrichRefreshOnline": "Perbarui dari online",
"@trackReEnrich": {
"description": "Menu action - re-embed metadata into audio file"
},
+102 -50
View File
@@ -16,13 +16,14 @@ import 'package:spotiflac_android/utils/file_access.dart';
import 'package:spotiflac_android/utils/image_cache_utils.dart';
import 'package:spotiflac_android/utils/lyrics_metadata_helper.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/utils/re_enrich_release_policy.dart';
import 'package:spotiflac_android/services/library_database.dart';
import 'package:spotiflac_android/services/batch_track_actions.dart';
import 'package:spotiflac_android/services/batch_metadata_re_enrich.dart';
import 'package:spotiflac_android/models/unified_library_item.dart';
import 'package:spotiflac_android/services/local_track_redownload_service.dart';
import 'package:spotiflac_android/widgets/batch_progress_dialog.dart';
import 'package:spotiflac_android/widgets/re_enrich_field_dialog.dart';
import 'package:spotiflac_android/widgets/re_enrich_review_sheet.dart';
import 'package:spotiflac_android/services/platform_bridge.dart';
import 'package:spotiflac_android/providers/local_library_provider.dart';
import 'package:spotiflac_android/providers/playback_provider.dart';
@@ -433,39 +434,18 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
Future<bool> _reEnrichLocalTrack(
LocalLibraryItem item, {
List<String>? updateFields,
required List<String> updateFields,
required Map<String, dynamic> resolvedMetadata,
}) async {
final durationMs = (item.duration ?? 0) * 1000;
final settings = ref.read(settingsProvider);
final artistTagMode = settings.artistTagMode;
await ref.read(settingsProvider.notifier).syncLyricsSettingsToBackend();
final request = <String, dynamic>{
'file_path': item.filePath,
'cover_url': '',
'max_quality': true,
'embed_lyrics': settings.embedLyrics,
'lyrics_mode': settings.lyricsMode,
'artist_tag_mode': artistTagMode,
'spotify_id': '',
'track_name': item.trackName,
'artist_name': item.artistName,
'album_name': item.albumName,
'album_artist': item.albumArtist ?? '',
'track_number': item.trackNumber ?? 0,
'disc_number': item.discNumber ?? 0,
'release_date': item.releaseDate ?? '',
'isrc': item.isrc ?? '',
'genre': item.genre ?? '',
'label': '',
'copyright': '',
'duration_ms': durationMs,
'search_online': true,
'replace_release_metadata': allowsReleaseIdentityReplacement(
ReEnrichOperationScope.batch,
),
// ignore: use_null_aware_elements
if (updateFields != null) 'update_fields': updateFields,
};
final request = buildBatchReEnrichRequest(
item: item,
settings: settings,
updateFields: updateFields,
resolvedMetadata: resolvedMetadata,
);
final result = await PlatformBridge.reEnrichFile(request);
final method = result['method'] as String?;
@@ -481,7 +461,7 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
return applyFfmpegReEnrichResult(
item: item,
result: result,
artistTagMode: ref.read(settingsProvider).artistTagMode,
artistTagMode: artistTagMode,
);
}
return false;
@@ -653,12 +633,89 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
return;
}
final updateFields = selection.isAll ? null : selection.fields;
await ref.read(settingsProvider.notifier).syncLyricsSettingsToBackend();
if (!mounted) return;
final settings = ref.read(settingsProvider);
final previews = <BatchReEnrichPreview>[];
var cancelled = false;
BatchProgressDialog.show(
context: context,
title: context.l10n.trackReEnrichSearching,
total: selected.length,
icon: Icons.manage_search,
onCancel: () {
cancelled = true;
BatchProgressDialog.dismiss(context);
},
);
for (var i = 0; i < selected.length; i++) {
if (!mounted || cancelled) break;
final item = selected[i];
BatchProgressDialog.update(
current: i + 1,
detail: '${item.trackName} - ${item.artistName}',
);
final updateFields = selection.updateFieldsFor(item);
if (updateFields.isEmpty) continue;
try {
final result = await PlatformBridge.reEnrichFile(
buildBatchReEnrichRequest(
item: item,
settings: settings,
updateFields: updateFields,
previewOnly: true,
),
);
final rawMetadata = result['enriched_metadata'];
if (result['method'] != 'preview' || rawMetadata is! Map) continue;
final enrichedMetadata = rawMetadata.map(
(key, value) => MapEntry(key.toString(), value),
);
final changes = buildReEnrichMetadataChanges(
item,
enrichedMetadata,
updateFields,
);
if (changes.isEmpty) continue;
previews.add(
BatchReEnrichPreview(
item: item,
updateFields: updateFields,
enrichedMetadata: enrichedMetadata,
changes: changes,
),
);
} catch (_) {}
}
if (!mounted) return;
if (!cancelled) BatchProgressDialog.dismiss(context);
if (cancelled) {
setState(() => isSelectionMode = true);
return;
}
if (previews.isEmpty) {
setState(() => isSelectionMode = true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.trackReEnrichNoChanges)),
);
return;
}
final confirmed = await showReEnrichReviewSheet(
context,
previews: previews,
);
if (!confirmed || !mounted) {
if (mounted) setState(() => isSelectionMode = true);
return;
}
var successCount = 0;
final total = selected.length;
var cancelled = false;
final total = previews.length;
cancelled = false;
BatchProgressDialog.show(
context: context,
title: context.l10n.trackReEnrichProgress,
@@ -672,26 +729,24 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
for (var i = 0; i < total; i++) {
if (!mounted || cancelled) break;
final item = selected[i];
final preview = previews[i];
BatchProgressDialog.update(
current: i + 1,
detail: '${item.trackName} - ${item.artistName}',
detail: '${preview.item.trackName} - ${preview.item.artistName}',
);
try {
final ok = await _reEnrichLocalTrack(item, updateFields: updateFields);
if (ok) {
successCount++;
}
final ok = await _reEnrichLocalTrack(
preview.item,
updateFields: preview.updateFields,
resolvedMetadata: preview.enrichedMetadata,
);
if (ok) successCount++;
} catch (_) {}
}
if (!mounted) {
return;
}
if (!mounted) return;
if (!cancelled) BatchProgressDialog.dismiss(context);
final settings = ref.read(settingsProvider);
final localLibraryPath = settings.localLibraryPath.trim();
final iosBookmark = settings.localLibraryBookmark;
try {
@@ -716,9 +771,6 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
return;
}
if (!cancelled) {
BatchProgressDialog.dismiss(context);
}
ScaffoldMessenger.of(context).clearSnackBars();
final failedCount = total - successCount;
final summary = failedCount <= 0
+2 -1
View File
@@ -16,7 +16,6 @@ import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/utils/adaptive_layout.dart';
import 'package:spotiflac_android/utils/audio_quality_badge_policy.dart';
import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/utils/re_enrich_release_policy.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
import 'package:spotiflac_android/utils/ffmpeg_reenrich.dart';
import 'package:spotiflac_android/utils/file_access.dart';
@@ -36,11 +35,13 @@ import 'package:spotiflac_android/services/music_player_service.dart';
import 'package:spotiflac_android/services/library_database.dart';
import 'package:spotiflac_android/services/local_track_redownload_service.dart';
import 'package:spotiflac_android/services/batch_track_actions.dart';
import 'package:spotiflac_android/services/batch_metadata_re_enrich.dart';
import 'package:spotiflac_android/services/downloaded_embedded_cover_resolver.dart';
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
import 'package:spotiflac_android/screens/favorite_artists_screen.dart';
import 'package:spotiflac_android/screens/downloaded_album_screen.dart';
import 'package:spotiflac_android/widgets/re_enrich_field_dialog.dart';
import 'package:spotiflac_android/widgets/re_enrich_review_sheet.dart';
import 'package:spotiflac_android/widgets/batch_progress_dialog.dart';
import 'package:spotiflac_android/widgets/cached_cover_image.dart';
import 'package:cached_network_image/cached_network_image.dart';
+97 -49
View File
@@ -3,39 +3,18 @@ part of 'queue_tab.dart';
extension _QueueTabBatchActions on _QueueTabState {
Future<bool> _reEnrichQueueLocalTrack(
LocalLibraryItem item, {
List<String>? updateFields,
required List<String> updateFields,
required Map<String, dynamic> resolvedMetadata,
}) async {
final durationMs = (item.duration ?? 0) * 1000;
final settings = ref.read(settingsProvider);
final artistTagMode = settings.artistTagMode;
await ref.read(settingsProvider.notifier).syncLyricsSettingsToBackend();
final request = <String, dynamic>{
'file_path': item.filePath,
'cover_url': '',
'max_quality': true,
'embed_lyrics': settings.embedLyrics,
'lyrics_mode': settings.lyricsMode,
'artist_tag_mode': artistTagMode,
'spotify_id': '',
'track_name': item.trackName,
'artist_name': item.artistName,
'album_name': item.albumName,
'album_artist': item.albumArtist ?? '',
'track_number': item.trackNumber ?? 0,
'disc_number': item.discNumber ?? 0,
'release_date': item.releaseDate ?? '',
'isrc': item.isrc ?? '',
'genre': item.genre ?? '',
'label': '',
'copyright': '',
'duration_ms': durationMs,
'search_online': true,
'replace_release_metadata': allowsReleaseIdentityReplacement(
ReEnrichOperationScope.batch,
),
// ignore: use_null_aware_elements
if (updateFields != null) 'update_fields': updateFields,
};
final request = buildBatchReEnrichRequest(
item: item,
settings: settings,
updateFields: updateFields,
resolvedMetadata: resolvedMetadata,
);
final result = await PlatformBridge.reEnrichFile(request);
final method = result['method'] as String?;
@@ -232,12 +211,89 @@ extension _QueueTabBatchActions on _QueueTabState {
return;
}
final updateFields = selection.isAll ? null : selection.fields;
await ref.read(settingsProvider.notifier).syncLyricsSettingsToBackend();
if (!mounted) return;
final settings = ref.read(settingsProvider);
final previews = <BatchReEnrichPreview>[];
var cancelled = false;
BatchProgressDialog.show(
context: context,
title: context.l10n.trackReEnrichSearching,
total: selectedLocalItems.length,
icon: Icons.manage_search,
onCancel: () {
cancelled = true;
BatchProgressDialog.dismiss(context);
},
);
for (var i = 0; i < selectedLocalItems.length; i++) {
if (!mounted || cancelled) break;
final item = selectedLocalItems[i];
BatchProgressDialog.update(
current: i + 1,
detail: '${item.trackName} - ${item.artistName}',
);
final updateFields = selection.updateFieldsFor(item);
if (updateFields.isEmpty) continue;
try {
final result = await PlatformBridge.reEnrichFile(
buildBatchReEnrichRequest(
item: item,
settings: settings,
updateFields: updateFields,
previewOnly: true,
),
);
final rawMetadata = result['enriched_metadata'];
if (result['method'] != 'preview' || rawMetadata is! Map) continue;
final enrichedMetadata = rawMetadata.map(
(key, value) => MapEntry(key.toString(), value),
);
final changes = buildReEnrichMetadataChanges(
item,
enrichedMetadata,
updateFields,
);
if (changes.isEmpty) continue;
previews.add(
BatchReEnrichPreview(
item: item,
updateFields: updateFields,
enrichedMetadata: enrichedMetadata,
changes: changes,
),
);
} catch (_) {}
}
if (!mounted) return;
if (!cancelled) BatchProgressDialog.dismiss(context);
if (cancelled) {
_setState(() => _isSelectionMode = true);
return;
}
if (previews.isEmpty) {
_setState(() => _isSelectionMode = true);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(context.l10n.trackReEnrichNoChanges)),
);
return;
}
final confirmed = await showReEnrichReviewSheet(
context,
previews: previews,
);
if (!confirmed || !mounted) {
if (mounted) _setState(() => _isSelectionMode = true);
return;
}
var successCount = 0;
final total = selectedLocalItems.length;
var cancelled = false;
final total = previews.length;
cancelled = false;
BatchProgressDialog.show(
context: context,
title: context.l10n.trackReEnrichProgress,
@@ -251,29 +307,24 @@ extension _QueueTabBatchActions on _QueueTabState {
for (var i = 0; i < total; i++) {
if (!mounted || cancelled) break;
final item = selectedLocalItems[i];
final preview = previews[i];
BatchProgressDialog.update(
current: i + 1,
detail: '${item.trackName} - ${item.artistName}',
detail: '${preview.item.trackName} - ${preview.item.artistName}',
);
try {
final ok = await _reEnrichQueueLocalTrack(
item,
updateFields: updateFields,
preview.item,
updateFields: preview.updateFields,
resolvedMetadata: preview.enrichedMetadata,
);
if (ok) {
successCount++;
}
if (ok) successCount++;
} catch (_) {}
}
if (!mounted) {
return;
}
if (!mounted) return;
if (!cancelled) BatchProgressDialog.dismiss(context);
final settings = ref.read(settingsProvider);
final localLibraryPath = settings.localLibraryPath.trim();
final iosBookmark = settings.localLibraryBookmark;
try {
@@ -298,9 +349,6 @@ extension _QueueTabBatchActions on _QueueTabState {
return;
}
if (!cancelled) {
BatchProgressDialog.dismiss(context);
}
ScaffoldMessenger.of(context).clearSnackBars();
final failedCount = total - successCount;
final summary = failedCount <= 0
+194
View File
@@ -0,0 +1,194 @@
import 'package:spotiflac_android/models/settings.dart';
import 'package:spotiflac_android/services/library_database.dart';
/// Field group keys understood by the Go re-enrich backend.
class ReEnrichFields {
static const String cover = 'cover';
static const String lyrics = 'lyrics';
static const String basicTags = 'basic_tags';
static const String trackInfo = 'track_info';
static const String releaseInfo = 'release_info';
static const String extra = 'extra';
static const List<String> all = [
cover,
lyrics,
basicTags,
trackInfo,
releaseInfo,
extra,
];
}
enum ReEnrichBatchMode { isrcOnly, missingOnly, selectedFields }
class ReEnrichFieldSelection {
final ReEnrichBatchMode mode;
final List<String> fields;
const ReEnrichFieldSelection({required this.mode, this.fields = const []});
List<String> updateFieldsFor(LocalLibraryItem item) {
switch (mode) {
case ReEnrichBatchMode.isrcOnly:
return const ['isrc'];
case ReEnrichBatchMode.selectedFields:
return fields;
case ReEnrichBatchMode.missingOnly:
return missingReEnrichFields(item);
}
}
}
bool _missingText(String? value) {
final normalized = value?.trim().toLowerCase() ?? '';
return normalized.isEmpty ||
normalized == 'unknown' ||
normalized == 'unknown title' ||
normalized == 'unknown artist' ||
normalized == 'unknown album';
}
/// Returns granular tag keys, preventing a missing value from causing its
/// already-populated neighbors in the same backend group to be overwritten.
List<String> missingReEnrichFields(LocalLibraryItem item) {
final fields = <String>[];
if (_missingText(item.trackName)) fields.add('track_name');
if (_missingText(item.artistName)) fields.add('artist_name');
if (_missingText(item.albumName)) fields.add('album_name');
if (_missingText(item.albumArtist)) fields.add('album_artist');
if ((item.trackNumber ?? 0) <= 0) fields.add('track_number');
if ((item.totalTracks ?? 0) <= 0) fields.add('total_tracks');
if ((item.discNumber ?? 0) <= 0) fields.add('disc_number');
if ((item.totalDiscs ?? 0) <= 0) fields.add('total_discs');
if (_missingText(item.releaseDate)) fields.add('release_date');
if (_missingText(item.isrc)) fields.add('isrc');
if (_missingText(item.genre)) fields.add('genre');
if (_missingText(item.composer)) fields.add('composer');
if (_missingText(item.label)) fields.add('label');
if (_missingText(item.copyright)) fields.add('copyright');
if (_missingText(item.coverPath)) fields.add('cover');
return fields;
}
Map<String, dynamic> buildBatchReEnrichRequest({
required LocalLibraryItem item,
required AppSettings settings,
required List<String> updateFields,
bool previewOnly = false,
Map<String, dynamic>? resolvedMetadata,
}) {
final request = <String, dynamic>{
'file_path': item.filePath,
'cover_url': '',
'max_quality': true,
'embed_lyrics': settings.embedLyrics,
'lyrics_mode': settings.lyricsMode,
'artist_tag_mode': settings.artistTagMode,
'spotify_id': '',
'track_name': item.trackName,
'artist_name': item.artistName,
'album_name': item.albumName,
'album_artist': item.albumArtist ?? '',
'track_number': item.trackNumber ?? 0,
'total_tracks': item.totalTracks ?? 0,
'disc_number': item.discNumber ?? 0,
'total_discs': item.totalDiscs ?? 0,
'release_date': item.releaseDate ?? '',
'isrc': item.isrc ?? '',
'genre': item.genre ?? '',
'composer': item.composer ?? '',
'label': item.label ?? '',
'copyright': item.copyright ?? '',
'duration_ms': (item.duration ?? 0) * 1000,
'search_online': resolvedMetadata == null,
'replace_release_metadata': false,
'update_fields': updateFields,
if (previewOnly) 'preview_only': true,
};
if (resolvedMetadata != null) {
request.addAll(resolvedMetadata);
}
return request;
}
class ReEnrichMetadataChange {
final String field;
final String oldValue;
final String newValue;
const ReEnrichMetadataChange({
required this.field,
required this.oldValue,
required this.newValue,
});
}
class BatchReEnrichPreview {
final LocalLibraryItem item;
final List<String> updateFields;
final Map<String, dynamic> enrichedMetadata;
final List<ReEnrichMetadataChange> changes;
const BatchReEnrichPreview({
required this.item,
required this.updateFields,
required this.enrichedMetadata,
required this.changes,
});
}
String _displayValue(Object? value) {
if (value == null) return '';
if (value is num && value == 0) return '';
return value.toString().trim();
}
List<ReEnrichMetadataChange> buildReEnrichMetadataChanges(
LocalLibraryItem item,
Map<String, dynamic> enrichedMetadata,
List<String> updateFields,
) {
final current = <String, Object?>{
'track_name': item.trackName,
'artist_name': item.artistName,
'album_name': item.albumName,
'album_artist': item.albumArtist,
'track_number': item.trackNumber,
'total_tracks': item.totalTracks,
'disc_number': item.discNumber,
'total_discs': item.totalDiscs,
'release_date': item.releaseDate,
'isrc': item.isrc,
'genre': item.genre,
'composer': item.composer,
'label': item.label,
'copyright': item.copyright,
'cover_url': item.coverPath,
};
final changes = <ReEnrichMetadataChange>[];
for (final field in current.keys) {
if (!enrichedMetadata.containsKey(field)) continue;
final oldValue = _displayValue(current[field]);
final newValue = _displayValue(enrichedMetadata[field]);
if (newValue.isEmpty || oldValue == newValue) continue;
changes.add(
ReEnrichMetadataChange(
field: field,
oldValue: oldValue,
newValue: newValue,
),
);
}
if (updateFields.contains(ReEnrichFields.lyrics) ||
updateFields.contains('lyrics')) {
changes.add(
const ReEnrichMetadataChange(
field: 'lyrics',
oldValue: '',
newValue: '__refresh_online__',
),
);
}
return changes;
}
+117 -99
View File
@@ -1,43 +1,19 @@
import 'package:flutter/material.dart';
import '../l10n/app_localizations.dart';
/// Field group keys matching the Go backend `update_fields` values.
class ReEnrichFields {
static const String cover = 'cover';
static const String lyrics = 'lyrics';
static const String basicTags = 'basic_tags';
static const String trackInfo = 'track_info';
static const String releaseInfo = 'release_info';
static const String extra = 'extra';
static const List<String> all = [
cover,
lyrics,
basicTags,
trackInfo,
releaseInfo,
extra,
];
}
/// Result returned by the re-enrich field selection sheet.
class ReEnrichFieldSelection {
final List<String> fields;
const ReEnrichFieldSelection(this.fields);
/// True when every available field is selected (or update_fields can be omitted).
bool get isAll => fields.length == ReEnrichFields.all.length;
}
import 'package:spotiflac_android/services/batch_metadata_re_enrich.dart';
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
Future<ReEnrichFieldSelection?> showReEnrichFieldDialog(
BuildContext context, {
required int selectedCount,
}) {
return showModalBottomSheet<ReEnrichFieldSelection>(
return showAppBottomSheet<ReEnrichFieldSelection>(
context: context,
useRootNavigator: true,
showDragHandle: true,
isScrollControlled: true,
title: AppLocalizations.of(context).trackReEnrich,
subtitle: AppLocalizations.of(context).trackReEnrichOnlineSubtitle,
maxHeightFactor: 0.9,
builder: (ctx) => _ReEnrichFieldSheet(selectedCount: selectedCount),
);
}
@@ -52,6 +28,7 @@ class _ReEnrichFieldSheet extends StatefulWidget {
class _ReEnrichFieldSheetState extends State<_ReEnrichFieldSheet> {
final Set<String> _selected = Set<String>.from(ReEnrichFields.all);
ReEnrichBatchMode _mode = ReEnrichBatchMode.missingOnly;
bool get _allSelected => _selected.length == ReEnrichFields.all.length;
@@ -117,80 +94,121 @@ class _ReEnrichFieldSheetState extends State<_ReEnrichFieldSheet> {
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context);
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;
return SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 4),
child: Text(
l10n.trackReEnrich,
style: textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
top: false,
child: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 8),
child: Text(
l10n.downloadedAlbumSelectedCount(widget.selectedCount),
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 4),
child: Text(
l10n.trackReEnrichOnlineSubtitle,
style: textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
SettingsGroup(
children: [
ListTile(
leading: const Icon(Icons.fingerprint),
title: Text(l10n.trackReEnrichModeIsrc),
subtitle: Text(l10n.trackReEnrichModeIsrcSubtitle),
trailing: _mode == ReEnrichBatchMode.isrcOnly
? Icon(Icons.check, color: colorScheme.primary)
: null,
onTap: () =>
setState(() => _mode = ReEnrichBatchMode.isrcOnly),
),
const Divider(height: 1, indent: 56),
ListTile(
leading: const Icon(Icons.playlist_add_check),
title: Text(l10n.trackReEnrichModeMissing),
subtitle: Text(l10n.trackReEnrichModeMissingSubtitle),
trailing: _mode == ReEnrichBatchMode.missingOnly
? Icon(Icons.check, color: colorScheme.primary)
: null,
onTap: () =>
setState(() => _mode = ReEnrichBatchMode.missingOnly),
),
const Divider(height: 1, indent: 56),
ListTile(
leading: const Icon(Icons.tune),
title: Text(l10n.trackReEnrichModeReplace),
subtitle: Text(l10n.trackReEnrichModeReplaceSubtitle),
trailing: _mode == ReEnrichBatchMode.selectedFields
? Icon(Icons.check, color: colorScheme.primary)
: null,
onTap: () =>
setState(() => _mode = ReEnrichBatchMode.selectedFields),
),
],
),
if (_mode == ReEnrichBatchMode.selectedFields) ...[
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 24),
child: Text(
l10n.trackReEnrichFieldsTitle,
style: Theme.of(context).textTheme.titleSmall?.copyWith(
color: colorScheme.onSurfaceVariant,
fontWeight: FontWeight.w600,
),
),
),
SettingsGroup(
children: [
CheckboxListTile(
title: Text(
l10n.trackReEnrichSelectAll,
style: const TextStyle(fontWeight: FontWeight.w600),
),
value: _allSelected,
tristate: true,
onChanged: _toggleAll,
controlAffinity: ListTileControlAffinity.leading,
),
for (final field in ReEnrichFields.all) ...[
const Divider(height: 1, indent: 56),
CheckboxListTile(
secondary: Icon(_iconFor(field), size: 20),
title: Text(_labelFor(field, l10n)),
value: _selected.contains(field),
onChanged: (value) => _toggle(field, value),
controlAffinity: ListTileControlAffinity.leading,
),
],
],
),
],
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed:
_mode == ReEnrichBatchMode.selectedFields &&
_selected.isEmpty
? null
: () => Navigator.pop(
context,
ReEnrichFieldSelection(
mode: _mode,
fields: _selected.toList(),
),
),
icon: const Icon(Icons.preview_outlined, size: 18),
label: Text(l10n.trackReEnrichReview),
),
),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 8),
child: Text(
l10n.downloadedAlbumSelectedCount(widget.selectedCount),
style: textTheme.bodySmall?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
),
const Divider(height: 1),
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
title: Text(
l10n.trackReEnrichSelectAll,
style: const TextStyle(fontWeight: FontWeight.w600),
),
value: _allSelected,
tristate: true,
onChanged: _toggleAll,
controlAffinity: ListTileControlAffinity.leading,
),
const Divider(height: 1, indent: 16, endIndent: 16),
for (final field in ReEnrichFields.all)
CheckboxListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 16),
secondary: Icon(_iconFor(field), size: 20),
title: Text(_labelFor(field, l10n)),
value: _selected.contains(field),
onChanged: (v) => _toggle(field, v),
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 8),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: _selected.isEmpty
? null
: () => Navigator.pop(
context,
ReEnrichFieldSelection(_selected.toList()),
),
icon: const Icon(Icons.auto_fix_high, size: 18),
label: Text(l10n.trackReEnrich),
),
),
),
],
],
),
),
);
}
+193
View File
@@ -0,0 +1,193 @@
import 'package:flutter/material.dart';
import 'package:spotiflac_android/l10n/l10n.dart';
import 'package:spotiflac_android/services/batch_metadata_re_enrich.dart';
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
import 'package:spotiflac_android/widgets/settings_group.dart';
Future<bool> showReEnrichReviewSheet(
BuildContext context, {
required List<BatchReEnrichPreview> previews,
}) async {
final changeCount = previews.fold<int>(
0,
(total, preview) => total + preview.changes.length,
);
if (changeCount == 0) return false;
final confirmed = await showAppBottomSheet<bool>(
context: context,
useRootNavigator: true,
title: context.l10n.trackReEnrichReviewTitle,
subtitle: context.l10n.trackReEnrichReviewSubtitle(
changeCount,
previews.length,
),
maxHeightFactor: 0.9,
builder: (sheetContext) => _ReEnrichReviewContent(previews: previews),
);
return confirmed == true;
}
class _ReEnrichReviewContent extends StatelessWidget {
final List<BatchReEnrichPreview> previews;
const _ReEnrichReviewContent({required this.previews});
String _fieldLabel(BuildContext context, String field) {
switch (field) {
case 'track_name':
return context.l10n.editMetadataFieldTitle;
case 'artist_name':
return context.l10n.editMetadataFieldArtist;
case 'album_name':
return context.l10n.editMetadataFieldAlbum;
case 'album_artist':
return context.l10n.editMetadataFieldAlbumArtist;
case 'track_number':
return context.l10n.editMetadataFieldTrackNum;
case 'total_tracks':
return context.l10n.editMetadataFieldTrackTotal;
case 'disc_number':
return context.l10n.editMetadataFieldDiscNum;
case 'total_discs':
return context.l10n.editMetadataFieldDiscTotal;
case 'release_date':
return context.l10n.editMetadataFieldDate;
case 'isrc':
return context.l10n.editMetadataFieldIsrc;
case 'genre':
return context.l10n.editMetadataFieldGenre;
case 'composer':
return context.l10n.editMetadataFieldComposer;
case 'label':
return context.l10n.editMetadataFieldLabel;
case 'copyright':
return context.l10n.editMetadataFieldCopyright;
case 'cover_url':
return context.l10n.editMetadataFieldCover;
case 'lyrics':
return context.l10n.trackReEnrichFieldLyrics;
default:
return field;
}
}
String _oldValue(BuildContext context, ReEnrichMetadataChange change) {
if (change.field == 'cover_url' && change.oldValue.isNotEmpty) {
return context.l10n.trackCoverCurrent;
}
return change.oldValue.isEmpty ? '' : change.oldValue;
}
String _newValue(BuildContext context, ReEnrichMetadataChange change) {
if (change.field == 'cover_url') {
return context.l10n.editMetadataAutoFillCoverAvailable;
}
if (change.newValue == '__refresh_online__') {
return context.l10n.trackReEnrichRefreshOnline;
}
return change.newValue;
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final visible = previews
.where((preview) => preview.changes.isNotEmpty)
.toList(growable: false);
return SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: ListView.builder(
shrinkWrap: true,
padding: const EdgeInsets.only(bottom: 8),
itemCount: visible.length,
itemBuilder: (context, index) {
final preview = visible[index];
return SettingsGroup(
children: [
ListTile(
leading: const Icon(Icons.audio_file_outlined),
title: Text(
preview.item.trackName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
subtitle: Text(
preview.item.artistName,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
for (final change in preview.changes) ...[
const Divider(height: 1, indent: 56),
Semantics(
label:
'${_fieldLabel(context, change.field)}: ${_oldValue(context, change)}, ${_newValue(context, change)}',
child: Padding(
padding: const EdgeInsets.fromLTRB(56, 10, 16, 10),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_fieldLabel(context, change.field),
style: Theme.of(context).textTheme.labelMedium
?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
const SizedBox(height: 3),
Text.rich(
TextSpan(
children: [
TextSpan(
text: _oldValue(context, change),
style: TextStyle(
color: colorScheme.onSurfaceVariant,
),
),
TextSpan(
text: '',
style: TextStyle(
color: colorScheme.primary,
),
),
TextSpan(
text: _newValue(context, change),
style: const TextStyle(
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
],
],
);
},
),
),
Padding(
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
child: SizedBox(
width: double.infinity,
child: FilledButton.icon(
onPressed: () => Navigator.pop(context, true),
icon: const Icon(Icons.save_outlined),
label: Text(context.l10n.trackReEnrichApplyChanges),
),
),
),
],
),
);
}
}