118 lines
3.6 KiB
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();
|
|
}
|
|
}
|