From 90b63c467e6d652267b6cce4368f1ccc4dce6cea Mon Sep 17 00:00:00 2001 From: "Hermes (Server)" Date: Thu, 27 Aug 2026 15:31:21 +0200 Subject: [PATCH] =?UTF-8?q?Zur=C3=BCck-Geste=20auf=20Nicht-Start-Tabs=20na?= =?UTF-8?q?vigiert=20zu=20Meine=20Musik=20statt=20die=20App=20zu=20minimie?= =?UTF-8?q?ren?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tab-Wechsel in HomeShell laufen nur über setState (IndexedStack + BottomNavigationBar), nie über Navigator.push — eine System-Zurück-Geste hatte auf Nicht-Start-Tabs also nie eine Route zum Poppen und minimierte stattdessen die App. build() jetzt in ein PopScope gewickelt, das bei _index != 0 zuerst zu Tab 0 zurückspringt. --- CHANGELOG.md | 15 ++++++++ lib/main.dart | 73 ++++++++++++++++++++++----------------- test/home_shell_test.dart | 67 +++++++++++++++++++++++++++++++++++ 3 files changed, 123 insertions(+), 32 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 82555e7..17836bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,21 @@ Format angelehnt an [Keep a Changelog](https://keepachangelog.com/de/1.0.0/). ## [Unreleased] +### ⬅️ Zurück-Geste auf Nicht-Start-Tabs minimierte die App (2026-08-27) + +- 🐛 **Auf "Favoriten" (oder einem anderen Tab außer "Meine Musik") tippen und + dann die System-Zurück-Geste nutzen minimierte die App**, statt zur + vorherigen Ansicht zu navigieren. Grund: Tab-Wechsel in `HomeShell` + laufen nur über `setState` (`IndexedStack` + `BottomNavigationBar`), nie + über `Navigator.push` — es lag also nie eine Route auf dem + Navigator-Stack, die eine Zurück-Geste hätte poppen können. +- 🔧 **Fix:** `build()` in `main.dart` in ein `PopScope` gewickelt + (`canPop: _index == 0`), das bei einem Nicht-Start-Tab zuerst zu Tab 0 + ("Meine Musik") zurückspringt, statt die App zu verlassen. +- ✅ **Getestet:** neuer Widget-Test in `home_shell_test.dart` (RED→GREEN, + simuliert die System-Zurück-Geste über `WidgetsApp.didPopRoute()`), volle + Suite (593 Tests) grün, `flutter analyze` ohne Befund. + ### 🔄 Doppelten Scan-Button auf Android entfernt (2026-08-27) - 🐛 **In den Einstellungen gab es auf Android zwei Buttons für dieselbe diff --git a/lib/main.dart b/lib/main.dart index e9fa0fc..fcaef9e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -222,39 +222,48 @@ class _HomeShellState extends State with WidgetsBindingObserver { const FavoritesScreen(), const YoutubeSearchScreen(), ]; - return Scaffold( - body: Column( - children: [ - Expanded(child: IndexedStack(index: _index, children: tabs)), - const MiniPlayer(), - ], - ), - // 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)), + 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(), + ], ), - 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'), + ], + ), ), ), ); diff --git a/test/home_shell_test.dart b/test/home_shell_test.dart index c5b87e7..23f82d8 100644 --- a/test/home_shell_test.dart +++ b/test/home_shell_test.dart @@ -72,4 +72,71 @@ void main() { handler.dispose(); await db.close(); }); + + testWidgets('Zurück-Geste auf einem Nicht-Start-Tab wechselt zu ' + '"Meine Musik" statt die App zu minimieren', (tester) async { + final db = MeloDb(NativeDatabase.memory()); + final lib = LibraryService(db); + final playlists = PlaylistService(db); + final handler = MeloAudioHandler(db: db); + final auth = BakaAuth(speicher: _MemorySpeicher()); + + await tester.pumpWidget(MultiProvider( + providers: [ + Provider.value(value: db), + ChangeNotifierProvider.value(value: lib), + ChangeNotifierProvider.value(value: playlists), + Provider.value(value: handler), + ChangeNotifierProvider(create: (_) => CategoryService(db)), + ChangeNotifierProvider(create: (_) => AppSettings()), + ChangeNotifierProvider(create: (_) => OfflineMode()), + ChangeNotifierProvider(create: (_) => auth), + ChangeNotifierProvider( + create: (_) => YtDownloadService(auth: auth)), + ChangeNotifierProvider( + create: (_) => YtSearchService(auth: auth)), + ChangeNotifierProvider( + create: (_) => DownloadService(db: db, navidrome: NavidromeService())), + ChangeNotifierProvider( + create: (_) => SyncService( + db: db, cloud: MeloCloudService(auth: auth))), + ], + child: const MaterialApp(home: HomeShell()), + )); + await tester.pump(); + + // Auf "Favoriten" wechseln (Index 3) — kein Navigator.push, nur der Tab. + await tester.tap(find.descendant( + of: find.byType(BottomNavigationBar), matching: find.text('Favoriten'))); + await tester.pump(); + expect( + tester + .widget(find.byType(BottomNavigationBar)) + .currentIndex, + 3); + + // System-Zurück-Geste simulieren — derselbe Weg, über den auch das + // echte Android-Zurück-Gesten-Signal bei WidgetsApp ankommt. + final widgetsAppState = tester.state(find.byType(WidgetsApp)); + // ignore: avoid_dynamic_calls + await (widgetsAppState as dynamic).didPopRoute(); + await tester.pump(); + + // Ohne Abfangen bliebe der Tab unverändert (Flutter minimiert dann die + // App statt zu navigieren) — die Geste muss stattdessen zu Tab 0 führen. + expect( + tester + .widget(find.byType(BottomNavigationBar)) + .currentIndex, + 0); + + // Baum abbauen, bevor der Test endet: der Positions-Timer des Audio- + // Handlers läuft sonst über das Testende hinaus weiter, und der + // Test-Rahmen meldet einen offenen Timer (wie in hauptmenue_test.dart + // dokumentiert). Aus demselben Grund bewusst kein db.close() nach + // einem tester.tap. + await tester.pumpWidget(const SizedBox()); + handler.dispose(); + await tester.pump(const Duration(milliseconds: 1)); + }); }