## Critical Fixes - Fix CRITICAL-1: Empty scans no longer delete entire library - Use timestamp-based tombstone logic instead of path-based - Only mark missing if livePaths is not empty - Fix CRITICAL-2: Re-scanned files now return from deleted state - Add deleted=false to SongsCompanion.insert in both scanners ## High Priority Fixes - HIGH-1: Fix SQLite variable limit crash on large libraries - Replace isNotIn() with timestamp comparison (O(1) not O(n)) - HIGH-2: Fix UI freezing during scan - Throttle progress updates (every 50 files or end of scan) - HIGH-3: Add error handling for scan failures - Wrap scanFolders/scanAndroidMediaStore in try/catch - Display scanError banner in LibraryScreen - HIGH-4: Add error handling for playback failures - Wrap loadPlaylist in try/catch ## Medium Priority Fixes - MEDIUM-1: Replace ! with ?? to handle unknown ProcessingState - MEDIUM-2: Escape LIKE wildcards in search queries - MEDIUM-4: Display StreamBuilder errors instead of treating as empty - MEDIUM-8: Add tooltips to all IconButtons for accessibility - Add doc comments to public database APIs ## Testing - Add regression tests for CRITICAL-1 and CRITICAL-2 - All 9 existing tests pass Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
88 lines
2.4 KiB
Dart
88 lines
2.4 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;
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|