From eabafecaeb4cc2afbf41a92c85d516f2d8cee6e8 Mon Sep 17 00:00:00 2001 From: zarzet Date: Fri, 24 Jul 2026 22:19:03 +0700 Subject: [PATCH] fix(navigation): open download queue from snackbar actions --- lib/screens/artist_screen.dart | 6 +- lib/screens/home_tab.dart | 11 +-- lib/screens/main_shell.dart | 18 +++++ lib/services/shell_navigation_service.dart | 25 ++++++ lib/widgets/view_queue_snackbar_action.dart | 12 +++ test/shell_navigation_service_test.dart | 90 +++++++++++++++++++++ 6 files changed, 150 insertions(+), 12 deletions(-) create mode 100644 lib/widgets/view_queue_snackbar_action.dart create mode 100644 test/shell_navigation_service_test.dart diff --git a/lib/screens/artist_screen.dart b/lib/screens/artist_screen.dart index b1e0db5b..42283120 100644 --- a/lib/screens/artist_screen.dart +++ b/lib/screens/artist_screen.dart @@ -30,6 +30,7 @@ import 'package:spotiflac_android/utils/clickable_metadata.dart'; import 'package:spotiflac_android/widgets/cached_cover_image.dart'; import 'package:spotiflac_android/widgets/motion_header_banner.dart'; import 'package:spotiflac_android/widgets/cross_extension_share_sheet.dart'; +import 'package:spotiflac_android/widgets/view_queue_snackbar_action.dart'; class _ArtistCache { static final _cache = TtlCache<_CacheEntry>(const Duration(minutes: 10)); @@ -1055,10 +1056,7 @@ class _ArtistScreenState extends ConsumerState ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(message), - action: SnackBarAction( - label: context.l10n.snackbarViewQueue, - onPressed: () {}, - ), + action: buildViewQueueSnackBarAction(context), ), ); } diff --git a/lib/screens/home_tab.dart b/lib/screens/home_tab.dart index c2fdd75d..a766fa10 100644 --- a/lib/screens/home_tab.dart +++ b/lib/screens/home_tab.dart @@ -37,6 +37,7 @@ import 'package:spotiflac_android/widgets/error_card.dart'; import 'package:spotiflac_android/widgets/in_library_badge.dart'; import 'package:spotiflac_android/widgets/preview_button.dart'; import 'package:spotiflac_android/widgets/settings_group.dart'; +import 'package:spotiflac_android/widgets/view_queue_snackbar_action.dart'; part 'home_tab_helpers.dart'; part 'home_tab_widgets.dart'; @@ -1077,10 +1078,7 @@ class _HomeTabState extends ConsumerState ScaffoldMessenger.of(this.context).showSnackBar( SnackBar( content: Text(queueSnackbarMessage), - action: SnackBarAction( - label: l10n.snackbarViewQueue, - onPressed: () {}, - ), + action: buildViewQueueSnackBarAction(this.context), ), ); } @@ -1109,10 +1107,7 @@ class _HomeTabState extends ConsumerState ScaffoldMessenger.of(this.context).showSnackBar( SnackBar( content: Text(queueSnackbarMessage), - action: SnackBarAction( - label: l10n.snackbarViewQueue, - onPressed: () {}, - ), + action: buildViewQueueSnackBarAction(this.context), ), ); } diff --git a/lib/screens/main_shell.dart b/lib/screens/main_shell.dart index f33c3715..ddd18134 100644 --- a/lib/screens/main_shell.dart +++ b/lib/screens/main_shell.dart @@ -105,6 +105,10 @@ class _MainShellState extends ConsumerState duration: const Duration(milliseconds: 180), value: 1, ); + ShellNavigationService.registerTabSelectionHandler( + owner: this, + handler: _onShellTabRequested, + ); ShellNavigationService.syncState( currentTabIndex: _currentIndex, showRepoTab: false, @@ -433,6 +437,7 @@ class _MainShellState extends ConsumerState @override void dispose() { WidgetsBinding.instance.removeObserver(this); + ShellNavigationService.unregisterTabSelectionHandler(this); _shareSubscription?.cancel(); _pageController.dispose(); _tabJumpTransitionController.dispose(); @@ -452,6 +457,19 @@ class _MainShellState extends ConsumerState ref.read(trackProvider.notifier).clear(); } + void _onShellTabRequested(ShellTab tab) { + final showStore = ref.read( + settingsProvider.select((s) => s.showExtensionStore), + ); + final index = switch (tab) { + ShellTab.home => 0, + ShellTab.library => 1, + ShellTab.repository => showStore ? 2 : null, + ShellTab.settings => showStore ? 3 : 2, + }; + if (index != null) _onNavTap(index); + } + void _onNavTap(int index) { if (index == 0 && _currentIndex == 0) { _resetHomeToMain(); diff --git a/lib/services/shell_navigation_service.dart b/lib/services/shell_navigation_service.dart index 4d79e152..d229c3c6 100644 --- a/lib/services/shell_navigation_service.dart +++ b/lib/services/shell_navigation_service.dart @@ -1,5 +1,7 @@ import 'package:flutter/widgets.dart'; +enum ShellTab { home, library, repository, settings } + class ShellNavigationService { static final GlobalKey homeTabNavigatorKey = GlobalKey(); @@ -10,6 +12,29 @@ class ShellNavigationService { static int _currentTabIndex = 0; static bool _showRepoTab = false; + static Object? _tabSelectionOwner; + static ValueChanged? _tabSelectionHandler; + + static void registerTabSelectionHandler({ + required Object owner, + required ValueChanged handler, + }) { + _tabSelectionOwner = owner; + _tabSelectionHandler = handler; + } + + static void unregisterTabSelectionHandler(Object owner) { + if (!identical(_tabSelectionOwner, owner)) return; + _tabSelectionOwner = null; + _tabSelectionHandler = null; + } + + static bool requestTab(ShellTab tab) { + final handler = _tabSelectionHandler; + if (handler == null) return false; + handler(tab); + return true; + } static void syncState({ required int currentTabIndex, diff --git a/lib/widgets/view_queue_snackbar_action.dart b/lib/widgets/view_queue_snackbar_action.dart new file mode 100644 index 00000000..8bcadb5b --- /dev/null +++ b/lib/widgets/view_queue_snackbar_action.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/services/shell_navigation_service.dart'; + +SnackBarAction buildViewQueueSnackBarAction(BuildContext context) { + return SnackBarAction( + label: context.l10n.snackbarViewQueue, + onPressed: () { + ShellNavigationService.requestTab(ShellTab.library); + }, + ); +} diff --git a/test/shell_navigation_service_test.dart b/test/shell_navigation_service_test.dart new file mode 100644 index 00000000..382c35a8 --- /dev/null +++ b/test/shell_navigation_service_test.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:spotiflac_android/l10n/app_localizations.dart'; +import 'package:spotiflac_android/services/shell_navigation_service.dart'; +import 'package:spotiflac_android/widgets/view_queue_snackbar_action.dart'; + +void main() { + group('ShellNavigationService tab requests', () { + test('forwards a named tab request to the registered shell', () { + final owner = Object(); + ShellTab? requestedTab; + addTearDown( + () => ShellNavigationService.unregisterTabSelectionHandler(owner), + ); + + ShellNavigationService.registerTabSelectionHandler( + owner: owner, + handler: (tab) => requestedTab = tab, + ); + + expect(ShellNavigationService.requestTab(ShellTab.library), isTrue); + expect(requestedTab, ShellTab.library); + }); + + test('does not remove a newer shell handler', () { + final oldOwner = Object(); + final currentOwner = Object(); + ShellTab? requestedTab; + addTearDown( + () => + ShellNavigationService.unregisterTabSelectionHandler(currentOwner), + ); + + ShellNavigationService.registerTabSelectionHandler( + owner: oldOwner, + handler: (_) {}, + ); + ShellNavigationService.registerTabSelectionHandler( + owner: currentOwner, + handler: (tab) => requestedTab = tab, + ); + + ShellNavigationService.unregisterTabSelectionHandler(oldOwner); + + expect(ShellNavigationService.requestTab(ShellTab.settings), isTrue); + expect(requestedTab, ShellTab.settings); + }); + + test('reports when no shell can handle the request', () { + final owner = Object(); + ShellNavigationService.registerTabSelectionHandler( + owner: owner, + handler: (_) {}, + ); + ShellNavigationService.unregisterTabSelectionHandler(owner); + + expect(ShellNavigationService.requestTab(ShellTab.library), isFalse); + }); + + testWidgets('View Queue snackbar action requests the Library tab', ( + tester, + ) async { + final owner = Object(); + ShellTab? requestedTab; + addTearDown( + () => ShellNavigationService.unregisterTabSelectionHandler(owner), + ); + ShellNavigationService.registerTabSelectionHandler( + owner: owner, + handler: (tab) => requestedTab = tab, + ); + + await tester.pumpWidget( + MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: Builder( + builder: (context) => buildViewQueueSnackBarAction(context), + ), + ), + ), + ); + + await tester.tap(find.text('View Queue')); + + expect(requestedTab, ShellTab.library); + }); + }); +}