HomeShell: PlayerExpansionController bereitstellen, Zurück-Taste schließt offenen Player

This commit is contained in:
Hermes (Server)
2026-08-29 14:24:09 +02:00
parent 2baac9ed12
commit 7a9493955a
2 changed files with 138 additions and 40 deletions
+65 -40
View File
@@ -19,6 +19,7 @@ import 'library/search_screen.dart';
import 'player/audio_effects.dart';
import 'player/audio_handler.dart';
import 'player/mini_player.dart';
import 'player/player_expansion_controller.dart';
import 'services/baka_auth.dart';
import 'services/logger_service.dart';
import 'services/download_service.dart';
@@ -150,9 +151,11 @@ class HomeShell extends StatefulWidget {
State<HomeShell> createState() => _HomeShellState();
}
class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
class _HomeShellState extends State<HomeShell>
with WidgetsBindingObserver, TickerProviderStateMixin {
int _index = 0;
late final AutoScan _autoScan;
late final PlayerExpansionController _expansion;
/// Adresse aus einem „Teilen"-Aufruf, die der Downloader übernehmen soll.
String? _geteilteAdresse;
@@ -162,6 +165,7 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
@override
void initState() {
super.initState();
_expansion = PlayerExpansionController(vsync: this);
final db = context.read<MeloDb>();
final lib = context.read<LibraryService>();
_autoScan = AutoScan(
@@ -190,6 +194,7 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
@override
void dispose() {
_expansion.dispose();
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@@ -222,47 +227,67 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
const FavoritesScreen(),
const YoutubeSearchScreen(),
];
return PopScope(
// Tab-Wechsel laufen nur über setState, nie über Navigator.push — die
// Zurück-Geste auf einem Nicht-Start-Tab hätte sonst nichts zum
// Poppen und würde die App minimieren statt zu "Meine Musik" zu gehen.
canPop: _index == 0,
onPopInvokedWithResult: (didPop, _) {
if (!didPop) setState(() => _index = 0);
},
child: Scaffold(
body: Column(
children: [
Expanded(child: IndexedStack(index: _index, children: tabs)),
const MiniPlayer(),
],
return ChangeNotifierProvider<PlayerExpansionController>.value(
value: _expansion,
// Consumer NUR um PopScope — sein `child:` (der Scaffold-Rumpf) wird
// laut Flutter-Semantik bei jedem _expansion-Tick NICHT neu gebaut,
// 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, _) {
if (didPop) return;
if (expansion.progress > 0) {
expansion.close(context);
} else {
setState(() => _index = 0);
}
},
child: child!,
),
// Haarlinie darüber: ohne sie geht die Leiste auf schwarzem Grund
// optisch im Inhalt auf und wirkt nicht wie ein Hauptmenü.
bottomNavigationBar: DecoratedBox(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: MeloTheme.border)),
child: Scaffold(
body: Column(
children: [
Expanded(child: IndexedStack(index: _index, children: tabs)),
const MiniPlayer(),
],
),
child: BottomNavigationBar(
currentIndex: _index,
onTap: _goTo,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.headphones), label: 'Meine Musik'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Suchen'),
BottomNavigationBarItem(
icon: Icon(Icons.download_outlined),
activeIcon: Icon(Icons.download),
label: 'Download'),
BottomNavigationBarItem(
icon: Icon(Icons.favorite_border),
activeIcon: Icon(Icons.favorite),
label: 'Favoriten'),
BottomNavigationBarItem(
icon: Icon(Icons.smart_display_outlined),
activeIcon: Icon(Icons.smart_display),
label: 'YT-Suche'),
],
// Haarlinie darüber: ohne sie geht die Leiste auf schwarzem Grund
// optisch im Inhalt auf und wirkt nicht wie ein Hauptmenü.
bottomNavigationBar: DecoratedBox(
decoration: const BoxDecoration(
border: Border(top: BorderSide(color: MeloTheme.border)),
),
child: BottomNavigationBar(
currentIndex: _index,
onTap: _goTo,
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.headphones), label: 'Meine Musik'),
BottomNavigationBarItem(icon: Icon(Icons.search), label: 'Suchen'),
BottomNavigationBarItem(
icon: Icon(Icons.download_outlined),
activeIcon: Icon(Icons.download),
label: 'Download'),
BottomNavigationBarItem(
icon: Icon(Icons.favorite_border),
activeIcon: Icon(Icons.favorite),
label: 'Favoriten'),
BottomNavigationBarItem(
icon: Icon(Icons.smart_display_outlined),
activeIcon: Icon(Icons.smart_display),
label: 'YT-Suche'),
],
),
),
),
),