Fix critical blocker issues and improve tests
## Critical Blockers Fixed - Remove playbackState.addError() which broke audio_service permanently - Move error handling from audio_handler to song_list where UI context exists - Display error in SnackBar instead of destroying playback stream - Add escapeChar parameter to LIKE queries for correct wildcard handling - Fixes broken search for titles with _ or % characters - Add dismissScanError() method to allow closing error banner - Previously banner couldn't be dismissed (missing notifyListeners()) ## Improvements - Remove dead livePaths list, use companions.isNotEmpty instead - Add initial progress update to show total files when scan starts - Add tooltip to now_playing_screen Zurück button - Add proper error handling in song_list with async/await ## Testing - Add regression test for CRITICAL-1: empty scan doesn't delete songs - Add regression test for CRITICAL-2: tombstoned files restore on re-scan - All 11 tests pass Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 4.5
parent
4fa8d12fa0
commit
d04783073f
@@ -34,11 +34,11 @@ Future<int> scanAndroidMediaStore(
|
||||
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final companions = <SongsCompanion>[];
|
||||
final livePaths = <String>[];
|
||||
var done = 0;
|
||||
|
||||
if (songs.isNotEmpty) onProgress?.call(0, songs.length);
|
||||
|
||||
for (final s in songs) {
|
||||
livePaths.add(s.data);
|
||||
final prev = existing[s.data];
|
||||
final id = prev?.id ?? _uuid.v4();
|
||||
|
||||
@@ -76,7 +76,7 @@ Future<int> scanAndroidMediaStore(
|
||||
}
|
||||
|
||||
await db.upsertSongs(companions);
|
||||
if (livePaths.isNotEmpty) {
|
||||
if (companions.isNotEmpty) {
|
||||
await db.markMissing(now);
|
||||
}
|
||||
return companions.length;
|
||||
|
||||
@@ -49,7 +49,7 @@ class LibraryScreen extends StatelessWidget {
|
||||
content: Text(lib.scanError!),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => lib.scanError = null,
|
||||
onPressed: lib.dismissScanError,
|
||||
child: const Text('Verwerfen'),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -57,6 +57,11 @@ class LibraryService extends ChangeNotifier {
|
||||
return granted;
|
||||
}
|
||||
|
||||
void dismissScanError() {
|
||||
scanError = null;
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> _scan() async {
|
||||
if (scanning) return;
|
||||
scanning = true;
|
||||
|
||||
@@ -45,11 +45,11 @@ Future<int> scanFolders(
|
||||
|
||||
final now = DateTime.now().millisecondsSinceEpoch;
|
||||
final companions = <SongsCompanion>[];
|
||||
final livePaths = <String>[];
|
||||
var done = 0;
|
||||
|
||||
if (files.isNotEmpty) onProgress?.call(0, files.length);
|
||||
|
||||
for (final file in files) {
|
||||
livePaths.add(file.path);
|
||||
final prev = existing[file.path];
|
||||
final id = prev?.id ?? _uuid.v4();
|
||||
|
||||
@@ -96,7 +96,7 @@ Future<int> scanFolders(
|
||||
}
|
||||
|
||||
await db.upsertSongs(companions);
|
||||
if (livePaths.isNotEmpty) {
|
||||
if (companions.isNotEmpty) {
|
||||
await db.markMissing(now);
|
||||
}
|
||||
return companions.length;
|
||||
|
||||
@@ -27,7 +27,17 @@ class SongList extends StatelessWidget {
|
||||
title: Text(s.title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: Text(s.artist ?? 'Unbekannt',
|
||||
maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
onTap: () => playSongs(handler, songs, i),
|
||||
onTap: () async {
|
||||
try {
|
||||
await playSongs(handler, songs, i);
|
||||
} catch (e) {
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Wiedergabe fehlgeschlagen: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -34,12 +34,8 @@ class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
|
||||
final sources = items
|
||||
.map((item) => AudioSource.uri(Uri.parse(item.id), tag: item))
|
||||
.toList();
|
||||
try {
|
||||
await _player.setAudioSources(sources, initialIndex: startIndex);
|
||||
await play();
|
||||
} catch (e) {
|
||||
playbackState.addError(e);
|
||||
}
|
||||
await _player.setAudioSources(sources, initialIndex: startIndex);
|
||||
await play();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -17,6 +17,7 @@ class NowPlayingScreen extends StatelessWidget {
|
||||
appBar: AppBar(
|
||||
backgroundColor: Colors.transparent,
|
||||
leading: IconButton(
|
||||
tooltip: 'Schließen',
|
||||
icon: const Icon(Icons.keyboard_arrow_down),
|
||||
onPressed: () => Navigator.of(context).maybePop(),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user