import 'dart:async'; import 'package:flutter/material.dart'; import '../services/player_service.dart'; import '../models/song.dart'; import '../utils/farb_theme.dart'; /// Bottom-Sheet mit der aktuellen Warteschlange: /// aktueller Song markiert, Tipp = abspielen, Drag-and-Drop = umsortieren. class WarteschlangeSheet extends StatefulWidget { const WarteschlangeSheet({super.key}); @override State createState() => _WarteschlangeSheetState(); } class _WarteschlangeSheetState extends State { final PlayerService _player = PlayerService(); Timer? _tick; @override void initState() { super.initState(); // Sekunden-Tick für die Sleep-Timer-Restzeitanzeige _tick = Timer.periodic(const Duration(seconds: 1), (_) { if (mounted && _player.sleepTimerAktiv) setState(() {}); }); } @override void dispose() { _tick?.cancel(); super.dispose(); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _player, builder: (context, _) { // Echte Wiedergabe-Reihenfolge (Position 0 = aktuell spielender Song): // normal = Warteschlange ab aktuellem Index, Shuffle = Zufallsreihenfolge. final reihenfolge = _player.wiedergabeReihenfolge; final wiederholung = _player.wiederholmodus == Wiederholmodus.playlist; return SafeArea( child: Padding( padding: EdgeInsets.only( bottom: MediaQuery.of(context).viewInsets.bottom, ), child: ConstrainedBox( constraints: BoxConstraints( maxHeight: MediaQuery.of(context).size.height * 0.75, ), child: Column( mainAxisSize: MainAxisSize.min, children: [ // Griff Container( width: 36, height: 4, margin: const EdgeInsets.only(top: 10, bottom: 4), decoration: BoxDecoration( color: MeloTheme.dunkel2, borderRadius: BorderRadius.circular(2), ), ), // Kopfzeile Padding( padding: const EdgeInsets.fromLTRB(20, 8, 8, 4), child: Row( children: [ const Icon(Icons.queue_music, size: 18, color: MeloTheme.rot), const SizedBox(width: 8), Text( 'Warteschlange', style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white, ), ), const SizedBox(width: 8), Text( '${reihenfolge.length} Titel', style: const TextStyle( fontSize: 12, color: MeloTheme.textSekundaer, ), ), const Spacer(), IconButton( icon: const Icon(Icons.close, size: 18, color: MeloTheme.textSekundaer), onPressed: () => Navigator.pop(context), tooltip: 'Schließen', ), ], ), ), const Divider(height: 1, color: MeloTheme.dunkel2), _sleepTimerZeile(), const Divider(height: 1, color: MeloTheme.dunkel2), // Liste: aktueller Song oben (markiert, nicht verschiebbar), // danach die tatsächlichen nächsten Lieder (Drag = umsortieren) if (reihenfolge.isEmpty) const Expanded( child: Center( child: Text( 'Keine Songs in der Warteschlange', style: TextStyle(fontSize: 13, color: MeloTheme.textSekundaer), ), ), ) else ...[ Flexible( child: ReorderableListView.builder( shrinkWrap: true, padding: const EdgeInsets.fromLTRB(8, 4, 8, 4), itemCount: reihenfolge.length, // Eigene Drag-Handles: nur kommende Songs sind // verschiebbar, der aktuelle bleibt oben gepinnt. buildDefaultDragHandles: false, // onReorderItem liefert den newIndex bereits angepasst onReorderItem: (alt, neu) => _player.verschiebeInWiedergabeReihenfolge(alt, neu), itemBuilder: (_, i) { // Der Key MUSS am obersten Widget des Items liegen // (Pflicht für ReorderableListView) final key = ValueKey( 'pos$i-${reihenfolge[i].id ?? reihenfolge[i].titel}'); final eintrag = _eintrag(reihenfolge[i], i); if (i == 0) { return KeyedSubtree(key: key, child: eintrag); } return ReorderableDelayedDragStartListener( key: key, index: i, child: eintrag, ); }, ), ), if (wiederholung) _wiederholungHinweis(), ], ], ), ), ), ); }, ); } /// Hinweis am Listenende, wenn Repeat (Playlist) aktiv ist: Nach dem /// letzten Eintrag beginnt die Wiedergabe wieder von vorn (bzw. wird /// im Shuffle neu gemischt). Widget _wiederholungHinweis() { return Padding( padding: const EdgeInsets.fromLTRB(20, 6, 20, 12), child: Row( children: [ const Icon(Icons.repeat, size: 14, color: MeloTheme.rot), const SizedBox(width: 6), Expanded( child: Text( _player.zufallsmodus ? 'Wiederholung aktiv — danach wird neu gemischt' : 'Wiederholung aktiv — danach geht es von vorn weiter', style: const TextStyle( fontSize: 11, color: MeloTheme.textSekundaer, ), ), ), ], ), ); } /// Sleep-Timer-Zeile: Optionen Aus/15/30/45/60 Min + Restzeit wenn aktiv Widget _sleepTimerZeile() { final aktiv = _player.sleepTimerAktiv; final rest = _player.sleepRestzeit; return Padding( padding: const EdgeInsets.fromLTRB(20, 8, 20, 8), child: Row( children: [ Icon( Icons.bedtime, size: 16, color: aktiv ? MeloTheme.rot : MeloTheme.textSekundaer, ), const SizedBox(width: 8), const Text( 'Schlaf-Timer', style: TextStyle(fontSize: 13, color: Colors.white), ), if (aktiv && rest != null) ...[ const SizedBox(width: 8), Text( _formatRestzeit(rest), style: const TextStyle(fontSize: 12, color: MeloTheme.rot), ), ], const Spacer(), _zeitChip('Aus', null, aktiv: !aktiv), _zeitChip('15', const Duration(minutes: 15)), _zeitChip('30', const Duration(minutes: 30)), _zeitChip('45', const Duration(minutes: 45)), _zeitChip('60', const Duration(minutes: 60)), ], ), ); } Widget _zeitChip(String label, Duration? dauer, {bool aktiv = false}) { return GestureDetector( onTap: () { if (dauer == null) { _player.cancelSleepTimer(); } else { _player.setSleepTimer(dauer); } }, child: Container( margin: const EdgeInsets.only(left: 6), padding: const EdgeInsets.symmetric(horizontal: 8, 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, ), ), ), ); } String _formatRestzeit(Duration rest) { final min = rest.inMinutes; final sek = rest.inSeconds % 60; return '⏱ ${min.toString().padLeft(2, '0')}:${sek.toString().padLeft(2, '0')}'; } /// Ein Eintrag der Wiedergabe-Reihenfolge. Position 0 = aktuell spielender /// Song: deutlich markiert (▶ + Highlight-Hintergrund + „Jetzt spielt"-Chip). Widget _eintrag(Song song, int position) { final istAktuell = position == 0; return ListTile( dense: true, // tileColor direkt am ListTile statt Container-Decoration: verhindert // die „ListTile background color may be invisible"-Assertion tileColor: istAktuell ? MeloTheme.rotHell : null, shape: istAktuell ? RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)) : null, leading: istAktuell ? const Icon(Icons.play_arrow, color: MeloTheme.rot, size: 20) : SizedBox( width: 18, child: Text( '$position', // Position in der Wiedergabe-Reihenfolge textAlign: TextAlign.center, style: const TextStyle( fontSize: 11, color: MeloTheme.textSekundaer), ), ), title: istAktuell ? Row( children: [ Expanded( child: Text( song.titel, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w700, color: MeloTheme.rot, ), ), ), const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: MeloTheme.rot, borderRadius: BorderRadius.circular(10), ), child: const Text( 'Jetzt spielt', style: TextStyle( fontSize: 10, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ], ) : Text( song.titel, maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle( fontSize: 13, fontWeight: FontWeight.w400, color: Colors.white, ), ), subtitle: Text( song.kuenstler, style: const TextStyle(fontSize: 11, color: MeloTheme.textSekundaer), ), trailing: Text( song.dauerFormatiert, style: const TextStyle(fontSize: 11, color: MeloTheme.textSekundaer), ), onTap: () => _player.spiele(song), ); } }