Vollbild-Player: Schließgeste live an progress gekoppelt, symmetrisch zum Öffnen
This commit is contained in:
@@ -15,6 +15,7 @@ import '../shared/theme.dart';
|
|||||||
import 'audio_handler.dart';
|
import 'audio_handler.dart';
|
||||||
import 'cover_farbe.dart';
|
import 'cover_farbe.dart';
|
||||||
import 'lrc.dart';
|
import 'lrc.dart';
|
||||||
|
import 'player_expansion_controller.dart';
|
||||||
import 'queue_screen.dart';
|
import 'queue_screen.dart';
|
||||||
|
|
||||||
/// Inhalt des Vollbild-Players — wird von HomeShell in ein per progress
|
/// Inhalt des Vollbild-Players — wird von HomeShell in ein per progress
|
||||||
@@ -49,8 +50,7 @@ class NowPlayingScreen extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
_VollbildLeiste(handler: handler, onLyrics: _showLyrics),
|
_VollbildLeiste(handler: handler, onLyrics: _showLyrics),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: _SchliessGeste(
|
||||||
onVerticalDragStart: (_) {}, // s. Task 8
|
|
||||||
child: StreamBuilder<MediaItem?>(
|
child: StreamBuilder<MediaItem?>(
|
||||||
stream: handler.mediaItem,
|
stream: handler.mediaItem,
|
||||||
builder: (context, snapshot) {
|
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<PlayerExpansionController>();
|
||||||
|
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
|
/// Die bisherigen AppBar-Actions (Songtext, Sleep-Timer, Warteschlange) als
|
||||||
/// eigene Kopfzeile statt echter AppBar — eine Route/Scaffold gibt es hier
|
/// eigene Kopfzeile statt echter AppBar — eine Route/Scaffold gibt es hier
|
||||||
/// nicht mehr.
|
/// nicht mehr.
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ void main() {
|
|||||||
// deshalb direkt in einen eigenen Scaffold gepumpt (Task 5 Step 3), statt
|
// 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
|
// eine Route zu pushen. Der Handler behält den zuvor gestarteten Titel in
|
||||||
// seinem mediaItem-Stream, den NowPlayingScreen darüber sofort sieht.
|
// seinem mediaItem-Stream, den NowPlayingScreen darüber sofort sieht.
|
||||||
Future<void> oeffnePlayer(WidgetTester tester) async {
|
Future<PlayerExpansionController> oeffnePlayer(WidgetTester tester) async {
|
||||||
final expansion = PlayerExpansionController(vsync: tester);
|
final expansion = PlayerExpansionController(vsync: tester);
|
||||||
addTearDown(expansion.dispose);
|
addTearDown(expansion.dispose);
|
||||||
final providers = [
|
final providers = [
|
||||||
@@ -138,6 +138,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
await tester.pumpAndSettle();
|
await tester.pumpAndSettle();
|
||||||
|
return expansion;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> raeumeAuf(WidgetTester tester) async {
|
Future<void> raeumeAuf(WidgetTester tester) async {
|
||||||
@@ -173,4 +174,25 @@ void main() {
|
|||||||
|
|
||||||
await raeumeAuf(tester);
|
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<MeloAudioHandler>().
|
||||||
|
// 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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user