diff --git a/lib/screens/now_playing_screen.dart b/lib/screens/now_playing_screen.dart index a3361685..fdcd0626 100644 --- a/lib/screens/now_playing_screen.dart +++ b/lib/screens/now_playing_screen.dart @@ -35,6 +35,7 @@ import 'package:spotiflac_android/utils/string_utils.dart'; import 'package:spotiflac_android/utils/synced_lyrics_scroll.dart'; import 'package:spotiflac_android/widgets/app_bottom_sheet.dart'; import 'package:spotiflac_android/widgets/app_loading_indicator.dart'; +import 'package:spotiflac_android/widgets/expressive_button.dart'; import 'package:spotiflac_android/widgets/expressive_icon_button.dart'; import 'package:spotiflac_android/widgets/aligned_lyric_pronunciation.dart'; import 'package:spotiflac_android/widgets/lyric_supplement_transition.dart'; @@ -1784,7 +1785,7 @@ class _NowPlayingScreenState extends ConsumerState { Widget _lyricsSection(ColorScheme colorScheme, {required bool isActive}) { if (_loadingMeta) { - return const Center(child: CircularProgressIndicator()); + return const Center(child: AppLoadingIndicator()); } if (_lyrics.isEmpty) { return Center( @@ -2396,26 +2397,33 @@ class _NowPlayingScreenState extends ConsumerState { padding: const EdgeInsets.fromLTRB(24, 4, 16, 8), child: Row( children: [ - Text( - context.l10n.nowPlayingUpNext, - style: textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - color: colorScheme.onSurface, + Expanded( + child: Text( + context.l10n.nowPlayingUpNext, + style: textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: colorScheme.onSurface, + ), ), ), - const Spacer(), - IconButton( + ExpressiveIconButton( tooltip: autoplay ? context.l10n.autoplayOn : context.l10n.autoplayOff, - isSelected: autoplay, + selected: autoplay, icon: const Icon(Icons.all_inclusive), - color: autoplay ? colorScheme.primary : null, + foregroundColor: autoplay + ? colorScheme.onSecondaryContainer + : colorScheme.onSurfaceVariant, + backgroundColor: autoplay + ? colorScheme.secondaryContainer + : null, onPressed: () => ref .read(settingsProvider.notifier) .setAutoplay(!autoplay), ), - IconButton( + const SizedBox(width: 4), + ExpressiveIconButton( tooltip: switch (repeatMode) { AudioServiceRepeatMode.one => context.l10n.nowPlayingRepeatOne, @@ -2423,15 +2431,19 @@ class _NowPlayingScreenState extends ConsumerState { context.l10n.nowPlayingRepeatOff, _ => context.l10n.nowPlayingRepeatAll, }, - isSelected: - repeatMode != AudioServiceRepeatMode.none, + selected: repeatMode != AudioServiceRepeatMode.none, icon: Icon( repeatMode == AudioServiceRepeatMode.one ? Icons.repeat_one : Icons.repeat, ), - color: repeatMode != AudioServiceRepeatMode.none - ? colorScheme.primary + foregroundColor: + repeatMode != AudioServiceRepeatMode.none + ? colorScheme.onSecondaryContainer + : colorScheme.onSurfaceVariant, + backgroundColor: + repeatMode != AudioServiceRepeatMode.none + ? colorScheme.secondaryContainer : null, onPressed: () => controller.setRepeatMode(switch (repeatMode) { @@ -2442,13 +2454,19 @@ class _NowPlayingScreenState extends ConsumerState { _ => AudioServiceRepeatMode.none, }), ), - IconButton( + const SizedBox(width: 4), + ExpressiveIconButton( tooltip: shuffleOn ? context.l10n.nowPlayingShuffleOn : context.l10n.nowPlayingPlayInOrder, - isSelected: shuffleOn, + selected: shuffleOn, icon: const Icon(Icons.shuffle), - color: shuffleOn ? colorScheme.primary : null, + foregroundColor: shuffleOn + ? colorScheme.onSecondaryContainer + : colorScheme.onSurfaceVariant, + backgroundColor: shuffleOn + ? colorScheme.secondaryContainer + : null, onPressed: () => controller.setShuffle(!shuffleOn), ), ], @@ -2458,10 +2476,11 @@ class _NowPlayingScreenState extends ConsumerState { padding: const EdgeInsets.fromLTRB(24, 0, 24, 8), child: SizedBox( width: double.infinity, - child: FilledButton.tonalIcon( + child: ExpressiveButton( + tonal: true, onPressed: () => _shuffleLibrary(controller), icon: const Icon(Icons.shuffle, size: 18), - label: Text(context.l10n.nowPlayingShuffleLibrary), + child: Text(context.l10n.nowPlayingShuffleLibrary), ), ), ), @@ -2501,17 +2520,25 @@ class _NowPlayingScreenState extends ConsumerState { enabled: !isCurrent, onRemove: () => controller.removeQueuedItem(item), child: ListTile( + selected: isCurrent, + selectedTileColor: colorScheme + .secondaryContainer + .withValues(alpha: 0.55), contentPadding: const EdgeInsets.only( left: 16, right: 4, ), - leading: Icon( - isCurrent - ? Icons.equalizer - : Icons.music_note, - color: isCurrent - ? colorScheme.primary - : colorScheme.onSurfaceVariant, + leading: ClipRRect( + borderRadius: BorderRadius.circular(8), + child: SizedBox.square( + dimension: 44, + child: PlayerArtwork( + artUri: item.artUri?.toString(), + colorScheme: colorScheme, + cacheWidth: 132, + iconSize: 22, + ), + ), ), title: Text( item.title, diff --git a/test/now_playing_lyrics_transition_test.dart b/test/now_playing_lyrics_transition_test.dart index 7337e5fe..c5d2d80d 100644 --- a/test/now_playing_lyrics_transition_test.dart +++ b/test/now_playing_lyrics_transition_test.dart @@ -40,6 +40,7 @@ import 'package:spotiflac_android/widgets/mornye_player_artwork.dart'; import 'package:spotiflac_android/widgets/mornye_artwork_contrast.dart'; import 'package:spotiflac_android/widgets/mini_player.dart'; import 'package:spotiflac_android/widgets/playback_seek_slider.dart'; +import 'package:spotiflac_android/widgets/player_artwork.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); @@ -1848,6 +1849,68 @@ void main() { ); } + for (final width in [320.0, 430.0]) { + testWidgets('Material queue shows covers and selected controls at $width', ( + tester, + ) async { + final queue = [item('first'), item('second')]; + final controller = _QueueController( + (index) => mediaItems.add(queue[index]), + ); + await pumpNowPlaying( + tester, + theme: AppTheme.dark(), + size: Size(width, 852), + queue: queue, + controller: controller, + playback: PlaybackState( + queueIndex: 0, + shuffleMode: AudioServiceShuffleMode.all, + repeatMode: AudioServiceRepeatMode.all, + ), + ); + mediaItems.add(queue.first); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.queue_music)); + await tester.pumpAndSettle(); + final rows = find.descendant( + of: find.byType(ReorderableListView), + matching: find.byType(ListTile), + ); + expect(rows, findsNWidgets(2)); + expect( + find.descendant(of: rows, matching: find.byType(PlayerArtwork)), + findsNWidgets(2), + ); + expect(tester.widgetList(rows).map((row) => row.selected), [ + true, + false, + ]); + final toggles = tester.widgetList( + find.descendant( + of: find.byType(DraggableScrollableSheet), + matching: find.byType(ExpressiveIconButton), + ), + ); + expect( + toggles.where((button) => button.selected == true).length, + greaterThanOrEqualTo(2), + ); + expect( + toggles + .where((button) => button.selected == true) + .every((button) => button.backgroundColor != null), + isTrue, + ); + await tester.tap( + find.descendant(of: rows, matching: find.text('Second')), + ); + await tester.pumpAndSettle(); + expect(controller.selected, [1]); + expect(tester.takeException(), isNull); + }); + } + testWidgets('Mornye player renders Apple-style transport controls', ( tester, ) async {