import 'dart:async'; import 'dart:io'; import 'package:flutter/material.dart'; import '../database/db_helper.dart'; import '../viewmodels/melo_home_viewmodel.dart'; import '../models/song.dart'; import '../models/playlist.dart'; import '../utils/farb_theme.dart'; import '../utils/user_effekte.dart'; import '../services/cloud_service.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/navidrome_browser.dart'; import '../widgets/playlist_sheet.dart'; import 'download_screen.dart'; import 'cloud_screen.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:permission_handler/permission_handler.dart'; import '../widgets/cloud_einstellungen.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(); int _aktiverTab = 0; String _nutzer = 'Baka'; // Aktueller Nutzer @override void initState() { super.initState(); _ladeNutzer(); _vm.addListener(() => setState(() {})); _vm.ladeSongs(); } Future _ladeNutzer() async { final p = await SharedPreferences.getInstance(); final n = p.getString('melo_nutzer') ?? ''; if (n.isNotEmpty && mounted) { setState(() => _nutzer = n); _vm.setzeNutzer(n); } // Erster Start? → einmalig Modus wählen (Cloud oder nur lokal) – Login ist freiwillig if (!p.containsKey('melo_modus')) { await _zeigeModusWahl(); return; } // Login-Effekt beim App-Start: nur wenn ein Token vorhanden ist (eingeloggt) final ok = await CloudService().restoreLogin(); if (ok && mounted) { UserEffekt.anwenden(_nutzer); ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(UserEffekt.fuer(_nutzer).begruessung), duration: const Duration(seconds: 3), )); } } /// Einmalige Auswahl beim ersten App-Start: Cloud-Sync oder nur lokal. Future _zeigeModusWahl() async { if (!mounted) return; final cloud = await showDialog( context: context, barrierDismissible: false, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('👋 Willkommen bei Melo!', style: TextStyle(color: Colors.white, fontSize: 16)), content: const Text( 'Wie möchtest du Melo nutzen?', style: TextStyle(color: Colors.white, fontSize: 14), ), actions: [ TextButton( onPressed: () => Navigator.pop(ctx, false), child: const Text('📱 Nur lokal', style: TextStyle(color: Colors.white)), ), TextButton( onPressed: () => Navigator.pop(ctx, true), child: const Text('☁️ Cloud (empfohlen)', style: TextStyle(color: MeloTheme.rot, fontWeight: FontWeight.bold)), ), ], ), ); final p = await SharedPreferences.getInstance(); await p.setString('melo_modus', cloud == true ? 'cloud' : 'lokal'); // Cloud gewählt → direkt zum Login-Dialog (Name + Passwort) if (cloud == true && mounted) { _nutzerWechseln(); } } Future _zeigeProfil() async { final p = await SharedPreferences.getInstance(); final modus = p.getString('melo_modus') ?? 'cloud'; if (!mounted) return; showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: Row(children: [ const Icon(Icons.person, color: MeloTheme.rot, size: 22), const SizedBox(width: 8), Text(_nutzer, style: const TextStyle(color: Colors.white, fontSize: 17)), ]), content: Column( mainAxisSize: MainAxisSize.min, children: [ _profilZeile(Icons.music_note, 'Lieder auf Gerät', '${_vm.songs.length}'), _profilZeile(Icons.cloud, 'Cloud-Server', '${_cloud.status().then((s) => s?['total'] ?? 0)}'), const Divider(color: MeloTheme.dunkel2), // Später von "nur lokal" auf Cloud wechseln – jederzeit möglich if (modus == 'lokal') ...[ ListTile( leading: const Icon(Icons.cloud_upload, color: Colors.blueAccent, size: 18), title: const Text('☁️ Cloud aktivieren', style: TextStyle(color: Colors.white, fontSize: 13)), subtitle: const Text('Musik sichern & geräteübergreifend nutzen', style: TextStyle(color: Colors.grey, fontSize: 11)), onTap: () async { final prefs = await SharedPreferences.getInstance(); await prefs.setString('melo_modus', 'cloud'); if (ctx.mounted) Navigator.pop(ctx); _nutzerWechseln(); }, ), const Divider(color: MeloTheme.dunkel2), ], ListTile( leading: const Icon(Icons.swap_horiz, color: Colors.grey, size: 18), title: const Text('Nutzer wechseln', style: TextStyle(color: Colors.white, fontSize: 13)), onTap: () { Navigator.pop(ctx); _nutzerWechseln(); }, ), ListTile( leading: const Icon(Icons.logout, color: Colors.red, size: 18), title: const Text('Abmelden', style: TextStyle(color: Colors.red, fontSize: 13)), onTap: () async { // Token wirklich löschen, sonst wäre man gar nicht abgemeldet await CloudService().logout(); final prefs = await SharedPreferences.getInstance(); await prefs.remove('melo_nutzer'); if (ctx.mounted) Navigator.pop(ctx); if (mounted) setState(() => _nutzer = ''); }, ), ], ), ), ); } Widget _profilZeile(IconData icon, String label, String wert) { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Row(children: [ Icon(icon, color: MeloTheme.rot, size: 16), const SizedBox(width: 8), Expanded(child: Text(label, style: const TextStyle(color: Colors.grey, fontSize: 12))), Text(wert, style: const TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w600)), ]), ); } void _nutzerWechseln() { final ctrl = TextEditingController(text: _nutzer); final pwCtrl = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('👤 Anmelden', style: TextStyle(color: Colors.white, fontSize: 15)), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( controller: ctrl, autofocus: true, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( hintText: 'Nutzername...', hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder(), ), ), const SizedBox(height: 8), TextField( controller: pwCtrl, obscureText: true, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( hintText: 'Passwort...', hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder(), ), ), ], ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen')), TextButton( onPressed: () async { final name = ctrl.text.trim(); final pass = pwCtrl.text; if (name.isEmpty || pass.isEmpty) return; final ok = await _vm.cloud.login(name, pass); if (!ctx.mounted) return; Navigator.pop(ctx); if (ok) { final p = await SharedPreferences.getInstance(); await p.setString('melo_nutzer', name); if (mounted) setState(() => _nutzer = name); _vm.setzeNutzer(name); // Login-Effekt: Akzentfarbe + Sound + Begrüßung pro Person UserEffekt.anwenden(name); if (mounted) { ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text(UserEffekt.fuer(name).begruessung), duration: const Duration(seconds: 3))); } } else { if (mounted) { ScaffoldMessenger.of(context).showSnackBar(const SnackBar( content: Text('❌ Login fehlgeschlagen – Name oder Passwort falsch'))); } } }, child: const Text('OK', style: TextStyle(color: MeloTheme.rot)), ), ], ), ); } @override void dispose() { _vm.dispose(); super.dispose(); } 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)), ), ], ), ), ); 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(); } } void _zeigeServerBrowser() { if (_vm.navidrome.istVerbunden) { // Abmelden _vm.navidrome.setCredentials('', '', ''); _vm.navidromeAlben.clear(); 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('🌐 Navidrome 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)), ), ], ), ); } void _zeigePlaylistSheet() { showModalBottomSheet( context: context, backgroundColor: MeloTheme.schwarz, isScrollControlled: true, builder: (_) => SizedBox( height: MediaQuery.of(context).size.height * 0.7, child: PlaylistSheet(vm: _vm), ), ); } void _songLoeschen(Song song) async { if (song.id != null) { final db = DbHelper(); await db.loeschSong(song.id!); if (song.dateiPfad.isNotEmpty) { try { await File(song.dateiPfad).delete(); } catch (_) {} } _vm.ladeSongs(); } } void _zeigePlaylists() { showModalBottomSheet( context: context, backgroundColor: MeloTheme.schwarz, isScrollControlled: true, builder: (_) => SizedBox( height: MediaQuery.of(context).size.height * 0.7, child: PlaylistSheet(vm: _vm), ), ); } 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}')), ); } } } Future _zeigeDownloadDialog() async { // Statt Dialog → zum Download-Tab wechseln setState(() => _aktiverTab = 1); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _vm, builder: (_, _) { if (_vm.ladt) { return const Scaffold( backgroundColor: MeloTheme.schwarz, body: Center(child: CircularProgressIndicator(color: MeloTheme.rot)), ); } final gesamtMB = _vm.songs.isEmpty ? '0.0' : (_vm.songs.fold(0, (int s, Song song) => s + song.groesseBytes) / 1048576).toStringAsFixed(1); final gesamtMin = _vm.songs.isEmpty ? 0 : (_vm.songs.fold(0, (int s, Song song) => s + song.dauerSekunden) / 60).round(); return Scaffold( backgroundColor: MeloTheme.schwarz, body: SafeArea( child: _aktiverTab == 1 ? DownloadScreen( downloader: _vm.downloader, onSongsChanged: _vm.ladeSongs, ) : _aktiverTab == 3 ? CloudScreen(cloud: _cloud, onSongsChanged: _vm.ladeSongs) : Column( children: [ MeloHeader(onDownload: _zeigeDownloadDialog, onSearch: _zeigeSuche, onServer: _zeigeProfil, onSettings: _zeigeEinstellungen), if (_vm.zeigeBotschaft) _botschaftBanner(), _tagBereich(), Expanded(child: _songListe()), const MiniPlayer(), const SizedBox(height: 8), ], ), ), bottomNavigationBar: _bottomNav(), ); }, ); } Widget _songListe() { final songs = _vm.gefilterteSongs; return Column( children: [ Padding( padding: const EdgeInsets.fromLTRB(20, 12, 20, 4), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('📂 Alle Songs', style: 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: songs.isEmpty ? _emptyStateWidget() : 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, onDelete: _songLoeschen, ), ), ), ], ); } void _zeigeEinstellungen() { showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('⚙ Einstellungen', style: TextStyle(color: Colors.white, fontSize: 16)), content: Column( mainAxisSize: MainAxisSize.min, children: [ // Stats Container( padding: const EdgeInsets.all(12), margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration(color: MeloTheme.dunkel2, borderRadius: BorderRadius.circular(10)), child: Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _statWert('${_vm.songs.length}', 'Songs'), _statWert('${(_vm.songs.fold(0, (int s, Song song) => s + song.groesseBytes) / 1048576).toStringAsFixed(1)} MB', 'Größe'), _statWert('${(_vm.songs.fold(0, (int s, Song song) => s + song.dauerSekunden) / 60).round()} Min', 'Dauer'), _statWert('${_vm.favoritenIds.length}', '❤️'), ]), ), ListTile( leading: const Icon(Icons.favorite, color: MeloTheme.rot), title: const Text('Favoriten', style: TextStyle(color: Colors.white)), subtitle: const Text('Deine Lieblingssongs', style: TextStyle(color: Colors.grey, fontSize: 12)), onTap: () { Navigator.pop(ctx); _vm.aktiveTags = {'★ Favoriten'}; }, ), ListTile( leading: const Icon(Icons.cloud, color: MeloTheme.rot), title: const Text('Cloud Sync', style: TextStyle(color: Colors.white)), subtitle: const Text('Auto-Sync & Einstellungen', style: TextStyle(color: Colors.grey, fontSize: 12)), onTap: () { Navigator.pop(ctx); showDialog( context: context, builder: (_) => const CloudEinstellungen(), ); }, ), ListTile( leading: const Icon(Icons.dns, color: MeloTheme.rot), title: const Text('Server verbinden', style: TextStyle(color: Colors.white)), subtitle: const Text('Navidrome / Musik-Server', style: TextStyle(color: Colors.grey, fontSize: 12)), onTap: () { Navigator.pop(ctx); _zeigeServerBrowser(); }, ), ListTile( leading: const Icon(Icons.person, color: MeloTheme.rot), title: const Text('Profil', style: TextStyle(color: Colors.white)), subtitle: Text(_nutzer.isEmpty ? 'Nicht angemeldet' : _nutzer, style: const TextStyle(color: Colors.grey, fontSize: 12)), onTap: () { Navigator.pop(ctx); _zeigeProfil(); }, ), SwitchListTile( title: const Text('Diagnosedaten senden', style: TextStyle(color: Colors.white, fontSize: 13)), subtitle: const Text('Absturz-Logs & Nutzungsdaten', style: TextStyle(color: Colors.grey, fontSize: 11)), value: AppConfig.sendeDiagnosedaten, activeColor: MeloTheme.rot, onChanged: (v) { AppConfig.sendeDiagnosedaten = v; }, ), if (Platform.isAndroid) ListTile( leading: const Icon(Icons.folder_open, color: MeloTheme.rot), title: const Text('Voller Speicherzugriff', style: TextStyle(color: Colors.white)), subtitle: const Text('Zum Speichern in Downloads/Music', style: TextStyle(color: Colors.grey, fontSize: 12)), onTap: () async { final status = await Permission.manageExternalStorage.request(); if (status.isGranted) { await SharedPreferences.getInstance().then((p) => p.setBool('manage_storage', true)); } }, ), ], ), ), ); } Widget _bottomNav() { return Container( decoration: const BoxDecoration( border: Border(top: BorderSide(color: MeloTheme.dunkel1)), ), child: BottomNavigationBar( type: BottomNavigationBarType.fixed, backgroundColor: MeloTheme.schwarz, selectedItemColor: MeloTheme.rot, unselectedItemColor: MeloTheme.textSekundaer, currentIndex: _aktiverTab.clamp(0, 3), onTap: (i) { setState(() => _aktiverTab = i); if (i == 0 && _vm.aktiveTags.contains('★ Favoriten')) { _vm.aktiveTags.clear(); } else if (i == 2) { // Playlisten öffnen _zeigePlaylists(); } }, items: const [ BottomNavigationBarItem(icon: Icon(Icons.music_note, size: 22), label: 'Musik'), BottomNavigationBarItem(icon: Icon(Icons.add_circle, size: 22), label: 'Lied +'), BottomNavigationBarItem(icon: Icon(Icons.queue_music, size: 22), label: 'Playlisten'), BottomNavigationBarItem(icon: Icon(Icons.cloud, size: 22), label: 'Cloud'), ], ), ); } /// 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)), ), ); } /// 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: Text(_vm.nutzerBotschaft ?? '🎵 Danke fürs Zuhören!', style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600, color: Colors.white)), ), GestureDetector( onTap: _vm.botschaftAusblenden, child: const Icon(Icons.close, size: 16, color: MeloTheme.textSekundaer), ), ], ), ), ); } /// Leerer Zustand – wenn keine Songs in der Bibliothek sind Widget _emptyStateWidget() { return Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Icon(Icons.library_music_outlined, size: 56, color: MeloTheme.rot.withValues(alpha: 0.5)), const SizedBox(height: 16), const Text('Noch keine Songs in Melo', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)), const SizedBox(height: 8), const Text('Füge Songs über "Lied +" hinzu, verbinde deinen Server oder starte einen lokalen Scan.', textAlign: TextAlign.center, style: TextStyle(color: MeloTheme.textSekundaer, fontSize: 12, height: 1.4)), const SizedBox(height: 20), ElevatedButton.icon( onPressed: _scanMusik, icon: const Icon(Icons.search, size: 16), label: const Text('Jetzt Musik scannen'), style: ElevatedButton.styleFrom( backgroundColor: MeloTheme.dunkel1, foregroundColor: Colors.white, side: const BorderSide(color: MeloTheme.dunkel2), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), ), ), ], ), ), ); } Widget _tagBereich() { return AnimatedSize( duration: const Duration(milliseconds: 200), child: _tagsOffen ? TagLeiste( tags: _vm.tags, aktiveTags: _vm.aktiveTags, onTagToggled: _vm.toggleTag, ) : const SizedBox.shrink(), ); } Widget _statWert(String wert, String label) { return Column(children: [ Text(wert, style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w700)), Text(label, style: const TextStyle(color: Colors.grey, fontSize: 10)), ]); } bool _tagsOffen = false; }