From bb057fd210a1b51907caf169bb6024c8ba7304dc Mon Sep 17 00:00:00 2001 From: zarzet <42882290+zarzet@users.noreply.github.com> Date: Fri, 25 Sep 2026 01:20:40 +0700 Subject: [PATCH] fix(library): keep album batch actions alive after selection closes Use the album screen context for ReplayGain and conversion so dismissing the selection overlay does not invalidate the pending operation. Cover local and downloaded albums in both themes. --- lib/screens/downloaded_album_screen.dart | 4 +- lib/screens/local_album_screen.dart | 4 +- test/album_replaygain_action_test.dart | 122 +++++++++++++++++++++++ 3 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 test/album_replaygain_action_test.dart diff --git a/lib/screens/downloaded_album_screen.dart b/lib/screens/downloaded_album_screen.dart index 6ec56726..903ac62f 100644 --- a/lib/screens/downloaded_album_screen.dart +++ b/lib/screens/downloaded_album_screen.dart @@ -738,7 +738,7 @@ class _DownloadedAlbumScreenState extends ConsumerState label: context.l10n.selectionConvertCount(selectedCount), onPressed: selectedCount > 0 ? () => showBatchConvertSheet( - context, + this.context, ref, _selectedUnifiedItems(tracks), onExitSelectionMode: exitSelectionMode, @@ -751,7 +751,7 @@ class _DownloadedAlbumScreenState extends ConsumerState label: context.l10n.selectionReplayGainCount(selectedCount), onPressed: selectedCount > 0 ? () => runBatchReplayGain( - context, + this.context, _selectedUnifiedItems(tracks), onExitSelectionMode: exitSelectionMode, ) diff --git a/lib/screens/local_album_screen.dart b/lib/screens/local_album_screen.dart index 4ebd3d67..e618787b 100644 --- a/lib/screens/local_album_screen.dart +++ b/lib/screens/local_album_screen.dart @@ -547,7 +547,7 @@ class _LocalAlbumScreenState extends ConsumerState label: context.l10n.selectionConvertCount(selectedCount), onPressed: selectedCount > 0 ? () => showBatchConvertSheet( - context, + this.context, ref, _selectedUnifiedItems(tracks), onExitSelectionMode: exitSelectionMode, @@ -563,7 +563,7 @@ class _LocalAlbumScreenState extends ConsumerState label: context.l10n.selectionReplayGainCount(selectedCount), onPressed: selectedCount > 0 ? () => runBatchReplayGain( - context, + this.context, _selectedUnifiedItems(tracks), onExitSelectionMode: exitSelectionMode, ) diff --git a/test/album_replaygain_action_test.dart b/test/album_replaygain_action_test.dart new file mode 100644 index 00000000..8311edec --- /dev/null +++ b/test/album_replaygain_action_test.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:spotiflac_android/l10n/l10n.dart'; +import 'package:spotiflac_android/providers/download_history_provider.dart'; +import 'package:spotiflac_android/screens/downloaded_album_screen.dart'; +import 'package:spotiflac_android/screens/local_album_screen.dart'; +import 'package:spotiflac_android/services/library_database.dart'; +import 'package:spotiflac_android/theme/app_theme.dart'; +import 'package:spotiflac_android/theme/mornye_theme.dart'; +import 'package:spotiflac_android/widgets/app_alert_dialog.dart'; +import 'package:spotiflac_android/widgets/selection_bottom_bar.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + const channel = MethodChannel('com.zarz.spotiflac/backend'); + final messenger = + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger; + + for (final mornye in [false, true]) { + for (final downloaded in [false, true]) { + testWidgets( + 'ReplayGain survives hiding the album selection (Mornye: $mornye, downloaded: $downloaded)', + (tester) async { + SharedPreferences.setMockInitialValues({}); + var attempts = 0; + messenger.setMockMethodCallHandler(channel, (call) async { + if (call.method == 'safCopyToTemp') attempts++; + // Simulate an inaccessible file. Reaching this call and showing + // the result proves confirmation actually starts the operation. + return null; + }); + addTearDown(() => messenger.setMockMethodCallHandler(channel, null)); + const path = 'content://library/document/track.flac'; + final item = DownloadHistoryItem( + id: 'track', + trackName: 'Track', + artistName: 'Artist', + albumName: 'Album', + filePath: path, + service: 'provider-a', + downloadedAt: DateTime(2026), + ); + await tester.pumpWidget( + ProviderScope( + overrides: [ + downloadedAlbumTracksProvider( + const DownloadedAlbumTracksRequest( + albumName: 'Album', + artistName: 'Artist', + ), + ).overrideWith((ref) async => [item]), + ], + child: MaterialApp( + theme: mornye + ? MornyeTheme.build(Brightness.light) + : AppTheme.light(), + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: SelectionOverlayHost( + child: downloaded + ? const DownloadedAlbumScreen( + albumName: 'Album', + artistName: 'Artist', + ) + : LocalAlbumScreen( + albumName: 'Album', + artistName: 'Artist', + tracks: [ + LocalLibraryItem( + id: 'track', + trackName: 'Track', + artistName: 'Artist', + albumName: 'Album', + filePath: path, + scannedAt: DateTime(2026), + ), + ], + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.scrollUntilVisible(find.text('Track'), 200); + await tester.longPress(find.text('Track')); + await tester.pumpAndSettle(); + final l10n = AppLocalizations.of( + tester.element(find.byType(SelectionBottomBar)), + ); + Future openConfirmation() async { + await tester.tap(find.text(l10n.selectionReplayGainCount(1))); + await tester.pumpAndSettle(); + expect(find.byType(SelectionBottomBar), findsNothing); + expect(find.byType(AppAlertDialog), findsOneWidget); + } + + await openConfirmation(); + await tester.tap(find.text(l10n.dialogCancel)); + await tester.pumpAndSettle(); + expect(attempts, 0); + expect(find.byType(SelectionBottomBar), findsOneWidget); + + await openConfirmation(); + await tester.tap( + find.descendant( + of: find.byType(AppDialogAction), + matching: find.text(l10n.replayGainBatchConfirmTitle), + ), + ); + await tester.pumpAndSettle(); + expect(attempts, 1); + expect(find.text(l10n.replayGainBatchSuccess(0, 1)), findsOneWidget); + expect(find.byType(SelectionBottomBar), findsNothing); + expect(tester.takeException(), isNull); + }, + ); + } + } +}