import 'package:flutter/material.dart'; import 'dart:async'; import '../viewmodels/melo_home_viewmodel.dart'; import '../models/song.dart'; import '../models/playlist.dart'; import '../utils/farb_theme.dart'; import '../widgets/mini_player.dart'; import '../widgets/melo_header.dart'; import '../widgets/statistik_card.dart'; import '../widgets/tag_leiste.dart'; import '../widgets/song_tile.dart'; import '../widgets/recent_widget.dart'; import 'download_screen.dart'; import 'cloud_screen.dart'; import 'settings_screen.dart'; import 'login_screen.dart'; import 'recap_screen.dart'; import 'now_playing_screen.dart'; import 'erweitert_screen.dart'; import '../services/cloud_service.dart'; import '../services/player_service.dart'; import '../services/auth_service.dart'; import '../services/sync_service.dart'; import '../config/app_config.dart'; class MeloHome extends StatefulWidget { const MeloHome({super.key}); @override State createState() => _MeloHomeState(); } class _MeloHomeState extends State { final MeloHomeViewModel _vm = MeloHomeViewModel(); final CloudService _cloud = CloudService(); /// 0=Start, 1=Bibliothek, 2=Jetzt läuft, 3=Cloud, 4=Mehr int _aktiverTab = 0; /// Aktives Segment im Bibliothek-Tab String _bibSegment = 'Songs'; static const _bibSegmente = [ 'Songs', 'Alben', 'Künstler', 'Jahre', 'Genres', 'Tags', 'Playlists', 'Favoriten', 'Downloads', ]; @override void initState() { super.initState(); _vm.ladeSongs(); // Gespeicherte Navidrome-Zugangsdaten laden → Musikserver-Statuspunkt _vm.navidrome.ladeGespeicherteZugangsdaten().then((_) { if (mounted) setState(() {}); }); // Cloud-Verbindung beim App-Start herstellen (feuern-und-vergessen). _cloud.verbinde(); // Tipp auf die Sync-Abschluss-/Fehler-Notification → Cloud-Tab öffnen SyncService.syncBenachrichtigungGetippt.addListener(_syncNotifGetippt); } @override void dispose() { SyncService.syncBenachrichtigungGetippt.removeListener(_syncNotifGetippt); _vm.dispose(); super.dispose(); } /// Öffnet den Cloud-Tab, wenn die Sync-Notification getippt wurde. void _syncNotifGetippt() { if (!mounted) return; setState(() => _aktiverTab = 3); } // ─── Navigation ───────────────────────────────────── /// Öffnet den Now-Playing-Fullscreen mit Slide-up-Animation void _oeffneNowPlaying() { Navigator.of(context).push( PageRouteBuilder( transitionDuration: const Duration(milliseconds: 350), reverseTransitionDuration: const Duration(milliseconds: 280), pageBuilder: (_, __, ___) => const NowPlayingScreen(), transitionsBuilder: (_, animation, __, child) { final offset = Tween( begin: const Offset(0, 1), end: Offset.zero, ).animate( CurvedAnimation( parent: animation, curve: Curves.easeOutCubic, reverseCurve: Curves.easeInCubic, ), ); return SlideTransition(position: offset, child: child); }, ), ); } Future _zeigeSuche() async { final controller = TextEditingController(); String modus = 'Alle'; final ergebnis = await showDialog( context: context, builder: (ctx) => StatefulBuilder( builder: (ctx, setDialogState) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('🔍 Song suchen', style: TextStyle(color: Colors.white, fontSize: 18)), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: controller, autofocus: true, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( hintText: 'Titel, Künstler oder Tag...', hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder(), ), ), const SizedBox(height: 12), Row( children: [ _suchChip('Alle', modus == 'Alle', () => setDialogState(() => modus = 'Alle')), const SizedBox(width: 6), _suchChip('Titel', modus == 'Titel', () => setDialogState(() => modus = 'Titel')), const SizedBox(width: 6), _suchChip('Künstler', modus == 'Künstler', () => setDialogState(() => modus = 'Künstler')), const SizedBox(width: 6), _suchChip('Tag', modus == 'Tag', () => setDialogState(() => modus = 'Tag')), ], ), ], ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen')), TextButton( onPressed: () => Navigator.pop(ctx, '${controller.text}|$modus'), child: const Text('Suchen', style: TextStyle(color: MeloTheme.rot)), ), ], ), ), ); controller.dispose(); if (ergebnis == null || ergebnis.isEmpty) return; final teile = ergebnis.split('|'); final suchtext = teile[0].toLowerCase(); final suchModus = teile.length > 1 ? teile[1] : 'Alle'; if (suchtext.isEmpty) return; // Tag-Namen die zum Suchtext passen final matchingTags = _vm.tags .where((t) => t['name'] != 'Alle' && t['name']!.toLowerCase().contains(suchtext)) .map((t) => t['name']!) .toSet(); final gefiltert = _vm.songs.where((s) { if (suchModus == 'Titel') return s.titel.toLowerCase().contains(suchtext); if (suchModus == 'Künstler') return s.kuenstler.toLowerCase().contains(suchtext); if (suchModus == 'Tag') { return s.tagIds != null && matchingTags.any((name) { final tag = _vm.tagsMap[name]; return tag != null && s.tagIds!.contains(tag.id); }); } // Alle if (s.titel.toLowerCase().contains(suchtext)) return true; if (s.kuenstler.toLowerCase().contains(suchtext)) return true; return s.tagIds != null && matchingTags.any((name) { final tag = _vm.tagsMap[name]; return tag != null && s.tagIds!.contains(tag.id); }); }).toList(); if (!mounted) return; showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: Text('🔍 ${gefiltert.length} Treffer', style: const TextStyle(color: Colors.white)), content: SizedBox( width: double.maxFinite, height: 300, child: gefiltert.isEmpty ? const Center(child: Text('Keine Treffer', style: TextStyle(color: Colors.grey))) : ListView.builder( itemCount: gefiltert.length, itemBuilder: (_, i) => ListTile( leading: Container( width: 36, height: 36, decoration: BoxDecoration( borderRadius: BorderRadius.circular(8), gradient: const LinearGradient(colors: [Color(0xFF1A0000), Color(0xFF660000)]), ), child: const Center(child: Text('♪', style: TextStyle(fontSize: 14, color: Colors.white54))), ), title: Text(gefiltert[i].titel, style: const TextStyle(color: Colors.white)), subtitle: Text(gefiltert[i].kuenstler, style: const TextStyle(color: Colors.grey)), onTap: () { Navigator.pop(ctx); _vm.spieleSong(gefiltert[i]); }, ), ), ), actions: [TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Schließen'))], ), ); } Future _scanMusik() async { final erlaubt = await _vm.scanner.frageSpeicherZugriff(); if (!erlaubt) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Bitte Speicherzugriff erlauben')), ); } return; } await _vm.scanner.scanneMusikOrdner(); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('Scannen fertig: ${_vm.scanner.anzahlNeueSongs} neue Songs gefunden')), ); await _vm.ladeSongs(); } } Future _zeigeServerBrowser() async { if (_vm.navidrome.istVerbunden) { // Trennen: auch gespeicherte Zugangsdaten löschen (SharedPreferences + // SecureStorage) — sonst ist der Chip nach App-Neustart wieder grün. await _vm.navidrome.loescheZugangsdaten(); _vm.navidromeAlben.clear(); if (mounted) setState(() {}); return; } // Verbinden: einfacher Login-Dialog final urlCtrl = TextEditingController(text: AppConfig.navidromeUrl); final userCtrl = TextEditingController(); final passCtrl = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('🌐 Musikserver verbinden', style: TextStyle(color: Colors.white, fontSize: 15)), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField(controller: urlCtrl, style: const TextStyle(color: Colors.white), decoration: const InputDecoration(labelText: 'Server', border: OutlineInputBorder(), labelStyle: TextStyle(color: Colors.grey))), const SizedBox(height: 8), TextField(controller: userCtrl, style: const TextStyle(color: Colors.white), decoration: const InputDecoration(labelText: 'Benutzer', border: OutlineInputBorder(), labelStyle: TextStyle(color: Colors.grey))), const SizedBox(height: 8), TextField(controller: passCtrl, obscureText: true, style: const TextStyle(color: Colors.white), decoration: const InputDecoration(labelText: 'Passwort', border: OutlineInputBorder(), labelStyle: TextStyle(color: Colors.grey))), ], ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen')), TextButton( onPressed: () async { _vm.verbindeNavidrome(urlCtrl.text, userCtrl.text, passCtrl.text); await _vm.ladeNavidromeAlben(); await _vm.navidrome.speichereZugangsdaten(urlCtrl.text, userCtrl.text, passCtrl.text); if (ctx.mounted) Navigator.pop(ctx); setState(() {}); }, child: const Text('Verbinden', style: TextStyle(color: MeloTheme.rot)), ), ], ), ).then((_) { urlCtrl.dispose(); userCtrl.dispose(); passCtrl.dispose(); }); } /// Detail-Dialog zum ☁️ Musikserver-Status (Feature: Umbenennung von /// „Navidrome verbinden“ → „Musikserver“). Future _zeigeMusikserverDetail() async { final verbunden = _vm.navidrome.istVerbunden; await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Row( children: [ Text('☁️', style: TextStyle(fontSize: 20)), SizedBox(width: 8), Text('Musikserver', style: TextStyle(color: Colors.white, fontSize: 17)), ], ), content: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 10, height: 10, decoration: BoxDecoration( shape: BoxShape.circle, color: verbunden ? const Color(0xFF4CAF50) : const Color(0xFFEF5350), ), ), const SizedBox(width: 8), Text( verbunden ? 'Verbunden' : 'Offline', style: TextStyle( color: verbunden ? const Color(0xFFA5D6A7) : const Color(0xFFEF9A9A), fontSize: 13, fontWeight: FontWeight.w600, ), ), ], ), const SizedBox(height: 10), Text( verbunden ? 'Verbunden mit Navidrome · musik.baka-net.de' : 'Nicht mit dem Musikserver verbunden.\nTippe auf „Verbinden“, um Alben zu durchstöbern.', style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 13), ), ], ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Schließen')), if (verbunden) TextButton( onPressed: () { Navigator.pop(ctx); _zeigeServerBrowser(); // trennt (Credentials leeren) }, child: const Text('Trennen', style: TextStyle(color: MeloTheme.textSekundaer)), ), TextButton( onPressed: () { Navigator.pop(ctx); if (!verbunden) _zeigeServerBrowser(); }, child: Text( verbunden ? 'Browser öffnen' : 'Verbinden', style: const TextStyle(color: MeloTheme.rot), ), ), ], ), ); } void _zeigeAddToPlaylist(Song song) async { final playlists = await _vm.playlists.allePlaylists(); if (!mounted || playlists.isEmpty) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('Erst eine Playlist erstellen')), ); } return; } final auswahl = await showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('Zu Playlist hinzufügen', style: TextStyle(color: Colors.white, fontSize: 16)), content: SizedBox( width: double.maxFinite, child: ListView.builder( shrinkWrap: true, itemCount: playlists.length, itemBuilder: (_, i) => ListTile( leading: const Icon(Icons.queue_music, color: MeloTheme.rot, size: 18), title: Text(playlists[i].name, style: const TextStyle(color: Colors.white, fontSize: 14)), subtitle: Text('${playlists[i].songCount} Songs', style: const TextStyle(fontSize: 11, color: MeloTheme.textSekundaer)), onTap: () => Navigator.pop(ctx, playlists[i]), ), ), ), actions: [TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen'))], ), ); if (auswahl != null && song.id != null) { await _vm.playlists.songHinzufuegen(auswahl.id!, song.id!); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('→ ${auswahl.name}')), ); } } } void _zeigeDownloadDialog() { // „Lied+“ (YT-Download) als eigenen Screen öffnen Navigator.push( context, MaterialPageRoute( builder: (_) => DownloadScreen( downloader: _vm.downloader, onSongsChanged: _vm.ladeSongs, ), ), ); } /// Korrupten Song mit bekannter ytUrl neu herunterladen Future _erneutHerunterladen(Song song) async { final neu = await _vm.downloader.reDownload(song); if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(neu != null ? '🔄 Neu geladen: ${neu.titel}' : 'Fehler: ${_vm.downloader.fehler ?? 'unbekannt'}'), ), ); await _vm.ladeSongs(); } @override Widget build(BuildContext context) { return ListenableBuilder( // _vm (Songs/Status), _cloud (Verbindung) UND SyncService.laeuftNotifier // (☁️-Chip pulsiert bei laufendem Sync, auch im Hintergrund) listenable: Listenable.merge([_vm, _cloud, SyncService.laeuftNotifier]), builder: (_, _) { if (_vm.ladt) { return const Scaffold( backgroundColor: MeloTheme.schwarz, body: Center(child: CircularProgressIndicator(color: MeloTheme.rot)), ); } return Scaffold( backgroundColor: MeloTheme.schwarz, body: SafeArea( child: switch (_aktiverTab) { 1 => _bibliothekTab(), 2 => _jetztLaeuftTab(), 3 => CloudScreen( cloud: _cloud, onZurueck: () => setState(() => _aktiverTab = 0), ), 4 => _mehrTab(), _ => _startTab(), }, ), bottomNavigationBar: _bottomNav(), ); }, ); } // ─── Tab: 🏠 Start ────────────────────────────────── Widget _startTab() { return Column( children: [ MeloHeader( onDownload: _zeigeDownloadDialog, onSearch: _zeigeSuche, onServer: _zeigeMusikserverDetail, serverVerbunden: _vm.navidrome.istVerbunden, // Chip pulsiert während des Syncs (SyncService-Läuft-Signal), // beim Verbinden und solange der Server lädt. serverSyncLaeuft: SyncService.laeuftNotifier.value || _cloud.status == CloudStatus.verbinde || _vm.serverLadt, ), Expanded( child: ListView( padding: const EdgeInsets.only(bottom: 8), children: [ // Weiterhören / Zuletzt gehört RecentWidget(songs: _vm.letzteSongs, onPlay: _vm.spieleSong), // Jahres-Recap (wie Spotify Wrapped) _recapKarte(), // Hidden Message "Seit 2008" if (_vm.zeigeBotschaft) _botschaftBanner(), StatistikCard( anzahlSongs: _vm.songs.length, gesamtMB: _vm.songs.isEmpty ? '0' : (_vm.songs.fold(0, (int s, Song song) => s + song.groesseBytes) / 1048576).toStringAsFixed(0), gesamtMin: _vm.songs.isEmpty ? 0 : (_vm.songs.fold(0, (int s, Song song) => s + song.dauerSekunden) / 60).round(), anzahlFavoriten: _vm.favoritenIds.length, ), _favoritenSchnellzugriff(), _neueDownloads(), ], ), ), const MiniPlayer(), const SizedBox(height: 8), ], ); } /// ⭐ Favoriten-Schnellzugriff auf dem Start-Tab (horizontale Chips) Widget _favoritenSchnellzugriff() { final favoriten = _vm.songs .where((s) => s.id != null && _vm.favoritenIds.contains(s.id)) .take(8) .toList(); if (favoriten.isEmpty) return const SizedBox.shrink(); return _horizontaleListe( titel: '⭐ Deine Favoriten', songs: favoriten, leerText: '', ); } /// ⬇ Neue Downloads auf dem Start-Tab (die 5 zuletzt hinzugefügten) Widget _neueDownloads() { final neue = _vm.songs.where((s) => s.istHeruntergeladen).take(5).toList(); if (neue.isEmpty) return const SizedBox.shrink(); return _horizontaleListe( titel: '⬇ Neue Downloads', songs: neue, leerText: '', ); } Widget _horizontaleListe({ required String titel, required List songs, required String leerText, }) { return Padding( padding: const EdgeInsets.fromLTRB(20, 4, 20, 8), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( titel, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white70), ), const SizedBox(height: 8), SizedBox( height: 52, child: ListView.separated( scrollDirection: Axis.horizontal, itemCount: songs.length, separatorBuilder: (_, __) => const SizedBox(width: 8), itemBuilder: (_, i) => GestureDetector( onTap: () => _vm.spieleSong(songs[i]), child: Container( padding: const EdgeInsets.symmetric(horizontal: 10), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(10), border: Border.all(color: MeloTheme.dunkel2), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Container( width: 28, height: 28, decoration: BoxDecoration( borderRadius: BorderRadius.circular(6), color: MeloTheme.rot.withValues(alpha: 0.3), ), child: const Center(child: Text('♪', style: TextStyle(fontSize: 12))), ), const SizedBox(width: 6), Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(songs[i].titel, style: const TextStyle(fontSize: 11, fontWeight: FontWeight.w500, color: Colors.white)), Text(songs[i].kuenstler, style: const TextStyle(fontSize: 9, color: MeloTheme.textSekundaer)), ], ), ], ), ), ), ), ), ], ), ); } // ─── Tab: 📚 Bibliothek ───────────────────────────── Widget _bibliothekTab() { return Column( children: [ _bibliothekHeader(), _segmentLeiste(), Expanded(child: _bibliothekInhalt()), const MiniPlayer(), const SizedBox(height: 8), ], ); } Widget _bibliothekHeader() { return Padding( padding: const EdgeInsets.fromLTRB(20, 12, 20, 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '📚 Bibliothek', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Colors.white), ), Text( 'Alles an einem Ort', style: TextStyle(fontSize: 12, color: MeloTheme.textSekundaer), ), ], ), Row(children: [ // „Lied+“ (YT-Download) in die Bibliothek integriert _bibButton(Icons.add_circle_outline, 'Lied +', _zeigeDownloadDialog), const SizedBox(width: 8), _bibButton(Icons.search, 'Suche', _zeigeSuche), ]), ], ), ); } Widget _bibButton(IconData icon, String label, VoidCallback onTap) { return GestureDetector( onTap: onTap, child: Container( height: 38, padding: const EdgeInsets.symmetric(horizontal: 12), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), border: Border.all(color: MeloTheme.dunkel2), ), child: Row( mainAxisSize: MainAxisSize.min, children: [ Icon(icon, size: 16, color: MeloTheme.rot), const SizedBox(width: 6), Text(label, style: const TextStyle(fontSize: 12, color: Colors.white70, fontWeight: FontWeight.w500)), ], ), ), ); } Widget _segmentLeiste() { return SizedBox( height: 38, child: ListView( scrollDirection: Axis.horizontal, padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4), children: _bibSegmente.map((seg) { final aktiv = _bibSegment == seg; return Padding( padding: const EdgeInsets.only(right: 8), child: GestureDetector( onTap: () => setState(() => _bibSegment = seg), child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7), decoration: BoxDecoration( color: aktiv ? MeloTheme.rot : MeloTheme.dunkel1, borderRadius: BorderRadius.circular(16), ), child: Text( seg, style: TextStyle( fontSize: 12, color: aktiv ? Colors.white : MeloTheme.textSekundaer, fontWeight: FontWeight.w600, ), ), ), ), ); }).toList(), ), ); } Widget _bibliothekInhalt() { switch (_bibSegment) { case 'Alben': return _gruppenListe(_gruppiereNach((s) => s.album)); case 'Künstler': return _gruppenListe(_gruppiereNach((s) => s.kuenstler)); case 'Jahre': return _gruppenListe(_gruppiereNach((s) => s.jahr ?? '')); case 'Genres': return _gruppenListe(_gruppiereNach((s) => s.genre ?? '')); case 'Tags': return _tagsSegment(); case 'Playlists': return _playlistsSegment(); case 'Favoriten': final favoriten = _vm.songs .where((s) => s.id != null && _vm.favoritenIds.contains(s.id)) .toList(); return _songListe(quelle: favoriten, titel: '⭐ Favoriten'); case 'Downloads': return _downloadsSegment(); default: return Column( children: [ TagLeiste( tags: _vm.tags, aktiveTags: _vm.aktiveTags, onTagToggled: _vm.toggleTag, ), Expanded( child: _songListe(titel: '📂 Alle Songs'), ), ], ); } } /// Gruppiert Songs nach einem Schlüssel (Album, Künstler, Jahr, Genre) Map> _gruppiereNach(String Function(Song) schluessel) { final gruppen = >{}; for (final s in _vm.songs) { final key = schluessel(s).trim(); if (key.isEmpty) continue; gruppen.putIfAbsent(key, () => []).add(s); } return gruppen; } /// Expandierbare Gruppen-Liste (Alben / Künstler / Jahre / Genres) Widget _gruppenListe(Map> gruppen) { final keys = gruppen.keys.toList()..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); if (keys.isEmpty) { return const Center( child: Text('Keine Einträge', style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 13)), ); } return ListView( padding: const EdgeInsets.fromLTRB(12, 4, 12, 8), children: keys.map((key) { final songs = gruppen[key]!; return Container( margin: const EdgeInsets.only(bottom: 6), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), border: Border.all(color: MeloTheme.dunkel2), ), child: Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( tilePadding: const EdgeInsets.symmetric(horizontal: 14), leading: const Icon(Icons.folder_outlined, color: MeloTheme.rot, size: 18), title: Text( key, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), subtitle: Text( '${songs.length} Song${songs.length != 1 ? 's' : ''}', style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 11), ), childrenPadding: const EdgeInsets.only(bottom: 6), children: songs.map((s) => SongTile( song: s, istFavorit: s.id != null && _vm.favoritenIds.contains(s.id), onFavoriteToggle: _vm.favoritenUmschalten, onPlay: _vm.spieleSong, onMetadataChanged: _vm.ladeSongs, onAddToPlaylist: _zeigeAddToPlaylist, onErneutHerunterladen: _erneutHerunterladen, onSpieleAlsNaechstes: (song) { _vm.spieleAlsNaechstes(song); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('▶ Als Nächstes: ${song.titel}')), ); }, onAmEndeHinzufuegen: (song) { _vm.amEndeHinzufuegen(song); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('→ Am Ende hinzugefügt: ${song.titel}')), ); }, )).toList(), ), ), ); }).toList(), ); } /// 🏷 Tags-Segment: Tag-Liste mit Zählern; Tipp filtert die Songliste Widget _tagsSegment() { final eintraege = _vm.tagCounts.entries.toList() ..sort((a, b) => b.value.compareTo(a.value)); if (eintraege.isEmpty) { return const Center( child: Text('Noch keine Tags', style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 13)), ); } return ListView( padding: const EdgeInsets.all(12), children: [ ...eintraege.map((e) => Padding( padding: const EdgeInsets.only(bottom: 6), child: Material( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () { _vm.aktiveTags = {e.key}; setState(() => _bibSegment = 'Songs'); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: MeloTheme.dunkel2), ), child: Row( children: [ const Icon(Icons.label_outline, color: MeloTheme.rot, size: 18), const SizedBox(width: 12), Expanded( child: Text( e.key, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), ), Text( '${e.value} Song${e.value != 1 ? 's' : ''}', style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12), ), const SizedBox(width: 6), const Icon(Icons.chevron_right, color: MeloTheme.textSekundaer, size: 18), ], ), ), ), ), )), ], ); } /// 📋 Playlists-Segment: Playlist-Liste, Tipp zeigt die Songs Widget _playlistsSegment() { return FutureBuilder>( future: _vm.playlists.allePlaylists(), builder: (context, snapshot) { if (snapshot.connectionState != ConnectionState.done) { return const Center(child: CircularProgressIndicator(color: MeloTheme.rot)); } final playlists = snapshot.data ?? []; if (playlists.isEmpty) { return const Center( child: Text( 'Noch keine Playlists\nTippe in der Songliste auf + um eine zu erstellen', textAlign: TextAlign.center, style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 13), ), ); } return ListView( padding: const EdgeInsets.all(12), children: playlists.map((pl) => Padding( padding: const EdgeInsets.only(bottom: 6), child: Material( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), child: InkWell( borderRadius: BorderRadius.circular(12), onTap: () => _zeigePlaylistSongs(pl), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 13), decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), border: Border.all(color: MeloTheme.dunkel2), ), child: Row( children: [ const Icon(Icons.queue_music, color: MeloTheme.rot, size: 18), const SizedBox(width: 12), Expanded( child: Text( pl.name, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), ), Text( '${pl.songCount} Songs', style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12), ), const SizedBox(width: 6), const Icon(Icons.chevron_right, color: MeloTheme.textSekundaer, size: 18), ], ), ), ), ), )).toList(), ); }, ); } Future _zeigePlaylistSongs(Playlist pl) async { final songs = await _vm.playlists.songs(pl.id!); if (!mounted) return; showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: Text('📋 ${pl.name}', style: const TextStyle(color: Colors.white, fontSize: 17)), content: SizedBox( width: double.maxFinite, height: 360, child: songs.isEmpty ? const Center(child: Text('Leere Playlist', style: TextStyle(color: MeloTheme.textSekundaer))) : ListView.builder( itemCount: songs.length, itemBuilder: (_, i) => ListTile( leading: const Icon(Icons.music_note, color: MeloTheme.rot, size: 18), title: Text(songs[i].titel, style: const TextStyle(color: Colors.white, fontSize: 14)), subtitle: Text(songs[i].kuenstler, style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 11)), onTap: () { Navigator.pop(ctx); _vm.spieleSong(songs[i], warteschlange: songs); }, ), ), ), actions: [TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Schließen'))], ), ); } /// ⬇ Downloads-Segment: lokale Songs + „Lied+“-Einstieg Widget _downloadsSegment() { final downloads = _vm.songs.where((s) => s.istHeruntergeladen).toList(); return Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 4), child: GestureDetector( onTap: _zeigeDownloadDialog, child: Container( width: double.infinity, padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( gradient: const LinearGradient(colors: [Color(0xFF3A0000), Color(0xFF1A0000)]), borderRadius: BorderRadius.circular(14), border: Border.all(color: MeloTheme.rot.withValues(alpha: 0.45)), ), child: const Row( children: [ Icon(Icons.add_circle_outline, color: MeloTheme.rot, size: 22), SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text('⬇ Lied +', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w700, color: Colors.white)), Text('Neuen Song von YouTube herunterladen', style: TextStyle(fontSize: 11, color: MeloTheme.textSekundaer)), ], ), ), Icon(Icons.chevron_right, color: MeloTheme.rot, size: 20), ], ), ), ), ), Expanded( child: _songListe(quelle: downloads, titel: '📂 Downloads'), ), ], ); } // ─── Tab: ▶️ Jetzt läuft ──────────────────────────── Widget _jetztLaeuftTab() { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Icon(Icons.play_circle_outline, size: 64, color: MeloTheme.dunkel2), const SizedBox(height: 16), const Text( 'Nichts spielt gerade', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white), ), const SizedBox(height: 6), const Text( 'Wähle einen Song in deiner Bibliothek,\ndann findest du ihn hier.', textAlign: TextAlign.center, style: TextStyle(fontSize: 13, color: MeloTheme.textSekundaer), ), const SizedBox(height: 20), ElevatedButton( onPressed: () => setState(() => _aktiverTab = 1), style: ElevatedButton.styleFrom( backgroundColor: MeloTheme.rot, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), ), child: const Text('Zur Bibliothek'), ), ], ), ); } // ─── Tab: ⚙️ Mehr ─────────────────────────────────── Widget _mehrTab() { return Column( children: [ const Padding( padding: EdgeInsets.fromLTRB(20, 12, 20, 4), child: Align( alignment: Alignment.centerLeft, child: Text( '⚙️ Mehr', style: TextStyle(fontSize: 22, fontWeight: FontWeight.w700, color: Colors.white), ), ), ), Expanded( child: ListView( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), children: [ _mehrKachel( icon: Icons.settings_outlined, titel: 'Einstellungen', untertitel: 'Cloud Sync, Musikserver, Recap & mehr', onTap: _oeffneEinstellungen, ), _mehrKachel( icon: Icons.handyman_outlined, titel: 'Erweitert', untertitel: 'Logs, Diagnose, Entwickler & Scanner', onTap: () { Navigator.push( context, MaterialPageRoute( builder: (_) => ErweitertScreen(onScan: _scanMusik), ), ); }, ), const SizedBox(height: 8), _sektionHeader('Info'), Container( decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(14), border: Border.all(color: MeloTheme.dunkel2), ), child: Column( children: [ _infoZeile('Version', '2.52.3'), const Divider(color: MeloTheme.dunkel2, height: 1), _infoZeile('Theme', 'Schwarz + Rot'), const Divider(color: MeloTheme.dunkel2, height: 1), _infoZeile('Musikserver', AppConfig.navidromeUrl), const Divider(color: MeloTheme.dunkel2, height: 1), _infoZeile('Cloud', AppConfig.cloudUrl), ], ), ), const SizedBox(height: 24), SizedBox( width: double.infinity, height: 48, child: OutlinedButton.icon( onPressed: () async { await AuthService().logout(); if (mounted) { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const LoginScreen()), ); } }, icon: const Icon(Icons.logout, size: 18), label: const Text('Abmelden', style: TextStyle(fontSize: 14)), style: OutlinedButton.styleFrom( foregroundColor: MeloTheme.rot, side: const BorderSide(color: MeloTheme.rot), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), ), ), const SizedBox(height: 24), ], ), ), const MiniPlayer(), const SizedBox(height: 8), ], ); } Widget _mehrKachel({ required IconData icon, required String titel, required String untertitel, required VoidCallback onTap, }) { return Padding( padding: const EdgeInsets.only(bottom: 8), child: Material( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(14), child: InkWell( onTap: onTap, borderRadius: BorderRadius.circular(14), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( borderRadius: BorderRadius.circular(14), border: Border.all(color: MeloTheme.dunkel2), ), child: Row( children: [ Container( width: 40, height: 40, decoration: BoxDecoration( color: MeloTheme.dunkel2, borderRadius: BorderRadius.circular(10), ), child: Icon(icon, color: MeloTheme.rot, size: 20), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( titel, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), ), const SizedBox(height: 2), Text( untertitel, style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 12), ), ], ), ), const Icon(Icons.chevron_right, color: MeloTheme.textSekundaer, size: 20), ], ), ), ), ), ); } Widget _sektionHeader(String titel) { return Padding( padding: const EdgeInsets.fromLTRB(4, 16, 4, 8), child: Text( titel, style: const TextStyle( color: MeloTheme.textSekundaer, fontSize: 11, fontWeight: FontWeight.w600, letterSpacing: 1.2, ), ), ); } Widget _infoZeile(String label, String wert) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), child: Row( children: [ Text( label, style: const TextStyle(color: MeloTheme.textSekundaer, fontSize: 13), ), const Spacer(), Flexible( child: Text( wert, textAlign: TextAlign.right, style: const TextStyle(color: Colors.white70, fontSize: 13), overflow: TextOverflow.ellipsis, ), ), ], ), ); } // ─── Einstellungen ────────────────────────────────── Future _oeffneEinstellungen() async { final result = await Navigator.push( context, MaterialPageRoute(builder: (_) => const SettingsScreen()), ); if (!mounted || result == null) return; switch (result) { case 'server': _zeigeServerBrowser(); break; case 'scanner': _scanMusik(); break; case 'logout': await AuthService().logout(); if (mounted) { Navigator.of(context).pushReplacement( MaterialPageRoute(builder: (_) => const LoginScreen()), ); } break; } } // ─── NavigationBar ────────────────────────────────── Widget _bottomNav() { return Container( decoration: const BoxDecoration( border: Border(top: BorderSide(color: MeloTheme.dunkel1)), ), child: NavigationBar( backgroundColor: MeloTheme.schwarz, indicatorColor: MeloTheme.rot.withValues(alpha: 0.25), selectedIndex: _aktiverTab, onDestinationSelected: (i) { if (i == 2) { // ▶️ Jetzt läuft: Fullscreen öffnen, wenn etwas spielt if (PlayerService().aktuellerSong != null) { _oeffneNowPlaying(); return; } setState(() => _aktiverTab = 2); return; } setState(() => _aktiverTab = i); }, destinations: const [ NavigationDestination(icon: Icon(Icons.home_outlined, size: 22), selectedIcon: Icon(Icons.home, size: 22), label: 'Start'), NavigationDestination(icon: Icon(Icons.library_music_outlined, size: 22), selectedIcon: Icon(Icons.library_music, size: 22), label: 'Bibliothek'), NavigationDestination(icon: Icon(Icons.play_circle_outline, size: 22), selectedIcon: Icon(Icons.play_circle_fill, size: 22), label: 'Jetzt läuft'), NavigationDestination(icon: Icon(Icons.cloud_outlined, size: 22), selectedIcon: Icon(Icons.cloud, size: 22), label: 'Cloud'), NavigationDestination(icon: Icon(Icons.more_horiz, size: 22), label: 'Mehr'), ], ), ); } // ─── Songliste ────────────────────────────────────── Widget _songListe({List? quelle, String titel = '📂 Alle Songs'}) { final songs = quelle ?? _vm.gefilterteSongs; return Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 12, 20, 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text(titel, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white)), Row(children: [ Text('${songs.length} Titel${_vm.aktiveTags.isNotEmpty ? ' (gefiltert)' : ''}', style: TextStyle(fontSize: 12, color: MeloTheme.rot)), const SizedBox(width: 8), GestureDetector( onTap: _scanMusik, child: Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( border: Border.all(color: MeloTheme.dunkel2), borderRadius: BorderRadius.circular(8), ), child: const Row( mainAxisSize: MainAxisSize.min, children: [ Icon(Icons.refresh, size: 12, color: MeloTheme.rot), SizedBox(width: 4), Text('Scannen', style: TextStyle(fontSize: 11, color: MeloTheme.rot)), ], ), ), ), ]), ], ), ), Expanded( child: ListView.builder( padding: const EdgeInsets.fromLTRB(20, 4, 20, 4), itemCount: songs.length, itemBuilder: (_, i) => SongTile( song: songs[i], istFavorit: songs[i].id != null && _vm.favoritenIds.contains(songs[i].id), onFavoriteToggle: _vm.favoritenUmschalten, onPlay: _vm.spieleSong, onMetadataChanged: _vm.ladeSongs, onAddToPlaylist: _zeigeAddToPlaylist, onErneutHerunterladen: _erneutHerunterladen, onSpieleAlsNaechstes: (song) { _vm.spieleAlsNaechstes(song); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('▶ Als Nächstes: ${song.titel}')), ); }, onAmEndeHinzufuegen: (song) { _vm.amEndeHinzufuegen(song); ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('→ Am Ende hinzugefügt: ${song.titel}')), ); }, ), ), ), ], ); } /// Hilfs-Widget für Such-Modus-Chips Widget _suchChip(String label, bool aktiv, VoidCallback onTap) { return GestureDetector( onTap: onTap, child: Container( padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4), decoration: BoxDecoration( color: aktiv ? MeloTheme.rot : MeloTheme.dunkel2, borderRadius: BorderRadius.circular(12), ), child: Text(label, style: TextStyle(fontSize: 11, color: aktiv ? Colors.white : MeloTheme.textSekundaer)), ), ); } /// Karte „Jahres-Recap“ – öffnet den Wrapped-artigen Recap-Screen Widget _recapKarte() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), child: GestureDetector( onTap: () { Navigator.push( context, MaterialPageRoute(builder: (_) => const RecapScreen()), ); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF3A0000), Color(0xFF1A0000), Color(0xFF0D0D0D)], begin: Alignment.topLeft, end: Alignment.bottomRight, ), borderRadius: BorderRadius.circular(14), border: Border.all(color: MeloTheme.rot.withValues(alpha: 0.45)), ), child: Row( children: [ Container( width: 42, height: 42, decoration: BoxDecoration( color: MeloTheme.rot.withValues(alpha: 0.18), borderRadius: BorderRadius.circular(12), ), child: const Center( child: Text('🎧', style: TextStyle(fontSize: 20)), ), ), const SizedBox(width: 14), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( 'Recap ${DateTime.now().year}', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w700, color: Colors.white, ), ), const SizedBox(height: 2), const Text( 'Woche · Monat · Jahr – deine Top-Songs, Künstler & Hörzeit', maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle(fontSize: 11, color: MeloTheme.textSekundaer), ), ], ), ), const Icon(Icons.chevron_right, color: MeloTheme.rot, size: 22), ], ), ), ), ); } /// Banner "💌 Seit 2008" – erscheint nach 10 Playbacks Widget _botschaftBanner() { return Padding( padding: const EdgeInsets.symmetric(horizontal: 20), child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( gradient: const LinearGradient(colors: [Color(0xFF2A0000), Color(0xFF1A0000)]), borderRadius: BorderRadius.circular(12), border: Border.all(color: MeloTheme.rot.withValues(alpha: 0.4)), ), child: Row( children: [ const Text('💌', style: TextStyle(fontSize: 20)), const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: const [ Text('Seit 2008', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w700, color: Colors.white)), Text('Danke, dass du immer da bist ♥', style: TextStyle(fontSize: 11, color: MeloTheme.textSekundaer)), ], ), ), GestureDetector( onTap: _vm.botschaftAusblenden, child: const Icon(Icons.close, size: 16, color: MeloTheme.textSekundaer), ), ], ), ), ); } }