diff --git a/lib/services/player_service.dart b/lib/services/player_service.dart index d54eb7a..d7aa808 100644 --- a/lib/services/player_service.dart +++ b/lib/services/player_service.dart @@ -277,17 +277,104 @@ class PlayerService extends ChangeNotifier { /// Index des aktuell spielenden Songs in der Warteschlange. int get aktuellerIndex => _aktuellerIndex; - /// Fügt [song] direkt nach dem aktuellen Song ein. + /// Setzt [song] als nächstes auf die Wiedergabe — direkt nach dem aktuellen + /// Song. Ist der Song bereits in der Warteschlange, wird er dorthin VERSCHOBEN + /// statt dupliziert. Im Shuffle wird die Reihenfolge nicht neu gemischt: + /// der Song wird an die nächste Position gepinnt (Play-Next hat Priorität). void spieleAlsNaechstes(Song song) { - if (_aktuellerIndex < 0 || _warteschlange.isEmpty) { + if (_warteschlange.isEmpty) { _warteschlange.add(song); - } else { - _warteschlange.insert(_aktuellerIndex + 1, song); + if (_zufallsmodus) _baueShuffleReihenfolge(); + notifyListeners(); + return; + } + if (_aktuellerIndex < 0) { + // Kein aktiver Song → ans Ende hängen + final vorhanden = _warteschlange.indexWhere((s) => s.id == song.id); + if (vorhanden < 0) _warteschlange.add(song); + if (_zufallsmodus) _baueShuffleReihenfolge(); + notifyListeners(); + return; + } + + final ziel = _aktuellerIndex + 1; + final vorhandenerIndex = _warteschlange.indexWhere((s) => s.id == song.id); + var gepinnt = -1; + var alterIndex = -1; + var einfuegePosition = -1; + var queueGeaendert = false; + + if (vorhandenerIndex >= 0 && vorhandenerIndex != _aktuellerIndex) { + // Bereits in der Queue → verschieben statt duplizieren + if (vorhandenerIndex != ziel) { + alterIndex = vorhandenerIndex; + final item = _warteschlange.removeAt(vorhandenerIndex); + einfuegePosition = vorhandenerIndex < ziel ? ziel - 1 : ziel; + _warteschlange.insert(einfuegePosition, item); + if (vorhandenerIndex < _aktuellerIndex) _aktuellerIndex -= 1; + gepinnt = einfuegePosition; + queueGeaendert = true; + } else { + gepinnt = vorhandenerIndex; // steht schon direkt nach dem aktuellen + } + } else if (vorhandenerIndex < 0) { + einfuegePosition = ziel; + _warteschlange.insert(ziel, song); + gepinnt = ziel; + queueGeaendert = true; + } + // vorhandenerIndex == _aktuellerIndex → Song läuft gerade, nichts zu tun + + if (_zufallsmodus && gepinnt >= 0) { + _pinneInShuffleReihenfolge( + gepinnt: gepinnt, + alterIndex: alterIndex, + einfuegePosition: einfuegePosition, + queueGeaendert: queueGeaendert, + ); } - if (_zufallsmodus) _baueShuffleReihenfolge(); notifyListeners(); } + /// Korrigiert [_shuffleReihenfolge] nach einer Queue-Änderung und pinnt + /// [gepinnt] (neuer Queue-Index des Songs) als nächstes. + void _pinneInShuffleReihenfolge({ + required int gepinnt, + required int alterIndex, + required int einfuegePosition, + required bool queueGeaendert, + }) { + if (queueGeaendert) { + // 1) Entfernung spiegeln (alter Index war evtl. noch in der Reihenfolge) + final entferntPos = alterIndex >= 0 + ? _shuffleReihenfolge.indexOf(alterIndex) + : -1; + if (entferntPos >= 0) { + _shuffleReihenfolge.removeAt(entferntPos); + if (entferntPos < _shufflePos) _shufflePos -= 1; + for (var k = 0; k < _shuffleReihenfolge.length; k++) { + if (_shuffleReihenfolge[k] > alterIndex) { + _shuffleReihenfolge[k] -= 1; + } + } + } + // 2) Einfügung spiegeln (Indizes ab der Einfügeposition rücken nach rechts) + for (var k = 0; k < _shuffleReihenfolge.length; k++) { + if (_shuffleReihenfolge[k] >= einfuegePosition) { + _shuffleReihenfolge[k] += 1; + } + } + } + // 3) Song an die Position direkt nach dem aktuellen pinnten + _shuffleReihenfolge.remove(gepinnt); + final pinPos = _shufflePos + 1 > _shuffleReihenfolge.length + ? _shuffleReihenfolge.length + : _shufflePos + 1; + _shuffleReihenfolge.insert(pinPos, gepinnt); + // _shufflePos so setzen, dass der gepinnte Song als nächstes gespielt wird + _shufflePos = pinPos - 1; + } + /// Hängt [song] ans Ende der Warteschlange an. void amEndeHinzufuegen(Song song) { _warteschlange.add(song); diff --git a/test/player_service_test.dart b/test/player_service_test.dart index bddf822..e8c38c5 100644 --- a/test/player_service_test.dart +++ b/test/player_service_test.dart @@ -51,6 +51,37 @@ void main() { expect(p.aktuellerIndex, -1); }); + test('spieleAlsNaechstes verschiebt vorhandenen Song statt zu duplizieren', () { + final p = PlayerService(); + p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')]); + p.spieleAlsNaechstes(_song(3, 'C')); + expect(p.warteschlange.map((s) => s.id).toList(), [1, 3, 2]); + expect(p.warteschlange.length, 3); + // Wiederholter Aufruf erzeugt weiterhin keine Duplikate + p.spieleAlsNaechstes(_song(3, 'C')); + expect(p.warteschlange.length, 3); + }); + + test('spieleAlsNaechstes verschiebt Song von vor dem aktuellen', () { + final p = PlayerService(); + p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C')], startIndex: 2); + p.spieleAlsNaechstes(_song(1, 'A')); + expect(p.warteschlange.map((s) => s.id).toList(), [2, 3, 1]); + expect(p.aktuellerIndex, 1); // aktueller Song (C) wandert auf Index 1 + }); + + test('spieleAlsNaechstes im Shuffle: Song wird als nächstes gespielt', () { + final p = PlayerService(); + p.setWarteschlange([_song(1, 'A'), _song(2, 'B'), _song(3, 'C'), _song(4, 'D')]); + p.setZufallsmodus(true); + p.spieleAlsNaechstes(_song(3, 'C')); + // Kein Duplikat: alle 4 Songs genau einmal + expect(p.warteschlange.map((s) => s.id).toSet(), {1, 2, 3, 4}); + expect(p.warteschlange.length, 4); + // C (jetzt direkt nach dem aktuellen) wird als nächstes gespielt + expect(p.naechsterIndex(), 1); + }); + test('amEndeHinzufuegen hängt ans Ende an', () { final p = PlayerService(); p.setWarteschlange([_song(1, 'A')]);