mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-28 20:32:15 +02:00
feat(player): swipe track headers and remove individual queue entries
This commit is contained in:
@@ -375,6 +375,37 @@ void main() {
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
'queue removal preserves duplicates, current audio and unshuffled order',
|
||||
() async {
|
||||
await handler.setQueueAndPlay([
|
||||
_tracks[0],
|
||||
_tracks[0],
|
||||
_tracks[1],
|
||||
_tracks[2],
|
||||
]);
|
||||
final original = List.of(handler.queue.value);
|
||||
await handler.setShuffleMode(AudioServiceShuffleMode.all);
|
||||
handler.removeQueuedItem(original[1]);
|
||||
handler.removeQueuedItem(original[1]); // A stale swipe is harmless.
|
||||
handler.removeQueuedItem(
|
||||
original[0],
|
||||
); // The playing occurrence is protected.
|
||||
await handler.setShuffleMode(AudioServiceShuffleMode.none);
|
||||
expect(handler.queue.value.map((item) => item.id), [
|
||||
'one',
|
||||
'two',
|
||||
'three',
|
||||
]);
|
||||
expect(native.resumedSources, ['/one.flac']);
|
||||
await handler.skipToNext();
|
||||
handler.removeQueuedItem(original[0]);
|
||||
expect(handler.playbackState.value.queueIndex, 0);
|
||||
expect(handler.mediaItem.value?.id, 'two');
|
||||
expect(native.sources['music-player'], '/two.flac');
|
||||
},
|
||||
);
|
||||
|
||||
test('restored shuffle can return to the saved original order', () async {
|
||||
await handler.restoreSession(
|
||||
items: [_tracks[1], _tracks[2], _tracks[0]],
|
||||
|
||||
@@ -105,6 +105,7 @@ void main() {
|
||||
Widget Function(Widget)? wrapPlayer,
|
||||
MotionArtwork? motionArtwork,
|
||||
MusicPlayerController? controller,
|
||||
List<MediaItem> queue = const [],
|
||||
}) async {
|
||||
tester.view.physicalSize = size;
|
||||
tester.view.devicePixelRatio = 1;
|
||||
@@ -141,7 +142,7 @@ void main() {
|
||||
? const Stream.empty()
|
||||
: Stream.value(playback)),
|
||||
),
|
||||
playQueueProvider.overrideWith((ref) => const Stream.empty()),
|
||||
playQueueProvider.overrideWith((ref) => Stream.value(queue)),
|
||||
systemVolumeProvider.overrideWith((ref) => Stream.value(0.5)),
|
||||
systemVolumeWriterProvider.overrideWith(
|
||||
(ref) =>
|
||||
@@ -160,6 +161,56 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
for (final page in ['player', 'lyrics', 'queue']) {
|
||||
testWidgets('cover swipe changes the track in the Mornye $page', (
|
||||
tester,
|
||||
) async {
|
||||
final queue = [item('first'), item('second')];
|
||||
final controller = _QueueController(
|
||||
(index) => mediaItems.add(queue[index]),
|
||||
);
|
||||
await pumpNowPlaying(
|
||||
tester,
|
||||
theme: MornyeTheme.build(Brightness.dark),
|
||||
size: const Size(393, 852),
|
||||
queue: queue,
|
||||
controller: controller,
|
||||
playback: PlaybackState(queueIndex: 0),
|
||||
);
|
||||
mediaItems.add(queue.first);
|
||||
await tester.pumpAndSettle();
|
||||
if (page != 'player') {
|
||||
await tester.tap(
|
||||
find.byIcon(
|
||||
page == 'lyrics'
|
||||
? CupertinoIcons.quote_bubble
|
||||
: CupertinoIcons.list_bullet,
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
final cover = find
|
||||
.byKey(
|
||||
ValueKey(
|
||||
page == 'player'
|
||||
? 'full-player-artwork'
|
||||
: 'compact-player-artwork',
|
||||
),
|
||||
)
|
||||
.hitTestable();
|
||||
final rect = tester.getRect(cover);
|
||||
final gesture = await tester.startGesture(rect.center);
|
||||
await gesture.moveBy(const Offset(110, 0));
|
||||
await tester.pump();
|
||||
expect(tester.getRect(cover), rect);
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(controller.selected, [1]);
|
||||
expect(find.text('Second').hitTestable(), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets(
|
||||
'portrait motion cover keeps the raised square-cover control positions',
|
||||
(tester) async {
|
||||
@@ -3139,6 +3190,18 @@ void main() {
|
||||
});
|
||||
}
|
||||
|
||||
class _QueueController extends MusicPlayerController {
|
||||
_QueueController(this.onSelected);
|
||||
final void Function(int) onSelected;
|
||||
final selected = <int>[];
|
||||
|
||||
@override
|
||||
Future<void> jumpTo(int index) async {
|
||||
selected.add(index);
|
||||
onSelected(index);
|
||||
}
|
||||
}
|
||||
|
||||
class _SeekController extends MusicPlayerController {
|
||||
final seeks = <Duration>[];
|
||||
final completions = <Completer<void>>[];
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
import 'package:audio_service/audio_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/widgets/player_queue_dismissible.dart';
|
||||
import 'package:spotiflac_android/widgets/player_track_swipe.dart';
|
||||
|
||||
void main() {
|
||||
final queue = [
|
||||
for (var i = 0; i < 3; i++)
|
||||
MediaItem(id: '$i', title: 'Song $i', artist: 'Artist $i'),
|
||||
];
|
||||
|
||||
for (final reducedMotion in [false, true]) {
|
||||
testWidgets(
|
||||
'cover drag moves only titles and selects once ($reducedMotion)',
|
||||
(tester) async {
|
||||
var index = 0;
|
||||
final selected = <int>[];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: MediaQuery(
|
||||
data: MediaQueryData(disableAnimations: reducedMotion),
|
||||
child: StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
return PlayerTrackSwipe(
|
||||
queue: queue,
|
||||
currentIndex: index,
|
||||
onSelected: (item) async {
|
||||
selected.add(queue.indexOf(item));
|
||||
setState(() => index = queue.indexOf(item));
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Center(
|
||||
child: SizedBox(
|
||||
width: 300,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const PlayerTrackSwipeRegion(
|
||||
child: SizedBox(
|
||||
key: ValueKey('cover'),
|
||||
width: 200,
|
||||
height: 200,
|
||||
),
|
||||
),
|
||||
PlayerTrackSwipeTitles(
|
||||
current: queue[index],
|
||||
builder: (item) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(item.title),
|
||||
Text(item.artist!),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final cover = find.byKey(const ValueKey('cover'));
|
||||
final coverRect = tester.getRect(cover);
|
||||
final titleX = tester.getTopLeft(find.text('Song 0')).dx;
|
||||
final gesture = await tester.startGesture(coverRect.center);
|
||||
await gesture.moveBy(const Offset(110, 0));
|
||||
await tester.pump();
|
||||
expect(tester.getRect(cover), coverRect);
|
||||
expect(tester.getTopLeft(find.text('Song 0')).dx, greaterThan(titleX));
|
||||
expect(find.text('Song 1'), findsOneWidget);
|
||||
await gesture.up();
|
||||
await tester.pumpAndSettle();
|
||||
expect(selected, [1]);
|
||||
expect(tester.getTopLeft(find.text('Song 1')).dx, closeTo(titleX, 0.1));
|
||||
|
||||
await tester.drag(find.text('Song 1'), const Offset(-180, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(selected, [1, 0]);
|
||||
// At the first song the previous direction resists, then returns.
|
||||
await tester.drag(cover, const Offset(-200, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(selected, [1, 0]);
|
||||
expect(tester.getTopLeft(find.text('Song 0')).dx, closeTo(titleX, 0.1));
|
||||
await tester.drag(cover, const Offset(20, 0));
|
||||
await tester.pumpAndSettle();
|
||||
expect(selected, [1, 0]);
|
||||
expect(tester.takeException(), isNull);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
testWidgets('queue left swipe removes just that occurrence', (tester) async {
|
||||
final entries = [queue[0], queue[0].copyWith(), queue[1]];
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: StatefulBuilder(
|
||||
builder: (context, setState) => Scaffold(
|
||||
body: Column(
|
||||
children: [
|
||||
for (final item in entries)
|
||||
PlayerQueueDismissible(
|
||||
key: ObjectKey(item),
|
||||
enabled: !identical(item, entries.first),
|
||||
onRemove: () => setState(() {
|
||||
entries.removeWhere((entry) => identical(entry, item));
|
||||
}),
|
||||
child: SizedBox(
|
||||
width: 320,
|
||||
height: 64,
|
||||
child: Text(item.title),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.drag(
|
||||
find.byType(PlayerQueueDismissible).at(1),
|
||||
const Offset(280, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(entries, hasLength(3));
|
||||
await tester.drag(
|
||||
find.byType(PlayerQueueDismissible).at(1),
|
||||
const Offset(-280, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(entries.map((item) => item.id), ['0', '1']);
|
||||
await tester.drag(
|
||||
find.byType(PlayerQueueDismissible).first,
|
||||
const Offset(-280, 0),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(entries, hasLength(2));
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user