HomeShell: PlayerExpansionController bereitstellen, Zurück-Taste schließt offenen Player
This commit is contained in:
+32
-7
@@ -19,6 +19,7 @@ import 'library/search_screen.dart';
|
|||||||
import 'player/audio_effects.dart';
|
import 'player/audio_effects.dart';
|
||||||
import 'player/audio_handler.dart';
|
import 'player/audio_handler.dart';
|
||||||
import 'player/mini_player.dart';
|
import 'player/mini_player.dart';
|
||||||
|
import 'player/player_expansion_controller.dart';
|
||||||
import 'services/baka_auth.dart';
|
import 'services/baka_auth.dart';
|
||||||
import 'services/logger_service.dart';
|
import 'services/logger_service.dart';
|
||||||
import 'services/download_service.dart';
|
import 'services/download_service.dart';
|
||||||
@@ -150,9 +151,11 @@ class HomeShell extends StatefulWidget {
|
|||||||
State<HomeShell> createState() => _HomeShellState();
|
State<HomeShell> createState() => _HomeShellState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
|
class _HomeShellState extends State<HomeShell>
|
||||||
|
with WidgetsBindingObserver, TickerProviderStateMixin {
|
||||||
int _index = 0;
|
int _index = 0;
|
||||||
late final AutoScan _autoScan;
|
late final AutoScan _autoScan;
|
||||||
|
late final PlayerExpansionController _expansion;
|
||||||
|
|
||||||
/// Adresse aus einem „Teilen"-Aufruf, die der Downloader übernehmen soll.
|
/// Adresse aus einem „Teilen"-Aufruf, die der Downloader übernehmen soll.
|
||||||
String? _geteilteAdresse;
|
String? _geteilteAdresse;
|
||||||
@@ -162,6 +165,7 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
|
|||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
|
_expansion = PlayerExpansionController(vsync: this);
|
||||||
final db = context.read<MeloDb>();
|
final db = context.read<MeloDb>();
|
||||||
final lib = context.read<LibraryService>();
|
final lib = context.read<LibraryService>();
|
||||||
_autoScan = AutoScan(
|
_autoScan = AutoScan(
|
||||||
@@ -190,6 +194,7 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_expansion.dispose();
|
||||||
WidgetsBinding.instance.removeObserver(this);
|
WidgetsBinding.instance.removeObserver(this);
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
@@ -222,14 +227,33 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
|
|||||||
const FavoritesScreen(),
|
const FavoritesScreen(),
|
||||||
const YoutubeSearchScreen(),
|
const YoutubeSearchScreen(),
|
||||||
];
|
];
|
||||||
return PopScope(
|
return ChangeNotifierProvider<PlayerExpansionController>.value(
|
||||||
// Tab-Wechsel laufen nur über setState, nie über Navigator.push — die
|
value: _expansion,
|
||||||
// Zurück-Geste auf einem Nicht-Start-Tab hätte sonst nichts zum
|
// Consumer NUR um PopScope — sein `child:` (der Scaffold-Rumpf) wird
|
||||||
// Poppen und würde die App minimieren statt zu "Meine Musik" zu gehen.
|
// laut Flutter-Semantik bei jedem _expansion-Tick NICHT neu gebaut,
|
||||||
canPop: _index == 0,
|
// das ist hier auch nicht nötig: PopScope selbst braucht nur einen
|
||||||
|
// aktuellen Wert bei jedem HomeShell-Rebuild (z.B. Zurück-Taste
|
||||||
|
// gedrückt), keine Frame-genaue Aktualisierung während des Ziehens.
|
||||||
|
// Der Blur-Layer/Mini-Player-Fade in Task 4 braucht dagegen echte
|
||||||
|
// Live-Reaktivität und bekommt dafür einen eigenen, davon unabhängigen
|
||||||
|
// AnimatedBuilder direkt an seiner Stelle im Stack — siehe
|
||||||
|
// Reaktivitäts-Regel in Global Constraints.
|
||||||
|
child: Consumer<PlayerExpansionController>(
|
||||||
|
builder: (context, expansion, child) => PopScope(
|
||||||
|
// Offener/halb gezogener Player hat Vorrang vor dem Tab-Reset:
|
||||||
|
// Zurück soll ihn zuerst schließen, nicht gleichzeitig auch noch
|
||||||
|
// den Tab wechseln.
|
||||||
|
canPop: expansion.progress == 0 && _index == 0,
|
||||||
onPopInvokedWithResult: (didPop, _) {
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
if (!didPop) setState(() => _index = 0);
|
if (didPop) return;
|
||||||
|
if (expansion.progress > 0) {
|
||||||
|
expansion.close(context);
|
||||||
|
} else {
|
||||||
|
setState(() => _index = 0);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
child: child!,
|
||||||
|
),
|
||||||
child: Scaffold(
|
child: Scaffold(
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -266,6 +290,7 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:melo/player/player_expansion_controller.dart';
|
||||||
|
|
||||||
|
// Minimaler Host, der dieselbe Provider/PopScope-Verdrahtung wie HomeShell
|
||||||
|
// nachstellt, ohne die volle App (Audio-Handler etc.) hochfahren zu müssen.
|
||||||
|
// Der eigentliche HomeShell-Umbau wird direkt gegen main.dart getestet,
|
||||||
|
// sobald Task 4/5 die Stack-Struktur eingeführt haben; dieser erste Test
|
||||||
|
// sichert nur die PopScope-Erweiterung ab, isoliert an einem Test-Double.
|
||||||
|
class _TestShell extends StatefulWidget {
|
||||||
|
const _TestShell();
|
||||||
|
@override
|
||||||
|
State<_TestShell> createState() => _TestShellState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TestShellState extends State<_TestShell> with TickerProviderStateMixin {
|
||||||
|
late final PlayerExpansionController _controller;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_controller = PlayerExpansionController(vsync: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_controller.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Eigener AnimatedBuilder um PopScope, damit canPop bei jedem
|
||||||
|
// _controller-Tick neu ausgewertet wird (Reaktivitäts-Regel aus dem
|
||||||
|
// Plan) — ohne ihn bliebe canPop auf dem Wert des allerersten Builds
|
||||||
|
// eingefroren, weil sonst nichts hier ein Rebuild auslöst.
|
||||||
|
return ChangeNotifierProvider<PlayerExpansionController>.value(
|
||||||
|
value: _controller,
|
||||||
|
child: AnimatedBuilder(
|
||||||
|
animation: _controller,
|
||||||
|
builder: (context, child) => PopScope(
|
||||||
|
canPop: _controller.progress == 0,
|
||||||
|
onPopInvokedWithResult: (didPop, _) {
|
||||||
|
if (!didPop) _controller.close(context);
|
||||||
|
},
|
||||||
|
child: child!,
|
||||||
|
),
|
||||||
|
child: const Scaffold(body: SizedBox()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
testWidgets('Zurück-Taste schließt den Player statt zu poppen, wenn offen',
|
||||||
|
(tester) async {
|
||||||
|
await tester.pumpWidget(const MaterialApp(home: _TestShell()));
|
||||||
|
final controller =
|
||||||
|
tester.state<_TestShellState>(find.byType(_TestShell))._controller;
|
||||||
|
|
||||||
|
controller.open(tester.element(find.byType(_TestShell)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(controller.progress, 1.0);
|
||||||
|
|
||||||
|
// pageBack() sucht einen sichtbaren Zurück-Button, den es hier ohne
|
||||||
|
// zweite Route nicht gibt (siehe Plan-Hinweis für den Umsetzer) —
|
||||||
|
// Navigator.maybePop() löst denselben PopScope-Callback aus.
|
||||||
|
await Navigator.maybePop(tester.element(find.byType(_TestShell)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(controller.progress, 0.0);
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user