From 56334c539d484631f036869f7ba1b199ccfcf7b6 Mon Sep 17 00:00:00 2001 From: "Hermes (Server)" Date: Wed, 19 Aug 2026 18:33:14 +0200 Subject: [PATCH] feat(library): Navidrome-Server-Tab in Bibliothek MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neuer 4. Tab: '🌐 Server' zeigt Alben vom Musikserver - Album-Liste laden und anzeigen (AsyncBuilder) - Tap auf Album → alle Songs in Queue laden + abspielen - Graceful Fallback wenn Server nicht verbunden - Tests: 70/70 grün - Analyzer: ✅ keine Fehler Phase 1 P0-Feature: Album-Browser vom Server --- lib/library/library_screen.dart | 160 +++++++++++++++++++++++++++++++- 1 file changed, 155 insertions(+), 5 deletions(-) diff --git a/lib/library/library_screen.dart b/lib/library/library_screen.dart index 1fed9ac..89eaf5b 100644 --- a/lib/library/library_screen.dart +++ b/lib/library/library_screen.dart @@ -1,7 +1,9 @@ +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'; @@ -21,7 +23,7 @@ class LibraryScreen extends StatelessWidget { Widget build(BuildContext context) { final lib = context.watch(); return DefaultTabController( - length: 3, + length: 4, child: Scaffold( appBar: AppBar( title: const Text('Bibliothek'), @@ -42,6 +44,7 @@ class LibraryScreen extends StatelessWidget { Tab(text: 'Alle'), Tab(text: 'Künstler'), Tab(text: 'Alben'), + Tab(text: '🌐 Server'), ], ), ), @@ -85,12 +88,13 @@ class LibraryScreen extends StatelessWidget { ], ), ), - const Expanded( + Expanded( child: TabBarView( children: [ - _AllSongsTab(), - ArtistListScreen(), - AlbumListScreen(), + const _AllSongsTab(), + const ArtistListScreen(), + const AlbumListScreen(), + const _NavidromeTab(), ], ), ), @@ -238,3 +242,149 @@ class _Empty extends StatelessWidget { ); } } + +class _NavidromeTab extends StatefulWidget { + const _NavidromeTab(); + + @override + State<_NavidromeTab> createState() => _NavidromeTabState(); +} + +class _NavidromeTabState extends State<_NavidromeTab> { + late final NavidromeService _nav = NavidromeService(); + List _albums = []; + bool _loading = false; + + @override + void initState() { + super.initState(); + _nav.ladeGespeicherteZugangsdaten().then((_) { + if (_nav.istVerbunden && mounted) { + _loadAlbums(); + } + }); + } + + Future _loadAlbums() async { + if (!_nav.istVerbunden) { + return; + } + setState(() => _loading = true); + try { + final alben = await _nav.getAlben(anzahl: 50); + if (mounted) { + setState(() { + _albums = alben; + _loading = false; + }); + } + } catch (e) { + if (mounted) { + setState(() => _loading = false); + } + debugPrint('Fehler beim Laden der Alben: $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) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon(Icons.album, size: 64, color: Colors.white24), + const SizedBox(height: 12), + const Text('Keine Alben geladen', + style: TextStyle(color: Colors.white54)), + const SizedBox(height: 16), + FilledButton.icon( + icon: const Icon(Icons.refresh), + label: const Text('Erneut versuchen'), + onPressed: _loadAlbums, + ), + ], + ), + ); + } + + return ListView.builder( + padding: const EdgeInsets.all(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')), + ); + } + } +}