fix(library): retain records when deletion fails

This commit is contained in:
zarzet
2026-07-27 14:24:39 +07:00
parent 73846c40ca
commit 1cdf4b4547
7 changed files with 94 additions and 40 deletions
+2 -3
View File
@@ -157,9 +157,8 @@ class _DownloadedAlbumScreenState extends ConsumerState<DownloadedAlbumScreen>
deleteItem: (id) async {
final item = tracksById[id];
if (item == null) return false;
try {
await deleteFile(item.filePath);
} catch (_) {}
final deleted = await deleteFile(item.filePath);
if (!deleted) return false;
historyNotifier.removeFromHistory(id);
return true;
},
+2 -3
View File
@@ -130,9 +130,8 @@ class _LocalAlbumScreenState extends ConsumerState<LocalAlbumScreen>
final item = tracksById[id];
if (item == null) return false;
if (!isCueVirtualPath(item.filePath)) {
try {
await deleteFile(item.filePath);
} catch (_) {}
final deleted = await deleteFile(item.filePath);
if (!deleted) return false;
}
await libraryNotifier.removeItem(id);
return true;
+3 -4
View File
@@ -476,10 +476,9 @@ extension _QueueTabSelectionActions on _QueueTabState {
for (final id in _selectedIds) {
final item = itemsById[id];
if (item != null) {
try {
final cleanPath = _cleanFilePath(item.filePath);
await deleteFile(cleanPath);
} catch (_) {}
final cleanPath = _cleanFilePath(item.filePath);
final fileDeleted = await deleteFile(cleanPath);
if (!fileDeleted) continue;
if (item.source == LibraryItemSource.downloaded) {
historyNotifier.removeFromHistory(item.historyItem!.id);
+22 -13
View File
@@ -120,33 +120,42 @@ extension _TrackMetadataFileActions on _TrackMetadataScreenState {
),
TextButton(
onPressed: () async {
var fileDeleted = true;
if (_isLocalItem) {
if (_isCueVirtualTrack && _localLibraryItem != null) {
await ref
.read(localLibraryProvider.notifier)
.removeItem(_localLibraryItem!.id);
} else {
try {
await deleteFile(cleanFilePath);
} catch (e) {
debugPrint('Failed to delete file: $e');
}
if (_localLibraryItem != null) {
fileDeleted = await deleteFile(cleanFilePath);
if (fileDeleted && _localLibraryItem != null) {
await ref
.read(localLibraryProvider.notifier)
.removeItem(_localLibraryItem!.id);
}
}
} else {
try {
await deleteFile(cleanFilePath);
} catch (e) {
debugPrint('Failed to delete file: $e');
fileDeleted = await deleteFile(cleanFilePath);
if (fileDeleted) {
ref
.read(downloadHistoryProvider.notifier)
.removeFromHistory(_downloadItem!.id);
}
}
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) {
+31 -9
View File
@@ -272,21 +272,43 @@ Future<bool> fileExists(String? path) async {
return File(realPath).exists();
}
Future<void> deleteFile(String? path) async {
if (path == null || path.isEmpty) return;
/// Deletes [path] and reports whether the file is confirmed absent afterward.
///
/// SAF providers are allowed to reject a delete request by returning `false`.
/// Callers that also remove a Library row must only do so when this returns
/// `true`, otherwise the app would hide a file that still exists on storage.
Future<bool> deleteFile(String? path) async {
if (path == null || path.isEmpty) return false;
// CUE virtual paths should NOT be deleted through this function —
// deleting album.cue would remove ALL tracks. Callers should handle
// CUE deletion specially (e.g. only delete when all tracks are removed).
if (isCueVirtualPath(path)) return;
if (isCueVirtualPath(path)) return false;
if (isContentUri(path)) {
await PlatformBridge.safDelete(path);
await musicPlayerHandler?.onSourceDeleted(path);
return;
try {
final deleted = await PlatformBridge.safDelete(path);
final confirmedAbsent = deleted || !await PlatformBridge.safExists(path);
if (confirmedAbsent) {
await musicPlayerHandler?.onSourceDeleted(path);
}
return confirmedAbsent;
} catch (_) {
return false;
}
}
final file = File(path);
try {
await File(path).delete();
} catch (_) {}
await musicPlayerHandler?.onSourceDeleted(path);
if (await file.exists()) {
await file.delete();
}
final confirmedAbsent = !await file.exists();
if (confirmedAbsent) {
await musicPlayerHandler?.onSourceDeleted(path);
}
return confirmedAbsent;
} catch (_) {
return false;
}
}
Future<FileAccessStat?> fileStat(String? path) async {
+10 -8
View File
@@ -74,7 +74,9 @@ class _DuplicateReviewSheetState extends ConsumerState<DuplicateReviewSheet> {
if (bitDepth > 0 && sampleRate > 0) {
final khz = sampleRate / 1000;
final khzText = khz % 1 == 0 ? khz.toInt().toString() : khz.toString();
return format.isEmpty ? '$bitDepth/$khzText' : '$bitDepth/$khzText $format';
return format.isEmpty
? '$bitDepth/$khzText'
: '$bitDepth/$khzText $format';
}
final bitrate = entry.bitrate ?? 0;
if (bitrate > 0) {
@@ -112,11 +114,10 @@ class _DuplicateReviewSheetState extends ConsumerState<DuplicateReviewSheet> {
final historyNotifier = ref.read(downloadHistoryProvider.notifier);
var deleted = 0;
for (final entry in entries) {
try {
await deleteFile(
DownloadedEmbeddedCoverResolver.cleanFilePath(entry.filePath),
);
} catch (_) {}
final fileDeleted = await deleteFile(
DownloadedEmbeddedCoverResolver.cleanFilePath(entry.filePath),
);
if (!fileDeleted) continue;
if (entry.source == 'downloaded') {
historyNotifier.removeFromHistory(entry.id);
} else {
@@ -200,8 +201,9 @@ class _DuplicateReviewSheetState extends ConsumerState<DuplicateReviewSheet> {
padding: const EdgeInsets.fromLTRB(24, 8, 24, 32),
child: Text(
context.l10n.duplicatesEmpty,
style: Theme.of(context).textTheme.bodyMedium
?.copyWith(color: colorScheme.onSurfaceVariant),
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: colorScheme.onSurfaceVariant,
),
),
);
}