From 5edfae01e5c44a2b3b30e17c93837b85bda9eebe Mon Sep 17 00:00:00 2001 From: Dustin Date: Tue, 4 Aug 2026 16:25:04 +0200 Subject: [PATCH] =?UTF-8?q?v2.48.2=20=E2=80=94=20Queue-UI=20(Warteschlange?= =?UTF-8?q?-Sheet)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## PlayerService - warteschlange Getter (unveränderliche Kopie) + aktuellerIndex - spieleAlsNaechstes(Song): fügt nach aktuellem Song ein - amEndeHinzufuegen(Song): hängt ans Ende an - verschiebeInWarteschlange(von, nach): Drag-and-Drop mit korrekter Anpassung des aktuellen Index ## WarteschlangeSheet (neu: lib/widgets/warteschlange_sheet.dart) - Bottom-Sheet: aktuelle Warteschlange, aktueller Song rot markiert (▶) - Tipp auf Song → sofort abspielen - ReorderableListView (Bordmittel) zum Umsortieren per Drag - Leer-Zustand + Titel-Zähler ## MiniPlayer - Tipp auf den Miniplayer öffnet die Warteschlange (Bottom-Sheet) Keine neuen Packages, flutter analyze 0 Issues, Tests 4/4 --- lib/services/player_service.dart | 45 +++++++++ lib/widgets/mini_player.dart | 36 +++++-- lib/widgets/warteschlange_sheet.dart | 145 +++++++++++++++++++++++++++ 3 files changed, 215 insertions(+), 11 deletions(-) create mode 100644 lib/widgets/warteschlange_sheet.dart diff --git a/lib/services/player_service.dart b/lib/services/player_service.dart index f759c1d..41ad637 100644 --- a/lib/services/player_service.dart +++ b/lib/services/player_service.dart @@ -256,6 +256,51 @@ class PlayerService extends ChangeNotifier { notifyListeners(); } + // ─── Warteschlange (Queue-UI) ─── + + /// Unveränderliche Kopie der Warteschlange für die Anzeige. + List get warteschlange => List.unmodifiable(_warteschlange); + + /// Index des aktuell spielenden Songs in der Warteschlange. + int get aktuellerIndex => _aktuellerIndex; + + /// Fügt [song] direkt nach dem aktuellen Song ein. + void spieleAlsNaechstes(Song song) { + if (_aktuellerIndex < 0 || _warteschlange.isEmpty) { + _warteschlange.add(song); + } else { + _warteschlange.insert(_aktuellerIndex + 1, song); + } + if (_zufallsmodus) _baueShuffleReihenfolge(); + notifyListeners(); + } + + /// Hängt [song] ans Ende der Warteschlange an. + void amEndeHinzufuegen(Song song) { + _warteschlange.add(song); + if (_zufallsmodus) _baueShuffleReihenfolge(); + notifyListeners(); + } + + /// Verschiebt einen Eintrag in der Warteschlange (Drag-and-Drop). + /// Der aktuelle Index wird entsprechend angepasst. + void verschiebeInWarteschlange(int von, int nach) { + if (von < 0 || von >= _warteschlange.length) return; + if (nach < 0 || nach >= _warteschlange.length) return; + if (von == nach) return; + final song = _warteschlange.removeAt(von); + _warteschlange.insert(nach, song); + if (_aktuellerIndex == von) { + _aktuellerIndex = nach; + } else if (von < _aktuellerIndex && nach >= _aktuellerIndex) { + _aktuellerIndex -= 1; + } else if (von > _aktuellerIndex && nach <= _aktuellerIndex) { + _aktuellerIndex += 1; + } + if (_zufallsmodus) _baueShuffleReihenfolge(); + notifyListeners(); + } + @override void dispose() { _autoNextSub?.cancel(); diff --git a/lib/widgets/mini_player.dart b/lib/widgets/mini_player.dart index 41e7081..f681cdb 100644 --- a/lib/widgets/mini_player.dart +++ b/lib/widgets/mini_player.dart @@ -5,6 +5,7 @@ import 'package:just_audio/just_audio.dart'; import '../services/player_service.dart'; import '../models/song.dart'; import '../utils/farb_theme.dart'; +import 'warteschlange_sheet.dart'; class MiniPlayer extends StatefulWidget { const MiniPlayer({super.key}); @@ -51,6 +52,16 @@ class _MiniPlayerState extends State { if (mounted) setState(() {}); } + /// Öffnet die Warteschlange als Bottom-Sheet + void _zeigeWarteschlange() { + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: MeloTheme.dunkel1, + builder: (_) => const WarteschlangeSheet(), + ); + } + @override void dispose() { _player.removeListener(_onPlayerChanged); @@ -69,16 +80,18 @@ class _MiniPlayerState extends State { ? _position.inSeconds / _dauer.inSeconds : 0.0; final hatCover = song.coverPfad != null && File(song.coverPfad!).existsSync(); - return Container( - margin: const EdgeInsets.symmetric(horizontal: 16), - decoration: BoxDecoration( - color: const Color(0xFF1A0A0A), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: const Color(0xFF2A1515)), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ + return GestureDetector( + onTap: _zeigeWarteschlange, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16), + decoration: BoxDecoration( + color: const Color(0xFF1A0A0A), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: const Color(0xFF2A1515)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ Padding( padding: const EdgeInsets.fromLTRB(12, 10, 12, 6), child: Row( @@ -129,7 +142,8 @@ class _MiniPlayerState extends State { ), ), ), - ], + ], + ), ), ); } diff --git a/lib/widgets/warteschlange_sheet.dart b/lib/widgets/warteschlange_sheet.dart new file mode 100644 index 0000000..854dc6c --- /dev/null +++ b/lib/widgets/warteschlange_sheet.dart @@ -0,0 +1,145 @@ +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(); + + @override + Widget build(BuildContext context) { + return ListenableBuilder( + listenable: _player, + builder: (context, _) { + final queue = _player.warteschlange; + final aktueller = _player.aktuellerIndex; + 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( + '${queue.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), + // Liste + if (queue.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, 16), + itemCount: queue.length, + // onReorderItem liefert den newIndex bereits angepasst + onReorderItem: (alt, neu) => + _player.verschiebeInWarteschlange(alt, neu), + itemBuilder: (_, i) => _eintrag(queue[i], i, aktueller), + ), + ), + ], + ), + ), + ), + ); + }, + ); + } + + Widget _eintrag(Song song, int index, int aktueller) { + final istAktuell = index == aktueller; + return ListTile( + key: ValueKey('$index-${song.id ?? song.titel}'), + dense: true, + leading: istAktuell + ? const Icon(Icons.play_arrow, color: MeloTheme.rot, size: 18) + : SizedBox( + width: 18, + child: Text( + '${index + 1}', + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 11, color: MeloTheme.textSekundaer), + ), + ), + title: Text( + song.titel, + style: TextStyle( + fontSize: 13, + fontWeight: istAktuell ? FontWeight.w700 : FontWeight.w400, + color: istAktuell ? MeloTheme.rot : 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), + ); + } +}