diff --git a/lib/services/player_service.dart b/lib/services/player_service.dart index d94e91b..fab2c10 100644 --- a/lib/services/player_service.dart +++ b/lib/services/player_service.dart @@ -1,12 +1,13 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:just_audio/just_audio.dart'; import '../models/song.dart'; import '../database/db_helper.dart'; import 'melo_logger.dart'; -class PlayerService { +class PlayerService extends ChangeNotifier { static final PlayerService _instanz = PlayerService._(); factory PlayerService() => _instanz; PlayerService._(); @@ -16,6 +17,16 @@ class PlayerService { int _aktuellerIndex = -1; StreamSubscription? _autoNextSub; + // ─── Zufallsmodus (Shuffle) ─── + bool _zufallsmodus = false; + + /// Zufällige Reihenfolge der Warteschlangen-Indizes (ohne den aktuellen Song). + /// Der aktuelle Song erscheint erst wieder, wenn er manuell gewählt wird. + final List _shuffleReihenfolge = []; + + /// Position des zuletzt gespielten Songs in [_shuffleReihenfolge] (-1 = noch nichts). + int _shufflePos = -1; + AudioPlayer get _p { if (_player == null) { _player = AudioPlayer(); @@ -47,12 +58,59 @@ class PlayerService { final StreamController _songWechsel = StreamController.broadcast(); Stream get onSongWechsel => _songWechsel.stream; + // ─── Zufallsmodus (Shuffle) ─── + + bool get zufallsmodus => _zufallsmodus; + + void setZufallsmodus(bool wert) { + if (_zufallsmodus == wert) return; + _zufallsmodus = wert; + if (wert) _baueShuffleReihenfolge(); + notifyListeners(); + } + + /// Baut die Zufallsreihenfolge neu: alle Indizes außer dem aktuellen Song, gemischt. + void _baueShuffleReihenfolge() { + _shuffleReihenfolge.clear(); + _shufflePos = -1; + if (_warteschlange.isEmpty) return; + if (_warteschlange.length == 1) { + _shuffleReihenfolge.add(0); + return; + } + final indices = List.generate(_warteschlange.length, (i) => i); + if (_aktuellerIndex >= 0 && _aktuellerIndex < _warteschlange.length) { + indices.remove(_aktuellerIndex); + } + indices.shuffle(Random()); + _shuffleReihenfolge.addAll(indices); + } + + /// Nächster Index in der Zufallsreihenfolge (null = alle durchgespielt). + int? _naechsterShuffleIndex() { + _shufflePos++; + if (_shufflePos < 0 || _shufflePos >= _shuffleReihenfolge.length) { + return null; + } + final idx = _shuffleReihenfolge[_shufflePos]; + return (idx >= 0 && idx < _warteschlange.length) ? idx : null; + } + Future spiele(Song song, {int position = 0}) async { _aktuellerIndex = _warteschlange.indexWhere((s) => s.id == song.id); if (_aktuellerIndex < 0) { _warteschlange.add(song); _aktuellerIndex = _warteschlange.length - 1; } + // Shuffle-Position synchron halten (Song kann über Queue-UI/Manuell gewählt sein) + if (_zufallsmodus) { + final pos = _shuffleReihenfolge.indexOf(_aktuellerIndex); + if (pos < 0) { + _baueShuffleReihenfolge(); + } else { + _shufflePos = pos; + } + } try { if (song.streamUrl != null && song.streamUrl!.isNotEmpty) { // Streaming vom Server @@ -67,6 +125,7 @@ class PlayerService { if (position > 0) await _p.seek(Duration(seconds: position)); await _p.play(); _songWechsel.add(song); + notifyListeners(); // Abspiel-Historie für den Jahres-Recap registrieren if (song.id != null) { try { @@ -117,15 +176,31 @@ class PlayerService { final pos = _p.position; if (pos.inSeconds > 5) { await _p.seek(Duration.zero); - } else { - final neuerIndex = _aktuellerIndex - 1; - if (neuerIndex >= 0) { - await spiele(_warteschlange[neuerIndex]); + return; + } + if (_zufallsmodus && _shuffleReihenfolge.isNotEmpty) { + _shufflePos--; + if (_shufflePos < 0) _shufflePos = 0; + final idx = _shuffleReihenfolge[_shufflePos]; + if (idx >= 0 && idx < _warteschlange.length) { + await spiele(_warteschlange[idx]); } + return; + } + final neuerIndex = _aktuellerIndex - 1; + if (neuerIndex >= 0) { + await spiele(_warteschlange[neuerIndex]); } } Future naechstes() async { + if (_player == null || aktuellerSong == null) return; + if (_zufallsmodus) { + final idx = _naechsterShuffleIndex(); + if (idx == null) return; + await spiele(_warteschlange[idx]); + return; + } final neuerIndex = _aktuellerIndex + 1; if (neuerIndex < _warteschlange.length) { await spiele(_warteschlange[neuerIndex]); @@ -135,14 +210,20 @@ class PlayerService { void setWarteschlange(List songs, {int startIndex = 0}) { _warteschlange.clear(); _warteschlange.addAll(songs); - _aktuellerIndex = startIndex; + _aktuellerIndex = _warteschlange.isEmpty + ? -1 + : startIndex.clamp(0, _warteschlange.length - 1); + _baueShuffleReihenfolge(); + notifyListeners(); } + @override void dispose() { _autoNextSub?.cancel(); _player?.dispose(); _player = null; _songWechsel.close(); + super.dispose(); } /// Markiert den aktuellen Song in der DB als korrupt diff --git a/lib/widgets/mini_player.dart b/lib/widgets/mini_player.dart index 224ffea..9e66820 100644 --- a/lib/widgets/mini_player.dart +++ b/lib/widgets/mini_player.dart @@ -43,10 +43,17 @@ class _MiniPlayerState extends State { _songSub = _player.onSongWechsel.listen((_) { if (mounted) setState(() {}); }); + // Shuffle-/Repeat-/Sleep-Zustand live halten + _player.addListener(_onPlayerChanged); + } + + void _onPlayerChanged() { + if (mounted) setState(() {}); } @override void dispose() { + _player.removeListener(_onPlayerChanged); _posSub?.cancel(); _stateSub?.cancel(); _songSub?.cancel(); @@ -98,6 +105,8 @@ class _MiniPlayerState extends State { ], ), ), + // Shuffle + _shuffleBtn(), // Steuerung _btn(Icons.skip_previous, _player.vorheriges), _playBtn(), @@ -123,7 +132,17 @@ class _MiniPlayerState extends State { ); } - Widget _btn(IconData icon, VoidCallback onTap) { + /// Shuffle-Icon — rot wenn aktiv + Widget _shuffleBtn() { + final aktiv = _player.zufallsmodus; + return _btn( + Icons.shuffle, + () => _player.setZufallsmodus(!aktiv), + farbe: aktiv ? MeloTheme.rot : Colors.white, + ); + } + + Widget _btn(IconData icon, VoidCallback onTap, {Color farbe = Colors.white}) { return Material( color: Colors.transparent, child: InkWell( @@ -132,7 +151,7 @@ class _MiniPlayerState extends State { child: Container( width: 32, height: 32, alignment: Alignment.center, - child: Icon(icon, size: 18, color: Colors.white), + child: Icon(icon, size: 18, color: farbe), ), ), );