Tinker-Feedback: 5 Bugfixes (Shuffle/Repeat, Full-Cover, Auto-Play, Scan-Button, Back-Geste) #6

Open
dustin wants to merge 10 commits from fix/tinker-feedback into fix/p0-vollwertigkeit
3 changed files with 123 additions and 32 deletions
Showing only changes of commit 90b63c467e - Show all commits
+15
View File
@@ -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
+10 -1
View File
@@ -222,7 +222,15 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
const FavoritesScreen(),
const YoutubeSearchScreen(),
];
return Scaffold(
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)),
@@ -257,6 +265,7 @@ class _HomeShellState extends State<HomeShell> with WidgetsBindingObserver {
],
),
),
),
);
}
}
+67
View File
@@ -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));
});
}