Zurück-Geste auf Nicht-Start-Tabs navigiert zu Meine Musik statt die App zu minimieren
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.
This commit is contained in:
@@ -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
|
||||
|
||||
+41
-32
@@ -222,39 +222,48 @@ class _HomeShellState extends State<HomeShell> 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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<MeloDb>.value(value: db),
|
||||
ChangeNotifierProvider<LibraryService>.value(value: lib),
|
||||
ChangeNotifierProvider<PlaylistService>.value(value: playlists),
|
||||
Provider<MeloAudioHandler>.value(value: handler),
|
||||
ChangeNotifierProvider<CategoryService>(create: (_) => CategoryService(db)),
|
||||
ChangeNotifierProvider<AppSettings>(create: (_) => AppSettings()),
|
||||
ChangeNotifierProvider<OfflineMode>(create: (_) => OfflineMode()),
|
||||
ChangeNotifierProvider<BakaAuth>(create: (_) => auth),
|
||||
ChangeNotifierProvider<YtDownloadService>(
|
||||
create: (_) => YtDownloadService(auth: auth)),
|
||||
ChangeNotifierProvider<YtSearchService>(
|
||||
create: (_) => YtSearchService(auth: auth)),
|
||||
ChangeNotifierProvider<DownloadService>(
|
||||
create: (_) => DownloadService(db: db, navidrome: NavidromeService())),
|
||||
ChangeNotifierProvider<SyncService>(
|
||||
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<BottomNavigationBar>(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<BottomNavigationBar>(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));
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user