Files
Melo/lib/library/library_service.dart

118 lines
3.6 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';
/// Ob der "Erneut scannen"-Button zusätzlich zum [addMusicLabel]-Button
/// angezeigt wird. Auf Android tun [LibraryService.pickFolderAndScan] und
/// [LibraryService.rescan] dasselbe (kompletter MediaStore-Scan) — dort
/// reicht ein Button. Pure Funktion statt direkter [Platform.isAndroid]-
/// Abfrage, damit sie unabhängig von der echten Plattform testbar ist.
bool zeigeErneutScannenButton(bool isAndroid) => !isAndroid;
/// 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;
bool notificationPermissionDenied = false;
String? scanError;
/// Fragt beim ersten Start alle für den Betrieb nötigen Berechtigungen an
/// (Musik-Zugriff + Wiedergabe-Benachrichtigung), statt sie erst bei Bedarf
/// einzeln einzufordern.
///
/// Nacheinander, nicht gleichzeitig: `permission_handler` wirft, wenn eine
/// zweite Abfrage startet, während die erste noch offen ist.
Future<void> ensureStartupPermissions() async {
await _ensurePermission();
final denied = !await ensureNotificationPermission();
if (denied == notificationPermissionDenied) return;
notificationPermissionDenied = denied;
notifyListeners();
}
/// 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();
final denied = !granted;
if (denied != permissionDenied) {
permissionDenied = denied;
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();
}
}