## 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>
93 lines
2.5 KiB
Dart
93 lines
2.5 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:file_picker/file_picker.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:uuid/uuid.dart';
|
|
|
|
import 'android_scan.dart';
|
|
import 'database.dart';
|
|
import 'permissions.dart';
|
|
import 'scan_service.dart';
|
|
|
|
/// Beschriftung des Hinzufügen-/Scan-Buttons je Plattform.
|
|
String get addMusicLabel =>
|
|
Platform.isAndroid ? 'Musik scannen' : 'Musikordner hinzufügen';
|
|
|
|
/// Koordiniert Ordnerwahl und Scans; hält den Scan-Fortschritt für die UI.
|
|
/// Android scannt automatisch über MediaStore (Scoped Storage lässt keinen
|
|
/// direkten Dateizugriff zu); Desktop scannt vom Nutzer gewählte Ordner.
|
|
class LibraryService extends ChangeNotifier {
|
|
LibraryService(this.db);
|
|
final MeloDb db;
|
|
static const _uuid = Uuid();
|
|
|
|
bool scanning = false;
|
|
int scanDone = 0;
|
|
int scanTotal = 0;
|
|
bool permissionDenied = false;
|
|
String? scanError;
|
|
|
|
/// Auf Android: automatischer Geräte-Scan. Auf Desktop: Ordner wählen + scannen.
|
|
Future<void> pickFolderAndScan() async {
|
|
if (!await _ensurePermission()) return;
|
|
if (Platform.isAndroid) {
|
|
await _scan();
|
|
return;
|
|
}
|
|
final path = await FilePicker.getDirectoryPath();
|
|
if (path == null) return;
|
|
await db.addFolder(FoldersCompanion.insert(
|
|
id: _uuid.v4(),
|
|
path: path,
|
|
updatedAtMs: DateTime.now().millisecondsSinceEpoch,
|
|
));
|
|
await _scan();
|
|
}
|
|
|
|
/// Scannt erneut (Android: ganzes Gerät; Desktop: alle gemerkten Ordner).
|
|
Future<void> rescan() async {
|
|
if (!await _ensurePermission()) return;
|
|
await _scan();
|
|
}
|
|
|
|
Future<bool> _ensurePermission() async {
|
|
final granted = await ensureAudioPermission();
|
|
permissionDenied = !granted;
|
|
notifyListeners();
|
|
return granted;
|
|
}
|
|
|
|
void dismissScanError() {
|
|
scanError = null;
|
|
notifyListeners();
|
|
}
|
|
|
|
Future<void> _scan() async {
|
|
if (scanning) return;
|
|
scanning = true;
|
|
scanDone = 0;
|
|
scanTotal = 0;
|
|
scanError = null;
|
|
notifyListeners();
|
|
try {
|
|
if (Platform.isAndroid) {
|
|
await scanAndroidMediaStore(db, onProgress: _onProgress);
|
|
} else {
|
|
final folders = (await db.activeFolders()).map((f) => f.path).toList();
|
|
await scanFolders(db, folders, onProgress: _onProgress);
|
|
}
|
|
} catch (e) {
|
|
scanError = 'Scan fehlgeschlagen: $e';
|
|
} finally {
|
|
scanning = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
void _onProgress(int done, int total) {
|
|
scanDone = done;
|
|
scanTotal = total;
|
|
notifyListeners();
|
|
}
|
|
}
|