mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-29 04:42:02 +02:00
feat(player): show covers and expressive controls in Material queue
This commit is contained in:
@@ -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<NowPlayingScreen> {
|
||||
|
||||
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<NowPlayingScreen> {
|
||||
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<NowPlayingScreen> {
|
||||
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<NowPlayingScreen> {
|
||||
_ => 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<NowPlayingScreen> {
|
||||
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<NowPlayingScreen> {
|
||||
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,
|
||||
|
||||
@@ -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<ListTile>(rows).map((row) => row.selected), [
|
||||
true,
|
||||
false,
|
||||
]);
|
||||
final toggles = tester.widgetList<ExpressiveIconButton>(
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user