diff --git a/lib/widgets/preview_button.dart b/lib/widgets/preview_button.dart index 33142f6d..13cf1677 100644 --- a/lib/widgets/preview_button.dart +++ b/lib/widgets/preview_button.dart @@ -39,8 +39,12 @@ class PreviewButton extends ConsumerWidget { // When the built-in player is currently on this track, mirror and control // it (consistent with the mini player) rather than the preview snippet. - final mainItem = ref.watch(currentMediaItemProvider).value; - if (_isCurrentMainTrack(mainItem)) { + final isCurrentMainTrack = ref.watch( + currentMediaItemProvider.select( + (item) => _isCurrentMainTrack(item.value), + ), + ); + if (isCurrentMainTrack) { final isPlaying = ref.watch(playbackPlayingProvider); return IconButton( iconSize: size, @@ -62,9 +66,13 @@ class PreviewButton extends ConsumerWidget { if (!track.hasPreview) return const SizedBox.shrink(); - final previewState = ref.watch(previewPlayerProvider); - final isActive = previewState.isActiveUrl(track.previewUrl); - final status = isActive ? previewState.status : PreviewStatus.idle; + final status = ref.watch( + previewPlayerProvider.select( + (state) => state.isActiveUrl(track.previewUrl) + ? state.status + : PreviewStatus.idle, + ), + ); final Widget icon; final String tooltip; diff --git a/test/preview_button_layout_test.dart b/test/preview_button_layout_test.dart index 32164132..effeb85d 100644 --- a/test/preview_button_layout_test.dart +++ b/test/preview_button_layout_test.dart @@ -7,6 +7,7 @@ import 'package:spotiflac_android/models/settings.dart'; import 'package:spotiflac_android/models/track.dart'; import 'package:spotiflac_android/providers/library_collections_provider.dart'; import 'package:spotiflac_android/providers/music_player_provider.dart'; +import 'package:spotiflac_android/providers/preview_player_provider.dart'; import 'package:spotiflac_android/providers/settings_provider.dart'; import 'package:spotiflac_android/widgets/preview_button.dart'; import 'package:spotiflac_android/widgets/track_collection_quick_actions.dart'; @@ -79,6 +80,69 @@ void main() { expectCenteredHitbox(tester, Icons.play_circle_fill_rounded); }); + testWidgets('preview progress does not rebuild track buttons', ( + tester, + ) async { + final player = _TestPreviewPlayer(); + var builds = 0; + await tester.pumpWidget( + ProviderScope( + overrides: [ + currentMediaItemProvider.overrideWith((ref) => Stream.value(null)), + previewPlayerProvider.overrideWith(() => player), + ], + child: MaterialApp( + localizationsDelegates: AppLocalizations.localizationsDelegates, + supportedLocales: AppLocalizations.supportedLocales, + home: Scaffold( + body: _CountingPreviewButton(track: track, onBuild: () => builds++), + ), + ), + ), + ); + await tester.pumpAndSettle(); + player.emit( + PreviewPlayerState( + activeUrl: track.previewUrl, + status: PreviewStatus.playing, + ), + ); + await tester.pump(); + expect(find.byIcon(Icons.pause_circle_filled_rounded), findsOneWidget); + final playingBuilds = builds; + for (var second = 1; second <= 3; second++) { + player.emit( + PreviewPlayerState( + activeUrl: track.previewUrl, + status: PreviewStatus.playing, + position: Duration(seconds: second), + duration: const Duration(seconds: 30), + ), + ); + await tester.pump(); + } + expect(builds, playingBuilds); + + player.emit( + PreviewPlayerState( + activeUrl: track.previewUrl, + status: PreviewStatus.paused, + ), + ); + await tester.pump(); + expect(find.byIcon(Icons.play_circle_fill_rounded), findsOneWidget); + expect(builds, playingBuilds + 1); + + player.emit( + const PreviewPlayerState( + activeUrl: 'https://example.com/another-preview.mp3', + status: PreviewStatus.playing, + ), + ); + await tester.pump(); + expect(find.byIcon(Icons.play_circle_outline_rounded), findsOneWidget); + }); + testWidgets('overflow menu icon is centered in its adjacent hitbox', ( tester, ) async { @@ -157,6 +221,25 @@ class _TestSettingsNotifier extends SettingsNotifier { AppSettings build() => const AppSettings(); } +class _TestPreviewPlayer extends PreviewPlayerController { + @override + PreviewPlayerState build() => const PreviewPlayerState(); + + void emit(PreviewPlayerState next) => state = next; +} + +class _CountingPreviewButton extends PreviewButton { + const _CountingPreviewButton({required super.track, required this.onBuild}); + + final VoidCallback onBuild; + + @override + Widget build(BuildContext context, WidgetRef ref) { + onBuild(); + return super.build(context, ref); + } +} + class _TestLibraryCollectionsNotifier extends LibraryCollectionsNotifier { @override LibraryCollectionsState build() => LibraryCollectionsState(isLoaded: true);