import 'package:audio_service/audio_service.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import '../player/audio_handler.dart'; import '../services/navidrome_service.dart'; import '../shared/cover.dart'; import 'album_list.dart'; import 'artist_list.dart'; import 'database.dart'; import 'library_service.dart'; import 'permissions.dart'; import 'song_list.dart'; import 'song_media.dart'; /// Bibliotheks-Tab mit drei Unteransichten: alle Songs, Künstler, Alben. /// Scan-Banner/-Fortschritt gelten für alle Unteransichten und liegen daher /// oberhalb der [TabBarView]. class LibraryScreen extends StatelessWidget { const LibraryScreen({super.key}); @override Widget build(BuildContext context) { final lib = context.watch(); return DefaultTabController( length: 4, child: Scaffold( appBar: AppBar( title: const Text('Bibliothek'), actions: [ IconButton( tooltip: addMusicLabel, icon: const Icon(Icons.create_new_folder_outlined), onPressed: lib.scanning ? null : lib.pickFolderAndScan, ), IconButton( tooltip: 'Erneut scannen', icon: const Icon(Icons.refresh), onPressed: lib.scanning ? null : lib.rescan, ), ], bottom: const TabBar( // Engere Abstände, damit '🌐 Server' nicht abgeschnitten wird // (4 feste Tabs, gleich breit) — Schriftgröße bleibt unverändert. labelPadding: EdgeInsets.symmetric(horizontal: 4), tabs: [ Tab(text: 'Alle'), Tab(text: 'Künstler'), Tab(text: 'Alben'), Tab(text: '🌐 Server'), ], ), ), body: Column( children: [ if (lib.permissionDenied) MaterialBanner( backgroundColor: Colors.red.shade900, content: const Text('Zugriff auf Musik verweigert. ' 'Bitte in den Einstellungen erlauben.'), actions: [ TextButton( onPressed: openMusicPermissionSettings, child: const Text('Einstellungen'), ), ], ), if (lib.scanError != null) MaterialBanner( backgroundColor: Colors.red.shade900, content: Text(lib.scanError!), actions: [ TextButton( onPressed: lib.dismissScanError, child: const Text('Verwerfen'), ), ], ), if (lib.scanning) Padding( padding: const EdgeInsets.all(12), child: Column( children: [ const LinearProgressIndicator(), const SizedBox(height: 6), Text( 'Scanne … ${lib.scanDone}' '${lib.scanTotal > 0 ? ' / ${lib.scanTotal}' : ''}', style: const TextStyle(color: Colors.white54, fontSize: 12), ), ], ), ), Expanded( child: TabBarView( children: [ const _AllSongsTab(), const ArtistListScreen(), const AlbumListScreen(), const _NavidromeTab(), ], ), ), ], ), ), ); } } /// "Alle"-Tab: zuletzt hinzugefügt + vollständige Songliste (unverändertes /// Verhalten aus der Zeit vor der Künstler-/Alben-Aufteilung). class _AllSongsTab extends StatelessWidget { const _AllSongsTab(); @override Widget build(BuildContext context) { final db = context.read(); final lib = context.watch(); return Column( children: [ StreamBuilder>( stream: db.watchRecent(limit: 10), builder: (context, snapshot) { final recent = snapshot.data ?? const []; if (recent.isEmpty || lib.scanning) return const SizedBox.shrink(); return _RecentlyAdded(songs: recent); }, ), Expanded( child: StreamBuilder>( stream: db.watchSongs(), builder: (context, snapshot) { if (snapshot.hasError) { return Center( child: Text('Fehler: ${snapshot.error}', style: const TextStyle(color: Colors.white54)), ); } final songs = snapshot.data ?? const []; if (songs.isEmpty && !lib.scanning) { return const _Empty(); } return SongList(songs); }, ), ), ], ); } } /// Horizontal scrollende Leiste mit den zuletzt hinzugefügten Songs. class _RecentlyAdded extends StatelessWidget { const _RecentlyAdded({required this.songs}); final List songs; @override Widget build(BuildContext context) { final handler = context.read(); return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Padding( padding: EdgeInsets.fromLTRB(16, 8, 16, 8), child: Text('Zuletzt hinzugefügt', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600)), ), SizedBox( height: 140, child: ListView.builder( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 16), itemCount: songs.length, itemBuilder: (context, i) { final s = songs[i]; return Padding( padding: const EdgeInsets.only(right: 12), child: SizedBox( width: 96, child: InkWell( onTap: () async { try { await playSongs(handler, songs, i); } catch (e) { if (context.mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text('Wiedergabe fehlgeschlagen: $e')), ); } } }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ CoverImage( artUri: s.coverPath != null ? Uri.file(s.coverPath!) : null, size: 96, radius: 8, ), const SizedBox(height: 4), Text( s.title, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontSize: 12), ), ], ), ), ), ); }, ), ), ], ); } } class _Empty extends StatelessWidget { const _Empty(); @override Widget build(BuildContext context) { final lib = context.read(); return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.library_music, size: 64, color: Colors.white24), const SizedBox(height: 12), const Text('Noch keine Musik', style: TextStyle(color: Colors.white54)), const SizedBox(height: 16), FilledButton.icon( icon: const Icon(Icons.create_new_folder_outlined), label: Text(addMusicLabel), onPressed: lib.pickFolderAndScan, ), ], ), ); } } class _NavidromeTab extends StatefulWidget { const _NavidromeTab(); @override State<_NavidromeTab> createState() => _NavidromeTabState(); } class _NavidromeTabState extends State<_NavidromeTab> { late final NavidromeService _nav = NavidromeService(); List _albums = []; List _artists = []; bool _loading = false; bool _showArtists = false; String? _error; @override void initState() { super.initState(); _nav.ladeGespeicherteZugangsdaten().then((_) { if (_nav.istVerbunden && mounted) { _loadAlbums(); } }); } Future _loadAlbums() async { if (!_nav.istVerbunden) { return; } setState(() { _loading = true; _error = null; }); try { final alben = await _nav.getAlben(anzahl: 50); if (mounted) { setState(() { _albums = alben; _loading = false; }); } } catch (e) { if (mounted) { setState(() { _loading = false; _error = e is NavidromeException ? e.message : 'Verbindungsfehler'; }); } debugPrint('Fehler beim Laden der Alben: $e'); } } Future _loadArtists() async { if (!_nav.istVerbunden) return; setState(() { _loading = true; _error = null; }); try { final artists = await _nav.getArtists(); if (mounted) { setState(() { _artists = artists; _loading = false; }); } } catch (e) { if (mounted) { setState(() { _loading = false; _error = e is NavidromeException ? e.message : 'Verbindungsfehler'; }); } debugPrint('Fehler beim Laden der Künstler: $e'); } } @override Widget build(BuildContext context) { if (!_nav.istVerbunden) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.cloud_off, size: 64, color: Colors.white24), const SizedBox(height: 12), const Text('Musikserver nicht verbunden', style: TextStyle(color: Colors.white54)), const SizedBox(height: 12), const Text('Gehe zu Einstellungen → Navidrome zum Verbinden', style: TextStyle(color: Colors.white30, fontSize: 12)), ], ), ); } if (_loading) { return const Center( child: CircularProgressIndicator(color: Colors.redAccent), ); } if (_albums.isEmpty) { final hatFehler = _error != null; return Center( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( mainAxisSize: MainAxisSize.min, children: [ Icon(hatFehler ? Icons.error_outline : Icons.album, size: 64, color: hatFehler ? Colors.redAccent : Colors.white24), const SizedBox(height: 12), Text( hatFehler ? 'Serverfehler: $_error' : 'Keine Alben geladen', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white54), ), const SizedBox(height: 16), FilledButton.icon( icon: const Icon(Icons.refresh), label: const Text('Erneut versuchen'), onPressed: _loadAlbums, ), ], ), ), ); } return Column( children: [ Padding( padding: const EdgeInsets.all(12), child: Row( children: [ Expanded( child: FilledButton( onPressed: () => setState(() { _showArtists = false; if (_albums.isEmpty) _loadAlbums(); }), style: FilledButton.styleFrom( backgroundColor: !_showArtists ? Colors.redAccent : Colors.grey.shade700, ), child: const Text('📀 Alben'), ), ), const SizedBox(width: 8), Expanded( child: FilledButton( onPressed: () => setState(() { _showArtists = true; if (_artists.isEmpty) _loadArtists(); }), style: FilledButton.styleFrom( backgroundColor: _showArtists ? Colors.redAccent : Colors.grey.shade700, ), child: const Text('🎤 Künstler'), ), ), ], ), ), Expanded( child: _showArtists ? ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 12), itemCount: _artists.length, itemBuilder: (context, i) { final artist = _artists[i]; return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: const Icon(Icons.person, size: 48), title: Text(artist.name), onTap: () => _playArtist(artist), ), ); }, ) : ListView.builder( padding: const EdgeInsets.symmetric(horizontal: 12), itemCount: _albums.length, itemBuilder: (context, i) { final album = _albums[i]; return Card( margin: const EdgeInsets.only(bottom: 8), child: ListTile( leading: Icon( Icons.album, size: 48, color: Colors.redAccent.withValues(alpha: 0.5), ), title: Text(album.name), subtitle: Text('${album.songCount} Songs'), onTap: () => _playAlbum(album), ), ); }, ), ), ], ); } Future _playAlbum(SubsonicAlbum album) async { final handler = context.read(); try { final songs = await _nav.getSongs(album.id); if (songs.isEmpty) { if (!mounted) { return; } final messenger = ScaffoldMessenger.of(context); messenger.showSnackBar( const SnackBar(content: Text('Album hat keine Songs')), ); return; } final items = songs .map((s) => MediaItem( id: _nav.streamUrl(s.id).toString(), title: s.titel, artist: s.kuenstler, album: s.album, duration: Duration(seconds: s.dauerSekunden), )) .toList(); await handler.loadPlaylist(items); } catch (e) { if (!mounted) { return; } final messenger = ScaffoldMessenger.of(context); messenger.showSnackBar( SnackBar(content: Text('Fehler: $e')), ); } } Future _playArtist(SubsonicArtist artist) async { final handler = context.read(); try { final songs = await _nav.getArtistSongs(artist.id); if (songs.isEmpty) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Künstler hat keine Songs')), ); return; } final items = songs .map((s) => MediaItem( id: _nav.streamUrl(s.id).toString(), title: s.titel, artist: s.kuenstler, album: s.album, duration: Duration(seconds: s.dauerSekunden), )) .toList(); await handler.loadPlaylist(items); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Fehler: $e')), ); } } }