import 'dart:async'; import 'dart:io'; import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'downloads/downloads_screen.dart'; import 'library/android_scan.dart'; import 'library/auto_scan.dart'; import 'library/category_service.dart'; import 'library/database.dart'; import 'library/favorites_screen.dart'; import 'library/library_service.dart'; import 'library/my_music_screen.dart'; import 'library/permissions.dart'; import 'library/playlist_service.dart'; import 'library/search_screen.dart'; import 'player/audio_effects.dart'; import 'player/audio_handler.dart'; import 'player/mini_player.dart'; import 'services/baka_auth.dart'; import 'services/logger_service.dart'; import 'services/melo_cloud_service.dart'; import 'services/offline_mode.dart'; import 'services/sync_service.dart'; import 'services/yt_download_service.dart'; import 'settings/app_settings.dart'; import 'shared/theme.dart'; late final MeloAudioHandler _handler; late final MeloDb _db; late final LibraryService _library; late final PlaylistService _playlists; late final OfflineMode _offlineMode; late final CategoryService _categories; late final AppSettings _settings; late final AudioEffects _effects; late final BakaAuth _bakaAuth; late final YtDownloadService _ytDownload; late final SyncService _sync; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await logger.init(); _db = MeloDb(); _library = LibraryService(_db); _playlists = PlaylistService(_db); _offlineMode = OfflineMode(); await _offlineMode.init(); _categories = CategoryService(_db); _settings = AppSettings(); await _settings.init(); _handler = await AudioService.init( builder: () => MeloAudioHandler(db: _db), config: const AudioServiceConfig( androidNotificationChannelId: 'de.baka.melo.audio', androidNotificationChannelName: 'Melo', androidNotificationOngoing: true, androidShowNotificationBadge: true, ), ); _effects = AudioEffects(); await _effects.init(); _bakaAuth = BakaAuth(); await _bakaAuth.laden(); _ytDownload = YtDownloadService(auth: _bakaAuth); _sync = SyncService( db: _db, cloud: MeloCloudService(auth: _bakaAuth), ); await _sync.laden(); // Die Audio-Session steht erst beim Abspielen fest und kann wechseln — // die Klangeffekte müssen dann neu daran gehängt werden. _handler.androidAudioSessionIdStream.listen((id) { if (id != null) _effects.attach(id); }); void uebernehmeEinstellungen() { _handler.autoPlayOnHeadphones = _settings.autoPlayOnHeadphones; _handler.normalizeVolume = _settings.normalizeVolume; } uebernehmeEinstellungen(); _settings.addListener(uebernehmeEinstellungen); // Der Offline-Schalter liegt in einem eigenen Dienst, wirkt aber genauso: // ohne diese Weitergabe war er nur gespeichert und die App streamte weiter. void uebernehmeOfflineModus() => _handler.offlineOnly = _offlineMode.enabled; uebernehmeOfflineModus(); _offlineMode.addListener(uebernehmeOfflineModus); runApp(const MeloApp()); } class MeloApp extends StatelessWidget { const MeloApp({super.key}); @override Widget build(BuildContext context) { return MultiProvider( providers: [ Provider.value(value: _handler), Provider.value(value: _db), ChangeNotifierProvider.value(value: _library), ChangeNotifierProvider.value(value: _playlists), ChangeNotifierProvider.value(value: _offlineMode), ChangeNotifierProvider.value(value: _categories), ChangeNotifierProvider.value(value: _settings), ChangeNotifierProvider.value(value: _effects), ChangeNotifierProvider.value(value: _bakaAuth), ChangeNotifierProvider.value(value: _ytDownload), ChangeNotifierProvider.value(value: _sync), ], child: MaterialApp( title: 'Melo', debugShowCheckedModeBanner: false, theme: MeloTheme.dark, home: const HomeShell(), ), ); } } class HomeShell extends StatefulWidget { const HomeShell({super.key}); @override State createState() => _HomeShellState(); } class _HomeShellState extends State with WidgetsBindingObserver { int _index = 0; late final AutoScan _autoScan; void _goTo(int index) => setState(() => _index = index); @override void initState() { super.initState(); final db = context.read(); final lib = context.read(); _autoScan = AutoScan( geraeteAnzahl: countAndroidMediaStoreSongs, bekannteAnzahl: db.countSongs, scanne: lib.rescan, ); WidgetsBinding.instance.addObserver(this); // Beim Start einmal mit dem Server abgleichen — neue Titel von anderen // Geräten sind dann sofort da. context.read().automatisch(); unawaited(_frageBenachrichtigungsRecht(context.read())); } /// Fragt das Recht auf die Wiedergabe-Benachrichtigung, sobald der erste /// Titel läuft. Beim kalten Start käme die Frage ohne Zusammenhang — und /// zweimal weggetippt sperrt Android sie dauerhaft. /// /// Scheitern darf das folgenlos: `permission_handler` wirft, wenn beim /// Start noch die Abfrage der Musik-Berechtigung offen ist. Die Wiedergabe /// läuft dann trotzdem, nur ohne Benachrichtigung — daraus einen /// unbehandelten Fehler zu machen, wäre die schlechtere Antwort. Future _frageBenachrichtigungsRecht(MeloAudioHandler handler) async { try { await handler.mediaItem.firstWhere((item) => item != null); await ensureNotificationPermission(); } catch (e) { logger.warning('Benachrichtigungs-Recht nicht erfragt: $e'); } } @override void dispose() { WidgetsBinding.instance.removeObserver(this); super.dispose(); } @override void didChangeAppLifecycleState(AppLifecycleState state) { // Zurück in der App: nachsehen, ob inzwischen Musik dazugekommen ist — // auf dem Gerät wie am Server. if (state == AppLifecycleState.resumed) { if (Platform.isAndroid) _autoScan.pruefe(); context.read().automatisch(); } } @override Widget build(BuildContext context) { final tabs = [ MyMusicScreen( onSearchTap: () => _goTo(2), onFavoritesTap: () => _goTo(3), ), const DownloadsScreen(), const SearchScreen(), const FavoritesScreen(), ]; return Scaffold( body: Column( children: [ Expanded(child: IndexedStack(index: _index, children: tabs)), const MiniPlayer(), ], ), // Haarlinie darüber: ohne sie geht die Leiste auf schwarzem Grund // optisch im Inhalt auf und wirkt nicht wie ein Hauptmenü. bottomNavigationBar: DecoratedBox( decoration: const BoxDecoration( border: Border(top: BorderSide(color: MeloTheme.border)), ), child: BottomNavigationBar( currentIndex: _index, onTap: _goTo, items: const [ BottomNavigationBarItem( icon: Icon(Icons.headphones), label: 'Meine Musik'), BottomNavigationBarItem( icon: Icon(Icons.download_outlined), activeIcon: Icon(Icons.download), label: 'Download'), BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Suchen'), BottomNavigationBarItem( icon: Icon(Icons.favorite_border), activeIcon: Icon(Icons.favorite), label: 'Favoriten'), ], ), ), ); } }