mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-02 17:18:36 +02:00
303 lines
9.4 KiB
Dart
303 lines
9.4 KiB
Dart
part of 'track_metadata_screen.dart';
|
|
|
|
extension _TrackMetadataFileActions on _TrackMetadataScreenState {
|
|
void _showEditMetadataSheet(
|
|
BuildContext context,
|
|
WidgetRef ref,
|
|
ColorScheme colorScheme,
|
|
) async {
|
|
Map<String, dynamic>? fileMetadata;
|
|
try {
|
|
final result = await PlatformBridge.readFileMetadata(cleanFilePath);
|
|
if (result['error'] == null) {
|
|
fileMetadata = result;
|
|
}
|
|
} catch (e) {
|
|
debugPrint('readFileMetadata failed, using item data: $e');
|
|
}
|
|
|
|
String val(String key, String? fallback) {
|
|
final v = fileMetadata?[key]?.toString();
|
|
return (v != null && v.isNotEmpty) ? v : (fallback ?? '');
|
|
}
|
|
|
|
final initialValues = <String, String>{
|
|
'title': val('title', trackName),
|
|
'artist': val('artist', artistName),
|
|
'album': val('album', albumName),
|
|
'album_artist': val('album_artist', albumArtist),
|
|
'date': val('date', releaseDate),
|
|
'track_number': (fileMetadata?['track_number'] ?? trackNumber ?? '')
|
|
.toString(),
|
|
'total_tracks': (fileMetadata?['total_tracks'] ?? totalTracks ?? '')
|
|
.toString(),
|
|
'disc_number': (fileMetadata?['disc_number'] ?? discNumber ?? '')
|
|
.toString(),
|
|
'total_discs': (fileMetadata?['total_discs'] ?? totalDiscs ?? '')
|
|
.toString(),
|
|
'genre': val('genre', genre),
|
|
'isrc': val('isrc', isrc),
|
|
'label': val('label', label),
|
|
'copyright': val('copyright', copyright),
|
|
'composer': val('composer', composer),
|
|
'comment': fileMetadata?['comment']?.toString() ?? '',
|
|
'lyrics': fileMetadata?['lyrics']?.toString() ?? '',
|
|
};
|
|
|
|
final initialDurationSeconds =
|
|
readPositiveInt(fileMetadata?['duration']) ?? duration ?? 0;
|
|
|
|
if (!context.mounted) return;
|
|
|
|
final saved = await showModalBottomSheet<bool>(
|
|
context: context,
|
|
useRootNavigator: true,
|
|
isScrollControlled: true,
|
|
backgroundColor: colorScheme.surface,
|
|
builder: (sheetContext) => _EditMetadataSheet(
|
|
colorScheme: colorScheme,
|
|
initialValues: initialValues,
|
|
filePath: cleanFilePath,
|
|
sourceTrackId: _spotifyId,
|
|
durationMs: initialDurationSeconds > 0
|
|
? initialDurationSeconds * 1000
|
|
: 0,
|
|
artistTagMode: ref.read(settingsProvider).artistTagMode,
|
|
),
|
|
);
|
|
|
|
if (saved == true && mounted) {
|
|
ScaffoldMessenger.of(this.context).showSnackBar(
|
|
SnackBar(content: Text(this.context.l10n.snackbarMetadataSaved)),
|
|
);
|
|
try {
|
|
final refreshed = await PlatformBridge.readFileMetadata(cleanFilePath);
|
|
final refreshedLyrics = refreshed['lyrics']?.toString().trim() ?? '';
|
|
_setState(() {
|
|
_editedMetadata = refreshed;
|
|
_lyricsError = null;
|
|
_isInstrumental = false;
|
|
_embeddedLyricsChecked = true;
|
|
if (refreshedLyrics.isNotEmpty) {
|
|
_lyrics = _cleanLrcForDisplay(refreshedLyrics);
|
|
_rawLyrics = refreshedLyrics;
|
|
_lyricsSource = context.l10n.trackLyricsEmbeddedSource;
|
|
_lyricsEmbedded = true;
|
|
} else {
|
|
_lyrics = null;
|
|
_rawLyrics = null;
|
|
_lyricsSource = null;
|
|
_lyricsEmbedded = false;
|
|
}
|
|
});
|
|
} catch (_) {
|
|
_setState(() {});
|
|
}
|
|
await _refreshEmbeddedCoverPreview(force: true);
|
|
_markMetadataChanged();
|
|
await _syncDownloadHistoryMetadata();
|
|
}
|
|
}
|
|
|
|
void _confirmDelete(
|
|
BuildContext screenContext,
|
|
WidgetRef ref,
|
|
ColorScheme colorScheme,
|
|
) {
|
|
showDialog<void>(
|
|
context: screenContext,
|
|
useRootNavigator: true,
|
|
builder: (dialogContext) => AlertDialog(
|
|
title: Text(dialogContext.l10n.trackDeleteConfirmTitle),
|
|
content: Text(dialogContext.l10n.trackDeleteConfirmMessage),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(dialogContext),
|
|
child: Text(dialogContext.l10n.dialogCancel),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
var fileDeleted = true;
|
|
if (_isLocalItem) {
|
|
if (_isCueVirtualTrack && _localLibraryItem != null) {
|
|
await ref
|
|
.read(localLibraryProvider.notifier)
|
|
.removeItem(_localLibraryItem!.id);
|
|
} else {
|
|
fileDeleted = await deleteFile(cleanFilePath);
|
|
if (fileDeleted && _localLibraryItem != null) {
|
|
await ref
|
|
.read(localLibraryProvider.notifier)
|
|
.removeItem(_localLibraryItem!.id);
|
|
}
|
|
}
|
|
} else {
|
|
fileDeleted = await deleteFile(cleanFilePath);
|
|
if (fileDeleted) {
|
|
ref
|
|
.read(downloadHistoryProvider.notifier)
|
|
.removeFromHistory(_downloadItem!.id);
|
|
}
|
|
}
|
|
|
|
if (!fileDeleted) {
|
|
if (screenContext.mounted) {
|
|
ScaffoldMessenger.of(screenContext).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
screenContext.l10n.snackbarError(
|
|
screenContext.l10n.snackbarFailedToWriteStorage,
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (dialogContext.mounted) {
|
|
Navigator.pop(dialogContext);
|
|
}
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
final navigator = Navigator.of(context);
|
|
if (navigator.canPop()) {
|
|
navigator.pop(true);
|
|
}
|
|
});
|
|
},
|
|
child: Text(
|
|
dialogContext.l10n.dialogDelete,
|
|
style: TextStyle(color: colorScheme.error),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _closeOptionsMenuAndRun(BuildContext sheetContext, VoidCallback action) {
|
|
Navigator.pop(sheetContext);
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
if (!mounted) return;
|
|
action();
|
|
});
|
|
}
|
|
|
|
Future<void> _openFile(BuildContext context, String filePath) async {
|
|
if (isCueVirtualPath(filePath)) {
|
|
_showCueVirtualTrackSnackBar(context);
|
|
return;
|
|
}
|
|
try {
|
|
final playbackCover =
|
|
normalizeOptionalString(_localCoverPath) ??
|
|
normalizeOptionalString(_embeddedCoverPreviewPath) ??
|
|
normalizeOptionalString(_coverUrl) ??
|
|
'';
|
|
await ref
|
|
.read(playbackProvider.notifier)
|
|
.playLocalPath(
|
|
path: filePath,
|
|
title: trackName,
|
|
artist: artistName,
|
|
album: albumName,
|
|
coverUrl: playbackCover,
|
|
);
|
|
} catch (e) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
context.l10n.snackbarCannotOpenFile(context.friendlyError(e)),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
void _copyToClipboard(BuildContext context, String text) {
|
|
Clipboard.setData(ClipboardData(text: text));
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(context.l10n.trackCopiedToClipboard),
|
|
duration: const Duration(seconds: 2),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _shareFile(BuildContext context) async {
|
|
if (_isCueVirtualTrack) {
|
|
_showCueVirtualTrackSnackBar(context);
|
|
return;
|
|
}
|
|
|
|
String sharePath = cleanFilePath;
|
|
if (!await fileExists(sharePath)) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(content: Text(context.l10n.snackbarFileNotFound)),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
final shareTitle = '$trackName - $artistName';
|
|
|
|
// For SAF content URIs, use native share intent directly (zero-copy)
|
|
if (isContentUri(sharePath)) {
|
|
try {
|
|
await PlatformBridge.shareContentUri(sharePath, title: shareTitle);
|
|
} catch (_) {
|
|
if (context.mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
context.l10n.snackbarCannotOpenFile('Failed to share file'),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
await SharePlus.instance.share(
|
|
ShareParams(files: [XFile(sharePath)], text: shareTitle),
|
|
);
|
|
}
|
|
|
|
String _formatFullDate(DateTime date) {
|
|
return '${date.day} ${_TrackMetadataScreenState._months[date.month - 1]} ${date.year}, '
|
|
'${date.hour.toString().padLeft(2, '0')}:'
|
|
'${date.minute.toString().padLeft(2, '0')}';
|
|
}
|
|
|
|
IconData _getServiceIcon(String service) {
|
|
switch (service.toLowerCase()) {
|
|
case MusicServices.tidal:
|
|
return Icons.waves;
|
|
case MusicServices.qobuz:
|
|
return Icons.album;
|
|
case MusicServices.amazon:
|
|
return Icons.shopping_cart;
|
|
default:
|
|
return Icons.cloud_download;
|
|
}
|
|
}
|
|
|
|
Color _getServiceColor(String service, ColorScheme colorScheme) {
|
|
switch (service.toLowerCase()) {
|
|
case MusicServices.tidal:
|
|
return const Color(0xFF0077B5);
|
|
case MusicServices.qobuz:
|
|
return const Color(0xFF0052CC);
|
|
case MusicServices.amazon:
|
|
return const Color(0xFFFF9900);
|
|
default:
|
|
return colorScheme.primary;
|
|
}
|
|
}
|
|
}
|