v2.48 — Shuffle-Modus
## PlayerService - PlayerService ist jetzt ChangeNotifier (UI hängt per addListener/ListenableBuilder an) - zufallsmodus Getter + setZufallsmodus(bool) - Zufallsreihenfolge: alle Queue-Indizes außer dem aktuellen Song, gemischt - naechstes(): bei Shuffle nächster Song aus Zufallsreihenfolge, ohne Wiederholung bis alle durch - vorheriges(): geht in der Zufallsreihenfolge zurück - spiele(): synchronisiert Shuffle-Position (auch bei manueller Auswahl) ## MiniPlayer - Shuffle-Icon (rot wenn aktiv, Tipp schaltet um) Keine neuen Packages, flutter analyze 0 Issues, Tests 4/4
This commit is contained in:
@@ -1,12 +1,13 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
import 'dart:math';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
import '../models/song.dart';
|
import '../models/song.dart';
|
||||||
import '../database/db_helper.dart';
|
import '../database/db_helper.dart';
|
||||||
import 'melo_logger.dart';
|
import 'melo_logger.dart';
|
||||||
|
|
||||||
class PlayerService {
|
class PlayerService extends ChangeNotifier {
|
||||||
static final PlayerService _instanz = PlayerService._();
|
static final PlayerService _instanz = PlayerService._();
|
||||||
factory PlayerService() => _instanz;
|
factory PlayerService() => _instanz;
|
||||||
PlayerService._();
|
PlayerService._();
|
||||||
@@ -16,6 +17,16 @@ class PlayerService {
|
|||||||
int _aktuellerIndex = -1;
|
int _aktuellerIndex = -1;
|
||||||
StreamSubscription<PlayerState>? _autoNextSub;
|
StreamSubscription<PlayerState>? _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<int> _shuffleReihenfolge = [];
|
||||||
|
|
||||||
|
/// Position des zuletzt gespielten Songs in [_shuffleReihenfolge] (-1 = noch nichts).
|
||||||
|
int _shufflePos = -1;
|
||||||
|
|
||||||
AudioPlayer get _p {
|
AudioPlayer get _p {
|
||||||
if (_player == null) {
|
if (_player == null) {
|
||||||
_player = AudioPlayer();
|
_player = AudioPlayer();
|
||||||
@@ -47,12 +58,59 @@ class PlayerService {
|
|||||||
final StreamController<Song?> _songWechsel = StreamController.broadcast();
|
final StreamController<Song?> _songWechsel = StreamController.broadcast();
|
||||||
Stream<Song?> get onSongWechsel => _songWechsel.stream;
|
Stream<Song?> 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<int>.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<void> spiele(Song song, {int position = 0}) async {
|
Future<void> spiele(Song song, {int position = 0}) async {
|
||||||
_aktuellerIndex = _warteschlange.indexWhere((s) => s.id == song.id);
|
_aktuellerIndex = _warteschlange.indexWhere((s) => s.id == song.id);
|
||||||
if (_aktuellerIndex < 0) {
|
if (_aktuellerIndex < 0) {
|
||||||
_warteschlange.add(song);
|
_warteschlange.add(song);
|
||||||
_aktuellerIndex = _warteschlange.length - 1;
|
_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 {
|
try {
|
||||||
if (song.streamUrl != null && song.streamUrl!.isNotEmpty) {
|
if (song.streamUrl != null && song.streamUrl!.isNotEmpty) {
|
||||||
// Streaming vom Server
|
// Streaming vom Server
|
||||||
@@ -67,6 +125,7 @@ class PlayerService {
|
|||||||
if (position > 0) await _p.seek(Duration(seconds: position));
|
if (position > 0) await _p.seek(Duration(seconds: position));
|
||||||
await _p.play();
|
await _p.play();
|
||||||
_songWechsel.add(song);
|
_songWechsel.add(song);
|
||||||
|
notifyListeners();
|
||||||
// Abspiel-Historie für den Jahres-Recap registrieren
|
// Abspiel-Historie für den Jahres-Recap registrieren
|
||||||
if (song.id != null) {
|
if (song.id != null) {
|
||||||
try {
|
try {
|
||||||
@@ -117,15 +176,31 @@ class PlayerService {
|
|||||||
final pos = _p.position;
|
final pos = _p.position;
|
||||||
if (pos.inSeconds > 5) {
|
if (pos.inSeconds > 5) {
|
||||||
await _p.seek(Duration.zero);
|
await _p.seek(Duration.zero);
|
||||||
} else {
|
return;
|
||||||
final neuerIndex = _aktuellerIndex - 1;
|
}
|
||||||
if (neuerIndex >= 0) {
|
if (_zufallsmodus && _shuffleReihenfolge.isNotEmpty) {
|
||||||
await spiele(_warteschlange[neuerIndex]);
|
_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<void> naechstes() async {
|
Future<void> 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;
|
final neuerIndex = _aktuellerIndex + 1;
|
||||||
if (neuerIndex < _warteschlange.length) {
|
if (neuerIndex < _warteschlange.length) {
|
||||||
await spiele(_warteschlange[neuerIndex]);
|
await spiele(_warteschlange[neuerIndex]);
|
||||||
@@ -135,14 +210,20 @@ class PlayerService {
|
|||||||
void setWarteschlange(List<Song> songs, {int startIndex = 0}) {
|
void setWarteschlange(List<Song> songs, {int startIndex = 0}) {
|
||||||
_warteschlange.clear();
|
_warteschlange.clear();
|
||||||
_warteschlange.addAll(songs);
|
_warteschlange.addAll(songs);
|
||||||
_aktuellerIndex = startIndex;
|
_aktuellerIndex = _warteschlange.isEmpty
|
||||||
|
? -1
|
||||||
|
: startIndex.clamp(0, _warteschlange.length - 1);
|
||||||
|
_baueShuffleReihenfolge();
|
||||||
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_autoNextSub?.cancel();
|
_autoNextSub?.cancel();
|
||||||
_player?.dispose();
|
_player?.dispose();
|
||||||
_player = null;
|
_player = null;
|
||||||
_songWechsel.close();
|
_songWechsel.close();
|
||||||
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Markiert den aktuellen Song in der DB als korrupt
|
/// Markiert den aktuellen Song in der DB als korrupt
|
||||||
|
|||||||
@@ -43,10 +43,17 @@ class _MiniPlayerState extends State<MiniPlayer> {
|
|||||||
_songSub = _player.onSongWechsel.listen((_) {
|
_songSub = _player.onSongWechsel.listen((_) {
|
||||||
if (mounted) setState(() {});
|
if (mounted) setState(() {});
|
||||||
});
|
});
|
||||||
|
// Shuffle-/Repeat-/Sleep-Zustand live halten
|
||||||
|
_player.addListener(_onPlayerChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onPlayerChanged() {
|
||||||
|
if (mounted) setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_player.removeListener(_onPlayerChanged);
|
||||||
_posSub?.cancel();
|
_posSub?.cancel();
|
||||||
_stateSub?.cancel();
|
_stateSub?.cancel();
|
||||||
_songSub?.cancel();
|
_songSub?.cancel();
|
||||||
@@ -98,6 +105,8 @@ class _MiniPlayerState extends State<MiniPlayer> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Shuffle
|
||||||
|
_shuffleBtn(),
|
||||||
// Steuerung
|
// Steuerung
|
||||||
_btn(Icons.skip_previous, _player.vorheriges),
|
_btn(Icons.skip_previous, _player.vorheriges),
|
||||||
_playBtn(),
|
_playBtn(),
|
||||||
@@ -123,7 +132,17 @@ class _MiniPlayerState extends State<MiniPlayer> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
return Material(
|
||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
@@ -132,7 +151,7 @@ class _MiniPlayerState extends State<MiniPlayer> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
width: 32, height: 32,
|
width: 32, height: 32,
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Icon(icon, size: 18, color: Colors.white),
|
child: Icon(icon, size: 18, color: farbe),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user