import 'package:flutter/material.dart'; import '../models/song.dart'; import '../models/playlist.dart'; import '../utils/farb_theme.dart'; import '../viewmodels/melo_home_viewmodel.dart'; /// Bottom-Sheet zum Durchstöbern von Playlists. class PlaylistSheet extends StatefulWidget { final MeloHomeViewModel vm; const PlaylistSheet({super.key, required this.vm}); @override State createState() => _PlaylistSheetState(); } class _PlaylistSheetState extends State { List _playlists = []; bool _ladt = true; @override void initState() { super.initState(); _laden(); } Future _laden() async { _ladt = true; setState(() {}); _playlists = await widget.vm.playlists.allePlaylists(); _ladt = false; setState(() {}); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(20, 12, 20, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ // Handle Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: MeloTheme.dunkel2, borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 12), // Header Row( children: [ const Icon(Icons.queue_music, size: 18, color: Colors.white), const SizedBox(width: 6), const Text('Playlists', style: TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white)), const Spacer(), _btn('+ Neu', _neuePlaylist), ], ), const SizedBox(height: 12), if (_ladt) const Expanded(child: Center(child: CircularProgressIndicator(color: MeloTheme.rot, strokeWidth: 2))) else if (_playlists.isEmpty) const Expanded(child: Center(child: Text('Noch keine Playlists\nTippe oben auf "+ Neu"', textAlign: TextAlign.center, style: TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)))) else Expanded(child: _listView()), ], ), ); } Widget _listView() { return RefreshIndicator( color: MeloTheme.rot, onRefresh: _laden, child: ListView.builder( itemCount: _playlists.length, itemBuilder: (_, i) => _playlistTile(_playlists[i]), ), ); } Widget _playlistTile(Playlist p) { return Container( margin: const EdgeInsets.only(bottom: 6), decoration: BoxDecoration( color: MeloTheme.dunkel1, borderRadius: BorderRadius.circular(12), ), child: ListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), leading: Container( width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(10), color: MeloTheme.rot.withValues(alpha: 0.3), ), child: const Center(child: Text('🎵', style: TextStyle(fontSize: 16))), ), title: Text(p.name, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white)), subtitle: Text('${p.songCount} Songs · ${p.erstelltAm.substring(0, 10)}', style: const TextStyle(fontSize: 11, color: MeloTheme.textSekundaer)), trailing: const Icon(Icons.chevron_right, size: 18, color: MeloTheme.textSekundaer), onTap: () => _playlistOffnen(context, p), ), ); } void _playlistOffnen(BuildContext context, Playlist p) async { final songs = await widget.vm.playlists.songs(p.id!); if (!context.mounted) return; showModalBottomSheet( context: context, backgroundColor: MeloTheme.schwarz, isScrollControlled: true, builder: (_) => SizedBox( height: MediaQuery.of(context).size.height * 0.6, child: _PlaylistDetail(p: p, initialSongs: songs, vm: widget.vm, onChanged: _laden), ), ); } void _neuePlaylist() { final ctrl = TextEditingController(); showDialog( context: context, builder: (ctx) => AlertDialog( backgroundColor: MeloTheme.dunkel1, title: const Text('Neue Playlist', style: TextStyle(color: Colors.white, fontSize: 16)), content: TextField( controller: ctrl, autofocus: true, style: const TextStyle(color: Colors.white), decoration: const InputDecoration( hintText: 'Name...', hintStyle: TextStyle(color: Colors.grey), border: OutlineInputBorder(), ), ), actions: [ TextButton(onPressed: () => Navigator.pop(ctx), child: const Text('Abbrechen')), TextButton( onPressed: () async { if (ctrl.text.trim().isEmpty) return; await widget.vm.playlists.erstellen(ctrl.text.trim()); if (ctx.mounted) Navigator.pop(ctx); _laden(); }, child: const Text('Erstellen', style: TextStyle(color: MeloTheme.rot)), ), ], ), ); } Widget _btn(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)), ), ); } } /// Detailansicht einer Playlist mit Songs class _PlaylistDetail extends StatefulWidget { final Playlist p; final List initialSongs; final MeloHomeViewModel vm; final VoidCallback onChanged; const _PlaylistDetail({ required this.p, required this.initialSongs, required this.vm, required this.onChanged, }); @override State<_PlaylistDetail> createState() => _PlaylistDetailState(); } class _PlaylistDetailState extends State<_PlaylistDetail> { late List _songs; @override void initState() { super.initState(); _songs = List.from(widget.initialSongs); } @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.fromLTRB(20, 12, 20, 0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Center( child: Container( width: 40, height: 4, decoration: BoxDecoration( color: MeloTheme.dunkel2, borderRadius: BorderRadius.circular(2), ), ), ), const SizedBox(height: 12), Row( children: [ GestureDetector( onTap: () => Navigator.pop(context), child: const Icon(Icons.arrow_back, size: 20, color: MeloTheme.rot), ), const SizedBox(width: 8), Text(widget.p.name, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white)), const Spacer(), Text('${_songs.length} Songs', style: const TextStyle(fontSize: 12, color: MeloTheme.textSekundaer)), ], ), const SizedBox(height: 12), Expanded( child: _songs.isEmpty ? const Center(child: Text('Playlist ist leer', style: TextStyle(fontSize: 12, color: MeloTheme.textSekundaer))) : ReorderableListView.builder( itemCount: _songs.length, onReorderItem: (oldIndex, newIndex) async { if (newIndex > oldIndex) newIndex--; setState(() { final moved = _songs.removeAt(oldIndex); _songs.insert(newIndex, moved); }); if (widget.p.id != null) { await widget.vm.playlists.reihenfolgeSpeichern(widget.p.id!, _songs); } widget.onChanged(); }, itemBuilder: (_, i) => _songTile(context, _songs[i]), ), ), ], ), ); } Widget _songTile(BuildContext context, Song song) { return Container( key: ValueKey(song.id ?? song.dateiPfad), margin: const EdgeInsets.only(bottom: 4), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), 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: GestureDetector( onTap: () { Navigator.pop(context); widget.vm.spieleSong(song, warteschlange: _songs); }, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(song.titel, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: Colors.white), overflow: TextOverflow.ellipsis), Text(song.kuenstler, style: const TextStyle(fontSize: 10, color: MeloTheme.textSekundaer)), ], ), ), ), GestureDetector( onTap: () async { await widget.vm.playlists.songEntfernen(widget.p.id!, song.id!); setState(() { _songs.removeWhere((s) => s.id == song.id); }); widget.onChanged(); }, child: const Icon(Icons.remove_circle_outline, size: 18, color: MeloTheme.textSekundaer), ), ], ), ); } }