import 'dart:async'; import 'package:flutter/material.dart'; import 'dart:io' show Platform; import 'package:audio_service/audio_service.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_background_service/flutter_background_service.dart'; import 'database/db_helper.dart'; import 'services/favoriten_service.dart'; import 'services/auth_service.dart'; import 'services/cloud_service.dart'; import 'services/melo_logger.dart'; import 'services/audio_handler.dart'; import 'services/musik_scanner.dart'; import 'services/sync_service.dart'; import 'utils/farb_theme.dart'; import 'utils/user_effekte.dart'; import 'screens/home_screen.dart'; import 'screens/login_screen.dart'; /// Globaler Notification-Plugin (wird in main() initialisiert) final FlutterLocalNotificationsPlugin notificationsPlugin = FlutterLocalNotificationsPlugin(); /// Globaler Timer für periodischen Auto-Scan (wird in MeloAppState.dispose gecancelt) Timer? _autoScanTimer; void main() async { WidgetsFlutterBinding.ensureInitialized(); // Logger startet sofort – zeichnet ALLES auf MeloLogger().init('2.52.3'); // ── Notifications initialisieren (Issue #9) ── const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher'); const initSettings = InitializationSettings(android: androidInit); await notificationsPlugin.initialize( settings: initSettings, // Tipp auf die Sync-Abschluss-/Fehler-Notification → Cloud-Tab öffnen onDidReceiveNotificationResponse: (response) { if (response.payload == 'sync_fertig' || response.payload == 'sync_fehler') { SyncService.syncBenachrichtigungGetippt.value++; } }, ); // Notification-Channel für Downloads (Fortschritt + Abschluss) const downloadChannel = AndroidNotificationChannel( 'de.baka.melo.downloads', 'Melo Downloads', description: 'Download-Fortschritt und Abschluss', importance: Importance.low, playSound: false, enableVibration: false, ); await notificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(downloadChannel); // Notification-Channel für den Cloud-Sync (Fortschritt + Abschluss) const syncChannel = AndroidNotificationChannel( 'de.baka.melo.sync', 'Melo Sync', description: 'Cloud-Sync-Fortschritt und Abschluss', importance: Importance.low, playSound: false, enableVibration: false, ); await notificationsPlugin .resolvePlatformSpecificImplementation< AndroidFlutterLocalNotificationsPlugin>() ?.createNotificationChannel(syncChannel); // ── Background Service initialisieren (Issue #3) ── await FlutterBackgroundService().configure( iosConfiguration: IosConfiguration(), androidConfiguration: AndroidConfiguration( onStart: _onServiceStart, autoStart: false, isForegroundMode: true, ), ); try { await DbHelper().db; await FavoritenService().init(); // Hintergrund-Audio-Service starten await AudioService.init( builder: () => MeloAudioHandler(), config: const AudioServiceConfig( androidNotificationChannelId: 'de.baka.melo.audio', androidNotificationChannelName: 'Melo Wiedergabe', androidStopForegroundOnPause: false, ), ); MeloLogger().zustand('start_ok', {'db': 'ok', 'audio': 'ok'}); } catch (e, stack) { MeloLogger().fehler('App-Start', e, stack); } // Auth initialisieren (Token aus SharedPreferences laden + online validieren) await AuthService().initialisieren(); // Favoriten-Toggle → Server-Push (Feuer-und-Vergessen, deterministisch via // `set` statt Toggle — verhindert Doppel-Toggle). Songs ohne cloud_id // (nicht hochgeladen) werden übersprungen; ein Fehlschlag wird beim // nächsten vollständigen Sync korrigiert (favoritenMerge-Vereinigung). FavoritenService().onStatusGeaendert = (songId, neuerStatus) async { try { final song = await DbHelper().songNachId(songId); final cid = song?.cloudId; if (cid == null || cid.isEmpty) return; await CloudService().setFavorite(cid, favorit: neuerStatus); } catch (e) { MeloLogger().fehler('favoriten_server_push', e); } }; // Android-Berechtigungen beim Start abfragen (Issue #4) // Speicherzugriff für Musik-Scan (READ_MEDIA_AUDIO / Storage) await MusikScanner().frageSpeicherZugriff(); // Auto-Scan: Musikordner beim Start scannen + periodisch alle 3h (Issue #6) _starteAutoScan(); // POST_NOTIFICATIONS für Android 13+ (Notifications für Downloads, Player) if (Platform.isAndroid) { await Permission.notification.request(); } // Per-User-Akzent (überschreibbar in den Einstellungen) + Begrüßungs-Sound // beim App-Start (wenn Token vorhanden). Fehler sind unkritisch. await UserEffekt.anwenden(AuthService().benutzer); // App-weiter Auto-Sync-Timer (F3): Intervall aus `cloud_interval` // (0 = Aus), erster Tick erst NACH dem Intervall — kein Sync beim Start. unawaited(SyncService.starteAutoSyncTimer()); runApp(const MeloApp()); } /// Führt Musik-Scan beim Start aus + startet Timer.periodic alle 3 Stunden (Issue #6) /// Scannt nur /Music, /Download, /Musik, /Downloads — nicht ganz /storage. void _starteAutoScan() { // Sofort beim Start scannen (asynchron, blockiert nicht) MusikScanner().scanneMusikOrdner().then((songs) { MeloLogger().zustand('auto_scan_start', {'gefunden': songs.length}); }).catchError((e) { MeloLogger().fehler('auto_scan_start', e); }); // Periodischer Scan alle 3 Stunden – Timer wird in MeloAppState.dispose() gecancelt _autoScanTimer = Timer.periodic(const Duration(hours: 3), (_) { MusikScanner().scanneMusikOrdner().then((songs) { MeloLogger().zustand('auto_scan_periodisch', {'gefunden': songs.length}); }).catchError((e) { MeloLogger().fehler('auto_scan_periodisch', e); }); }); } /// Background Service Callback – hält App im Vordergrund beim Download (Issue #3) @pragma('vm:entry-point') Future _onServiceStart(ServiceInstance service) async { WidgetsFlutterBinding.ensureInitialized(); // Service läuft als Foreground-Service und hält den Prozess am Leben. // Der eigentliche Download läuft im Haupt-Isolate – der Service dient nur // als WakeLock / Foreground-Garantie. service.on('stopService').listen((_) { service.stopSelf(); }); return true; } class MeloApp extends StatefulWidget { const MeloApp({super.key}); @override State createState() => _MeloAppState(); } class _MeloAppState extends State { @override Widget build(BuildContext context) { final auth = AuthService(); return MaterialApp( title: 'Melo', debugShowCheckedModeBanner: false, theme: MeloTheme.theme, home: auth.istEingeloggt ? const Scaffold(body: MeloHome()) : const LoginScreen(), ); } @override void dispose() { _autoScanTimer?.cancel(); _autoScanTimer = null; super.dispose(); } }