diff --git a/lib/player/now_playing_screen.dart b/lib/player/now_playing_screen.dart index f4b39f8..4673629 100644 --- a/lib/player/now_playing_screen.dart +++ b/lib/player/now_playing_screen.dart @@ -15,6 +15,7 @@ import '../shared/theme.dart'; import 'audio_handler.dart'; import 'cover_farbe.dart'; import 'lrc.dart'; +import 'player_expansion_controller.dart'; import 'queue_screen.dart'; /// Inhalt des Vollbild-Players — wird von HomeShell in ein per progress @@ -49,8 +50,7 @@ class NowPlayingScreen extends StatelessWidget { children: [ _VollbildLeiste(handler: handler, onLyrics: _showLyrics), Expanded( - child: GestureDetector( - onVerticalDragStart: (_) {}, // s. Task 8 + child: _SchliessGeste( child: StreamBuilder( stream: handler.mediaItem, builder: (context, snapshot) { @@ -153,6 +153,44 @@ class WanderndesCover extends StatelessWidget { } } +/// Schließt den Vollbild-Player per Wischgeste nach unten — symmetrisch zum +/// Öffnen am Mini-Player, live über denselben PlayerExpansionController. +/// Eigenes StatefulWidget, weil eine lokale Rohstrecke über die Gestendauer +/// mitgeführt werden muss (analog zum bisherigen `_versatz`-Muster in +/// mini_player.dart). +class _SchliessGeste extends StatefulWidget { + const _SchliessGeste({required this.child}); + final Widget child; + + @override + State<_SchliessGeste> createState() => _SchliessGesteState(); +} + +class _SchliessGesteState extends State<_SchliessGeste> { + double _strecke = 0; + + @override + Widget build(BuildContext context) { + final controller = context.read(); + return GestureDetector( + // Ohne opaque behavior fängt die Geste keine Berührungen über den + // großflächigen, unbemalten Bereichen des Inhalts ab (z.B. über dem + // Cover-Platzhalter, der nur ein leeres SizedBox ist) — GestureDetector + // reagiert per Default (deferToChild) nur dort, wo ein Nachfahre + // tatsächlich etwas zeichnet (per Testlauf verifiziert). + behavior: HitTestBehavior.opaque, + onVerticalDragStart: (_) => _strecke = 0, + onVerticalDragUpdate: (details) { + _strecke += details.delta.dy; + controller.dragBy(details.delta.dy); + }, + onVerticalDragEnd: (details) => controller.dragEnd( + context, _strecke, details.velocity.pixelsPerSecond.dy), + child: widget.child, + ); + } +} + /// Die bisherigen AppBar-Actions (Songtext, Sleep-Timer, Warteschlange) als /// eigene Kopfzeile statt echter AppBar — eine Route/Scaffold gibt es hier /// nicht mehr. diff --git a/test/player/now_playing_heart_test.dart b/test/player/now_playing_heart_test.dart index 72e4459..c63c0f3 100644 --- a/test/player/now_playing_heart_test.dart +++ b/test/player/now_playing_heart_test.dart @@ -98,7 +98,7 @@ void main() { // deshalb direkt in einen eigenen Scaffold gepumpt (Task 5 Step 3), statt // eine Route zu pushen. Der Handler behält den zuvor gestarteten Titel in // seinem mediaItem-Stream, den NowPlayingScreen darüber sofort sieht. - Future oeffnePlayer(WidgetTester tester) async { + Future oeffnePlayer(WidgetTester tester) async { final expansion = PlayerExpansionController(vsync: tester); addTearDown(expansion.dispose); final providers = [ @@ -138,6 +138,7 @@ void main() { ), ); await tester.pumpAndSettle(); + return expansion; } Future raeumeAuf(WidgetTester tester) async { @@ -173,4 +174,25 @@ void main() { await raeumeAuf(tester); }); + + testWidgets('Ziehen nach unten im Vollbild-Player verringert progress live', + (tester) async { + // _TestShell (home_shell_expansion_test.dart) hat bewusst keinen echten + // MeloAudioHandler-Provider (siehe dortiger Kommentar) — ein echter + // NowPlayingScreen bräuchte den aber für context.read(). + // Dieser Test läuft deshalb hier, wo oeffnePlayer() bereits alle nötigen + // Provider bereitstellt (Plan Task 8 nennt now_playing_screen_test.dart + // selbst als Alternative zu home_shell_expansion_test.dart). + final expansion = await oeffnePlayer(tester); + expect(expansion.progress, 1.0); + + await tester.drag(find.byType(NowPlayingScreen), const Offset(0, 150)); + await tester.pump(); + expect(expansion.progress, lessThan(1.0)); + + // dragEnd() (ausgelöst durch tester.drag) startet ein fling() — ohne + // pumpAndSettle bliebe dessen Ticker beim Testende aktiv. + await tester.pumpAndSettle(); + await raeumeAuf(tester); + }); }