Fix: PlayerExpansionController.dragEnd rät Öffnen/Schließen nicht mehr aus Vorzeichen

Bug (Code-Review, P0-nah): dragEnd() wählte die Entscheidungslogik allein
über das Vorzeichen von strecke. Bei bereits vollständig offenem Player
klappte ein schwaches, versehentliches Fingerzittern nach oben (kleine
negative Strecke, weit unter der Wisch-Schwelle) den Player ungewollt zu,
weil das Vorzeichen in den ÖFFNEN-Zweig führte statt in den (korrekten)
Schließen-Zweig, der bei so einer schwachen Geste keine Wirkung hätte.

dragEnd() bekommt jetzt den Pflichtparameter `ausVollbild`, der den
Aufrufer-Kontext explizit benennt statt ihn zu erraten. mini_player.dart
(Öffnen-Geste) ruft mit ausVollbild: false, now_playing_screen.dart
(Schließen-Geste) mit ausVollbild: true.

Test zuerst (RED mit der alten 3-Parameter-Signatur reproduziert, dann
GREEN nach dem Fix): test/player/player_expansion_controller_test.dart.
Bestehende Tests (player_expansion_controller_test.dart,
home_shell_expansion_test.dart) an die neue Signatur angepasst.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SM6e73V9ZethPiqHSocxUA
This commit is contained in:
Hermes (Server)
2026-08-29 15:36:46 +02:00
co-authored by Claude Sonnet 5
parent 1734a656a1
commit 6687f2f67c
5 changed files with 53 additions and 17 deletions
+2 -1
View File
@@ -92,7 +92,8 @@ class _LeisteState extends State<_Leiste> {
onVerticalDragEnd: (details) => context onVerticalDragEnd: (details) => context
.read<PlayerExpansionController>() .read<PlayerExpansionController>()
.dragEnd(context, _vertikaleStrecke, .dragEnd(context, _vertikaleStrecke,
details.velocity.pixelsPerSecond.dy), details.velocity.pixelsPerSecond.dy,
ausVollbild: false),
onHorizontalDragUpdate: (details) => setState(() { onHorizontalDragUpdate: (details) => setState(() {
_ziehtHorizontalGerade = true; _ziehtHorizontalGerade = true;
_horizontalVersatz += details.delta.dx; _horizontalVersatz += details.delta.dx;
+2 -1
View File
@@ -185,7 +185,8 @@ class _SchliessGesteState extends State<_SchliessGeste> {
controller.dragBy(details.delta.dy); controller.dragBy(details.delta.dy);
}, },
onVerticalDragEnd: (details) => controller.dragEnd( onVerticalDragEnd: (details) => controller.dragEnd(
context, _strecke, details.velocity.pixelsPerSecond.dy), context, _strecke, details.velocity.pixelsPerSecond.dy,
ausVollbild: true),
child: widget.child, child: widget.child,
); );
} }
+16 -10
View File
@@ -35,16 +35,22 @@ class PlayerExpansionController extends ChangeNotifier {
} }
/// [strecke]/[geschwindigkeit] wie in wischen.dart (negativ = nach oben). /// [strecke]/[geschwindigkeit] wie in wischen.dart (negativ = nach oben).
/// Bei einer Öffnen-Geste (strecke <= 0) direkt über /// [ausVollbild] benennt den Aufrufer-Kontext explizit, statt ihn aus dem
/// [oeffnetVollbildMitStrecke] entschieden; bei einer Schließen-Geste /// Vorzeichen von [strecke] zu raten (früherer Bug: bei bereits offenem
/// (strecke >= 0, im Vollbild-Player nach unten gezogen) über dieselbe /// Player klappte ein schwaches, versehentliches Aufwärtszittern — negative
/// Funktion mit gespiegeltem Vorzeichen — "stark genug nach unten" ist /// Strecke, weit unter der Schwelle — den Player zu, weil das Vorzeichen
/// spiegelbildlich zu "stark genug nach oben". Fliegt danach mit der /// allein in den ÖFFNEN-Zweig führte, der bei so einer schwachen Geste
/// echten Loslass-Geschwindigkeit als Startimpuls zum entschiedenen Ziel. /// `false` liefert). `false` (Mini-Player, Öffnen-Geste) entscheidet direkt
void dragEnd(BuildContext context, double strecke, double geschwindigkeit) { /// über [oeffnetVollbildMitStrecke]; `true` (Vollbild-Player, Schließen-
final zielIstOffen = strecke <= 0 /// Geste) über dieselbe Funktion mit gespiegeltem Vorzeichen — "stark genug
? oeffnetVollbildMitStrecke(strecke, geschwindigkeit) /// nach unten" ist spiegelbildlich zu "stark genug nach oben". Fliegt
: !oeffnetVollbildMitStrecke(-strecke, -geschwindigkeit); /// danach mit der echten Loslass-Geschwindigkeit als Startimpuls zum
/// entschiedenen Ziel.
void dragEnd(BuildContext context, double strecke, double geschwindigkeit,
{required bool ausVollbild}) {
final zielIstOffen = ausVollbild
? !oeffnetVollbildMitStrecke(-strecke, -geschwindigkeit)
: oeffnetVollbildMitStrecke(strecke, geschwindigkeit);
if (MediaQuery.disableAnimationsOf(context)) { if (MediaQuery.disableAnimationsOf(context)) {
_animation.value = zielIstOffen ? 1.0 : 0.0; _animation.value = zielIstOffen ? 1.0 : 0.0;
return; return;
+1 -1
View File
@@ -156,7 +156,7 @@ void main() {
expect(controller.progress, closeTo(250 / oeffnenReferenzHoehe, 0.01)); expect(controller.progress, closeTo(250 / oeffnenReferenzHoehe, 0.01));
expect(find.byType(BackdropFilter), findsOneWidget); expect(find.byType(BackdropFilter), findsOneWidget);
controller.dragEnd(ctx, -250, 0); controller.dragEnd(ctx, -250, 0, ausVollbild: false);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.progress, 1.0); expect(controller.progress, 1.0);
@@ -98,7 +98,7 @@ void main() {
})); }));
controller.dragBy(-200); controller.dragBy(-200);
controller.dragEnd(ctx, -200, 0); // Strecke > wischStreckeSchwelle (80) controller.dragEnd(ctx, -200, 0, ausVollbild: false); // Strecke > wischStreckeSchwelle (80)
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.progress, 1.0); expect(controller.progress, 1.0);
controller.dispose(); controller.dispose();
@@ -116,7 +116,7 @@ void main() {
})); }));
controller.dragBy(-30); controller.dragBy(-30);
controller.dragEnd(ctx, -30, 0); // unter wischStreckeSchwelle controller.dragEnd(ctx, -30, 0, ausVollbild: false); // unter wischStreckeSchwelle
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.progress, 0.0); expect(controller.progress, 0.0);
controller.dispose(); controller.dispose();
@@ -136,7 +136,7 @@ void main() {
controller.open(ctx); controller.open(ctx);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
controller.dragBy(200); // nach unten, Betrag > Referenzhöhe/2 controller.dragBy(200); // nach unten, Betrag > Referenzhöhe/2
controller.dragEnd(ctx, 200, 0); controller.dragEnd(ctx, 200, 0, ausVollbild: true);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.progress, 0.0); expect(controller.progress, 0.0);
controller.dispose(); controller.dispose();
@@ -156,7 +156,35 @@ void main() {
controller.open(ctx); controller.open(ctx);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
controller.dragBy(30); controller.dragBy(30);
controller.dragEnd(ctx, 30, 0); controller.dragEnd(ctx, 30, 0, ausVollbild: true);
await tester.pumpAndSettle();
expect(controller.progress, 1.0);
controller.dispose();
});
testWidgets(
'dragEnd bei bereits vollem Player: schwaches Fingerzittern nach oben '
'klappt ihn NICHT zu (asymmetrischer Bug — dragEnd darf die '
'Entscheidungsrichtung nicht aus dem Vorzeichen von strecke raten, '
'sondern muss den Aufrufer-Kontext kennen)', (tester) async {
late PlayerExpansionController controller;
late BuildContext ctx;
await tester.pumpWidget(mitMediaQuery(false, (context) {
ctx = context;
controller = PlayerExpansionController(vsync: tester);
return const SizedBox();
}));
controller.open(ctx);
await tester.pumpAndSettle();
// Der Player ist voll offen (progress==1). Der Nutzer verrutscht beim
// Versuch, einen Knopf zu treffen, leicht nach oben — eine winzige,
// negative Strecke weit unter der Wisch-Schwelle. Das darf gar keine
// Zustandsänderung auslösen, weil die Geste viel zu schwach ist — schon
// gar nicht ein Zuklappen, das würde "nach oben ziehen schließt den
// offenen Player" bedeuten, was es nicht tut.
controller.dragBy(-5);
controller.dragEnd(ctx, -5, 0, ausVollbild: true);
await tester.pumpAndSettle(); await tester.pumpAndSettle();
expect(controller.progress, 1.0); expect(controller.progress, 1.0);
controller.dispose(); controller.dispose();