import 'package:flutter/material.dart'; import '../services/navidrome_service.dart'; import '../utils/farb_theme.dart'; import '../viewmodels/melo_home_viewmodel.dart'; /// Navidrome-Browser als Bottom-Sheet. /// Manuell in home_screen.dart einbaubar. class NavidromeBrowser extends StatefulWidget { final MeloHomeViewModel vm; const NavidromeBrowser({super.key, required this.vm}); @override State createState() => _NavidromeBrowserState(); } class _NavidromeBrowserState extends State { String? _gewaehltesAlbum; @override Widget build(BuildContext context) { final vm = widget.vm; return Padding( padding: const EdgeInsets.fromLTRB(20, 16, 20, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Header Row( children: [ const Text('📡 Navidrome', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white)), const Spacer(), if (!vm.navidrome.istVerbunden) _btn(context, 'Verbinden', () => _zeigeLoginDialog(context)) else ...[ Text(vm.navidrome.istVerbunden ? '✅' : '', style: const TextStyle(fontSize: 14)), const SizedBox(width: 8), _btn(context, 'Alben laden', () => vm.ladeNavidromeAlben().then((_) => setState(() {}))), ], ], ), const SizedBox(height: 12), // Inhalt if (!vm.navidrome.istVerbunden) _platzhalter('Server-URL + Zugangsdaten eingeben') else if (vm.serverLadt) const Center(child: Padding( padding: EdgeInsets.all(20), child: CircularProgressIndicator(color: MeloTheme.rot, strokeWidth: 2), )) else if (_gewaehltesAlbum != null) _albumSongListe(context, _gewaehltesAlbum!) else if (vm.navidromeAlben.isEmpty) _platzhalter('"Alben laden" um Musik zu sehen') else _albumListe(context, vm.navidromeAlben), ], ), ); } Widget _platzhalter(String text) { return Expanded(child: Center( child: Text(text, style: const TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)), )); } Widget _albumListe(BuildContext context, List alben) { return Expanded( child: Column( children: [ Text('${alben.length} Alben', style: const TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)), const SizedBox(height: 8), Expanded( child: ListView.builder( itemCount: alben.length, itemBuilder: (_, i) => GestureDetector( onTap: () async { setState(() => _gewaehltesAlbum = alben[i].id); }, child: Container( margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Container( width: 36, height: 36, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: MeloTheme.rot.withValues(alpha: 0.3), ), child: const Center(child: Text('💿', style: TextStyle(fontSize: 14))), ), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(alben[i].name, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white)), Text('${alben[i].songCount} Songs', style: const TextStyle(fontSize: 10, color: MeloTheme.textSekundaer)), ], ), ), const Icon(Icons.chevron_right, size: 16, color: MeloTheme.textSekundaer), ], ), ), ), ), ), ], ), ); } Widget _albumSongListe(BuildContext context, String albumId) { return FutureBuilder>( future: widget.vm.ladeAlbumSongs(albumId), builder: (_, snap) { if (snap.connectionState != ConnectionState.done) { return const Expanded(child: Center( child: CircularProgressIndicator(color: MeloTheme.rot, strokeWidth: 2), )); } final songs = snap.data ?? []; return Expanded( child: Column( children: [ Row( children: [ GestureDetector( onTap: () => setState(() => _gewaehltesAlbum = null), child: const Icon(Icons.arrow_back, size: 18, color: MeloTheme.rot), ), const SizedBox(width: 8), Text('${songs.length} Songs', style: const TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)), ], ), const SizedBox(height: 8), Expanded( child: ListView.builder( itemCount: songs.length, itemBuilder: (_, i) => _songTile(context, songs[i]), ), ), ], ), ); }, ); } Widget _songTile(BuildContext context, SubsonicSong s) { return Container( margin: const EdgeInsets.only(bottom: 6), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(10), ), child: Row( children: [ Container( width: 32, height: 32, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), color: MeloTheme.rot.withValues(alpha: 0.3), ), child: const Center(child: Text('♪', style: TextStyle(fontSize: 14))), ), const SizedBox(width: 8), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(s.titel, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white), overflow: TextOverflow.ellipsis), Text('${s.kuenstler} · ${s.dauerFormatiert}', style: const TextStyle(fontSize: 10, color: MeloTheme.textSekundaer)), ], ), ), GestureDetector( onTap: () => widget.vm.downloadNavidromeSong(s).then((_) => setState(() {})), child: const Icon(Icons.download, size: 18, color: MeloTheme.rot), ), const SizedBox(width: 8), GestureDetector( onTap: () => widget.vm.starteNavidromeStream(s), child: const Icon(Icons.play_arrow, size: 18, color: Colors.greenAccent), ), ], ), ); } Widget _btn(BuildContext context, String label, VoidCallback onTap) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( border: Border.all(color: MeloTheme.dunkel2), borderRadius: BorderRadius.circular(8), ), child: Text(label, style: const TextStyle(fontSize: 11, color: MeloTheme.rot)), ), ); } void _zeigeLoginDialog(BuildContext context) { final urlCtrl = TextEditingController(); final userCtrl = TextEditingController(); final passCtrl = TextEditingController(); bool verbindet = false; String? fehler; showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setDialogState) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('🌐 Navidrome', style: TextStyle(color: Colors.white, fontSize: 16)), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField(controller: urlCtrl, style: const TextStyle(color: Colors.white), decoration: const InputDecoration(labelText: 'Server-URL', hintText: 'https://musik.baka-net.de', labelStyle: TextStyle(color: Colors.grey), hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder())), const SizedBox(height: 8), TextField(controller: userCtrl, style: const TextStyle(color: Colors.white), decoration: const InputDecoration(labelText: 'Benutzer', labelStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder())), const SizedBox(height: 8), TextField(controller: passCtrl, style: const TextStyle(color: Colors.white), obscureText: true, decoration: const InputDecoration(labelText: 'Passwort', labelStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder())), if (fehler != null) ...[ const SizedBox(height: 8), Text(fehler!, style: const TextStyle(color: Colors.red, fontSize: 12)), ], if (verbindet) const Padding(padding: EdgeInsets.only(top: 12), child: SizedBox(width: 20, height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: MeloTheme.rot))), ], ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen')), TextButton( onPressed: verbindet ? null : () async { verbindet = true; setDialogState(() {}); widget.vm.verbindeNavidrome(urlCtrl.text, userCtrl.text, passCtrl.text); final ok = await widget.vm.ladeNavidromeAlben(); verbindet = false; if (ok && ctx.mounted) { Navigator.pop(ctx); if (context.mounted) setState(() {}); } else if (ctx.mounted) { setDialogState(() => fehler = '❌ Keine Verbindung\nPrüfe URL + Zugangsdaten'); } }, child: const Text('Verbinden', style: TextStyle(color: MeloTheme.rot)), ), ], ), ), ); } } extension on SubsonicSong { String get dauerFormatiert { final min = dauerSekunden ~/ 60; final sek = dauerSekunden % 60; return '$min:${sek.toString().padLeft(2, '0')}'; } }