implemented shuffle mode
This commit is contained in:
parent
3178f0515f
commit
b7075aad27
12 changed files with 212 additions and 41 deletions
21
lib/domain/entities/shuffle_mode.dart
Normal file
21
lib/domain/entities/shuffle_mode.dart
Normal file
|
@ -0,0 +1,21 @@
|
|||
enum ShuffleMode {
|
||||
none,
|
||||
standard,
|
||||
plus
|
||||
}
|
||||
|
||||
extension ShuffleModeExtension on String {
|
||||
ShuffleMode toShuffleMode() {
|
||||
switch (this) {
|
||||
case 'ShuffleMode.none':
|
||||
return ShuffleMode.none;
|
||||
case 'ShuffleMode.standard':
|
||||
return ShuffleMode.standard;
|
||||
case 'ShuffleMode.plus':
|
||||
return ShuffleMode.plus;
|
||||
default:
|
||||
// TODO: does this make sense? maybe throw an error?
|
||||
return ShuffleMode.none;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ import 'package:dartz/dartz.dart';
|
|||
|
||||
import '../../core/error/failures.dart';
|
||||
import '../entities/playback_state.dart';
|
||||
import '../entities/shuffle_mode.dart';
|
||||
import '../entities/song.dart';
|
||||
|
||||
abstract class AudioRepository {
|
||||
|
@ -10,10 +11,12 @@ abstract class AudioRepository {
|
|||
Stream<List<Song>> get queueStream;
|
||||
Stream<int> get queueIndexStream;
|
||||
Stream<int> get currentPositionStream;
|
||||
Stream<ShuffleMode> get shuffleModeStream;
|
||||
|
||||
Future<Either<Failure, void>> playSong(int index, List<Song> songList);
|
||||
Future<Either<Failure, void>> play();
|
||||
Future<Either<Failure, void>> pause();
|
||||
Future<Either<Failure, void>> skipToNext();
|
||||
Future<Either<Failure, void>> skipToPrevious();
|
||||
}
|
||||
Future<Either<Failure, void>> setShuffleMode(ShuffleMode shuffleMode);
|
||||
}
|
||||
|
|
|
@ -51,6 +51,7 @@ class QueuePage extends StatelessWidget {
|
|||
);
|
||||
case StreamStatus.waiting:
|
||||
case StreamStatus.done:
|
||||
default:
|
||||
return Container();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import 'package:meta/meta.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:mucke/domain/entities/shuffle_mode.dart';
|
||||
|
||||
import '../../domain/entities/playback_state.dart';
|
||||
import '../../domain/entities/song.dart';
|
||||
|
@ -38,6 +39,9 @@ abstract class _AudioStore with Store {
|
|||
@observable
|
||||
ObservableStream<int> queueIndexStream;
|
||||
|
||||
@observable
|
||||
ObservableStream<ShuffleMode> shuffleModeStream;
|
||||
|
||||
@action
|
||||
void init() {
|
||||
if (!_initialized) {
|
||||
|
@ -51,6 +55,8 @@ abstract class _AudioStore with Store {
|
|||
|
||||
queueIndexStream = _audioRepository.queueIndexStream.asObservable();
|
||||
|
||||
shuffleModeStream = _audioRepository.shuffleModeStream.asObservable(initialValue: ShuffleMode.none);
|
||||
|
||||
_disposers.add(autorun((_) {
|
||||
updateSong(currentSong.value);
|
||||
}));
|
||||
|
@ -93,6 +99,10 @@ abstract class _AudioStore with Store {
|
|||
_audioRepository.skipToPrevious();
|
||||
}
|
||||
|
||||
Future<void> setShuffleMode(ShuffleMode shuffleMode) async {
|
||||
_audioRepository.setShuffleMode(shuffleMode);
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> updateSong(Song streamValue) async {
|
||||
print('updateSong');
|
||||
|
|
|
@ -22,7 +22,7 @@ class NextIndicator extends StatelessWidget {
|
|||
return GestureDetector(
|
||||
onTap: () => onTapAction(context),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
padding: const EdgeInsets.all(10.0),
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
|
@ -32,8 +32,10 @@ class NextIndicator extends StatelessWidget {
|
|||
Icons.expand_less,
|
||||
color: Colors.white70,
|
||||
),
|
||||
if (index < queue.length - 1)
|
||||
NextSong(song: queue[index + 1]),
|
||||
NextSong(
|
||||
queue: queue,
|
||||
index: index,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
|
|
@ -3,29 +3,37 @@ import 'package:flutter/material.dart';
|
|||
import '../../domain/entities/song.dart';
|
||||
|
||||
class NextSong extends StatelessWidget {
|
||||
const NextSong({Key key, this.song}) : super(key: key);
|
||||
const NextSong({Key key, this.queue, this.index}) : super(key: key);
|
||||
|
||||
final Song song;
|
||||
final List<Song> queue;
|
||||
final int index;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white70,
|
||||
),
|
||||
children: [
|
||||
TextSpan(text: '${song.title}'),
|
||||
const TextSpan(text: ' • '),
|
||||
TextSpan(
|
||||
text: '${song.artist}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
if (index < queue.length - 1) {
|
||||
final Song song = queue[index + 1];
|
||||
return RichText(
|
||||
text: TextSpan(
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Colors.white70,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
children: [
|
||||
TextSpan(text: '${song.title}'),
|
||||
const TextSpan(text: ' • '),
|
||||
TextSpan(
|
||||
text: '${song.artist}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w300,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return Container(
|
||||
height: 16.0,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
|
|||
import 'next_button.dart';
|
||||
import 'play_pause_button.dart';
|
||||
import 'previous_button.dart';
|
||||
import 'shuffle_button.dart';
|
||||
|
||||
class PlaybackControl extends StatelessWidget {
|
||||
const PlaybackControl({Key key}) : super(key: key);
|
||||
|
@ -13,14 +14,19 @@ class PlaybackControl extends StatelessWidget {
|
|||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Row(
|
||||
children: const [
|
||||
Icon(Icons.repeat, size: 20.0),
|
||||
IconButton(
|
||||
icon: Icon(Icons.repeat, size: 20.0),
|
||||
onPressed: null,
|
||||
),
|
||||
PreviousButton(iconSize: 32.0),
|
||||
PlayPauseButton(
|
||||
circle: true,
|
||||
iconSize: 52.0,
|
||||
),
|
||||
NextButton(iconSize: 32.0),
|
||||
Icon(Icons.shuffle, size: 20.0),
|
||||
ShuffleButton(
|
||||
iconSize: 20.0,
|
||||
),
|
||||
],
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
),
|
||||
|
|
57
lib/presentation/widgets/shuffle_button.dart
Normal file
57
lib/presentation/widgets/shuffle_button.dart
Normal file
|
@ -0,0 +1,57 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import '../../domain/entities/shuffle_mode.dart';
|
||||
import '../state/audio_store.dart';
|
||||
|
||||
class ShuffleButton extends StatelessWidget {
|
||||
const ShuffleButton({Key key, this.iconSize = 20.0}) : super(key: key);
|
||||
|
||||
final double iconSize;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final AudioStore audioStore = Provider.of<AudioStore>(context);
|
||||
|
||||
return Observer(
|
||||
builder: (BuildContext context) {
|
||||
switch (audioStore.shuffleModeStream.value) {
|
||||
case ShuffleMode.none:
|
||||
return IconButton(
|
||||
icon: const Icon(
|
||||
Icons.shuffle,
|
||||
color: Colors.white24,
|
||||
),
|
||||
iconSize: iconSize,
|
||||
onPressed: () {
|
||||
audioStore.setShuffleMode(ShuffleMode.standard);
|
||||
},
|
||||
);
|
||||
case ShuffleMode.standard:
|
||||
return IconButton(
|
||||
icon: const Icon(
|
||||
Icons.shuffle,
|
||||
color: Colors.white,
|
||||
),
|
||||
iconSize: iconSize,
|
||||
onPressed: () {
|
||||
audioStore.setShuffleMode(ShuffleMode.none);
|
||||
},
|
||||
);
|
||||
default:
|
||||
return IconButton(
|
||||
icon: const Icon(
|
||||
Icons.shuffle,
|
||||
color: Colors.blue,
|
||||
),
|
||||
iconSize: iconSize,
|
||||
onPressed: () {
|
||||
audioStore.setShuffleMode(ShuffleMode.none);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ import 'dart:async';
|
|||
import 'package:audio_service/audio_service.dart';
|
||||
|
||||
import '../../domain/entities/playback_state.dart' as entity;
|
||||
import '../../domain/entities/shuffle_mode.dart';
|
||||
import '../models/playback_state_model.dart';
|
||||
import '../models/song_model.dart';
|
||||
import 'audio_manager_contract.dart';
|
||||
|
@ -10,18 +11,19 @@ import 'audio_player_task.dart';
|
|||
|
||||
typedef Conversion<S, T> = T Function(S);
|
||||
|
||||
// index geht verloren, wenn noch kein Subscriber vorhanden ist, weil die events nicht gebuffert werden (kann ich nicht ändern)
|
||||
// deshalb sollte sofort ein listener erstellt werden, der den jeweils letzten wert speichert
|
||||
// der fertige Stream nimmt dann diesen letzten Wert, wenn er keinen im source stream findet
|
||||
// so sollte kein memory leak entstehen, weil immer nur ein wert gebuffert wird
|
||||
|
||||
class AudioManagerImpl implements AudioManager {
|
||||
AudioManagerImpl() {
|
||||
// this listener prevents the loss of data, when custom events are sent but not used yet
|
||||
// the customEventStream only works, when there is a listener
|
||||
AudioService.customEventStream.listen((event) {
|
||||
final data = event as Map<String, dynamic>;
|
||||
if (data.containsKey(KEY_INDEX)) {
|
||||
_queueIndex = data[KEY_INDEX] as int;
|
||||
}
|
||||
if (data.containsKey(SET_SHUFFLE_MODE)) {
|
||||
final modeString = data[SET_SHUFFLE_MODE] as String;
|
||||
_shuffleMode = modeString.toShuffleMode();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -34,6 +36,7 @@ class AudioManagerImpl implements AudioManager {
|
|||
final Stream customEventStream = AudioService.customEventStream;
|
||||
|
||||
int _queueIndex;
|
||||
ShuffleMode _shuffleMode;
|
||||
|
||||
@override
|
||||
Stream<SongModel> get currentSongStream =>
|
||||
|
@ -72,6 +75,22 @@ class AudioManagerImpl implements AudioManager {
|
|||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<ShuffleMode> get shuffleModeStream => _shuffleModeStream(AudioService.customEventStream.cast());
|
||||
|
||||
Stream<ShuffleMode> _shuffleModeStream(Stream<Map<String, dynamic>> source) async* {
|
||||
if (_shuffleMode != null) {
|
||||
yield _shuffleMode;
|
||||
}
|
||||
|
||||
await for (final data in source) {
|
||||
if (data.containsKey(SET_SHUFFLE_MODE)) {
|
||||
final modeString = data[SET_SHUFFLE_MODE] as String;
|
||||
yield modeString.toShuffleMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<int> get currentPositionStream => _position().distinct();
|
||||
|
||||
|
@ -112,6 +131,11 @@ class AudioManagerImpl implements AudioManager {
|
|||
await AudioService.skipToPrevious();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setShuffleMode(ShuffleMode shuffleMode) async {
|
||||
await AudioService.customAction(SET_SHUFFLE_MODE, shuffleMode.toString());
|
||||
}
|
||||
|
||||
Stream<T> _filterStream<S, T>(Stream<S> stream, Conversion<S, T> fn) async* {
|
||||
T lastItem;
|
||||
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import 'package:mucke/domain/entities/shuffle_mode.dart';
|
||||
|
||||
import '../../domain/entities/playback_state.dart';
|
||||
import '../models/song_model.dart';
|
||||
|
||||
|
@ -9,10 +11,12 @@ abstract class AudioManager {
|
|||
Stream<int> get queueIndexStream;
|
||||
/// Current position in the song in milliseconds.
|
||||
Stream<int> get currentPositionStream;
|
||||
Stream<ShuffleMode> get shuffleModeStream;
|
||||
|
||||
Future<void> playSong(int index, List<SongModel> songList);
|
||||
Future<void> play();
|
||||
Future<void> pause();
|
||||
Future<void> skipToNext();
|
||||
Future<void> skipToPrevious();
|
||||
Future<void> setShuffleMode(ShuffleMode shuffleMode);
|
||||
}
|
|
@ -6,11 +6,13 @@ import 'package:just_audio/just_audio.dart';
|
|||
import 'package:moor/isolate.dart';
|
||||
import 'package:moor/moor.dart';
|
||||
|
||||
import '../../domain/entities/shuffle_mode.dart';
|
||||
import 'moor_music_data_source.dart';
|
||||
|
||||
const String INIT = 'INIT';
|
||||
const String PLAY_WITH_CONTEXT = 'PLAY_WITH_CONTEXT';
|
||||
const String APP_LIFECYCLE_RESUMED = 'APP_LIFECYCLE_RESUMED';
|
||||
const String SET_SHUFFLE_MODE = 'SET_SHUFFLE_MODE';
|
||||
|
||||
const String KEY_INDEX = 'INDEX';
|
||||
|
||||
|
@ -21,6 +23,9 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
final _mediaItems = <String, MediaItem>{};
|
||||
List<MediaItem> _originalPlaybackContext = <MediaItem>[];
|
||||
List<MediaItem> _playbackContext = <MediaItem>[];
|
||||
|
||||
ShuffleMode _shuffleMode = ShuffleMode.none;
|
||||
|
||||
int _index = -1;
|
||||
int get playbackIndex => _index;
|
||||
set playbackIndex(int i) {
|
||||
|
@ -84,7 +89,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
Future<void> onSkipToNext() async {
|
||||
if (_incrementIndex()) {
|
||||
await _audioPlayer.stop();
|
||||
_startPlayback(_index);
|
||||
_startPlayback(playbackIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -92,7 +97,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
Future<void> onSkipToPrevious() async {
|
||||
if (_decrementIndex()) {
|
||||
await _audioPlayer.stop();
|
||||
_startPlayback(_index);
|
||||
_startPlayback(playbackIndex);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -109,6 +114,8 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
return _playWithContext(_context, index);
|
||||
case APP_LIFECYCLE_RESUMED:
|
||||
return _onAppLifecycleResumed();
|
||||
case SET_SHUFFLE_MODE:
|
||||
return _setShuffleMode((arguments as String).toShuffleMode());
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
@ -126,11 +133,14 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
Future<void> _playWithContext(List<String> context, int index) async {
|
||||
print('AudioPlayerTask._playWithContext');
|
||||
final _mediaItems = await _getMediaItemsFromPaths(context);
|
||||
final permutation = _generateSongPermutation(_mediaItems);
|
||||
final permutation = _generateSongPermutation(_mediaItems.length, index);
|
||||
_playbackContext = _getPermutatedSongs(_mediaItems, permutation);
|
||||
playbackIndex = index;
|
||||
if (_shuffleMode == ShuffleMode.none)
|
||||
playbackIndex = index;
|
||||
else
|
||||
playbackIndex = 0;
|
||||
AudioServiceBackground.setQueue(_playbackContext);
|
||||
_startPlayback(index);
|
||||
_startPlayback(playbackIndex);
|
||||
}
|
||||
|
||||
Future<void> _onAppLifecycleResumed() async {
|
||||
|
@ -138,6 +148,12 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
// AudioServiceBackground.setQueue(_playbackContext);
|
||||
}
|
||||
|
||||
Future<void> _setShuffleMode(ShuffleMode mode) async {
|
||||
_shuffleMode = mode;
|
||||
AudioServiceBackground.sendCustomEvent({SET_SHUFFLE_MODE: _shuffleMode.toString()});
|
||||
// TODO: adapt queue
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
// TODO: optimize -> too slow for whole library
|
||||
Future<List<MediaItem>> _getMediaItemsFromPaths(List<String> paths) async {
|
||||
|
@ -146,19 +162,29 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
final song = await _moorMusicDataSource.getSongByPath(path);
|
||||
mediaItems.add(song.toMediaItem());
|
||||
}
|
||||
// TODO: not good, side effects...
|
||||
_originalPlaybackContext = mediaItems;
|
||||
|
||||
return mediaItems;
|
||||
}
|
||||
|
||||
// TODO: test
|
||||
// TODO: needs implementation for shuffle mode
|
||||
List<int> _generateSongPermutation(List<MediaItem> songs) {
|
||||
List<int> _generateSongPermutation(int length, int startIndex) {
|
||||
// permutation[i] = j; => song j is on the i-th position in the permutated list
|
||||
final permutation = <int>[];
|
||||
List<int> permutation;
|
||||
|
||||
for (var i = 0; i < songs.length; i++) {
|
||||
permutation.add(i);
|
||||
switch (_shuffleMode) {
|
||||
case ShuffleMode.none:
|
||||
permutation = List<int>.generate(length, (i) => i);
|
||||
break;
|
||||
case ShuffleMode.standard:
|
||||
final tmp = List<int>.generate(length, (i) => i)
|
||||
..removeAt(startIndex)
|
||||
..shuffle();
|
||||
permutation = [startIndex] + tmp;
|
||||
break;
|
||||
case ShuffleMode.plus:
|
||||
break;
|
||||
}
|
||||
|
||||
return permutation;
|
||||
|
@ -179,7 +205,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
|
|||
processingState: AudioProcessingState.ready,
|
||||
);
|
||||
|
||||
// TODO: needs implementation for shuffle mode (play first song)
|
||||
final _mediaItem = _playbackContext[index];
|
||||
await AudioServiceBackground.setMediaItem(_mediaItem);
|
||||
await _audioPlayer.setFilePath(_mediaItem.id);
|
||||
|
|
|
@ -2,6 +2,7 @@ import 'package:dartz/dartz.dart';
|
|||
|
||||
import '../../core/error/failures.dart';
|
||||
import '../../domain/entities/playback_state.dart';
|
||||
import '../../domain/entities/shuffle_mode.dart';
|
||||
import '../../domain/entities/song.dart';
|
||||
import '../../domain/repositories/audio_repository.dart';
|
||||
import '../datasources/audio_manager_contract.dart';
|
||||
|
@ -25,6 +26,9 @@ class AudioRepositoryImpl implements AudioRepository {
|
|||
@override
|
||||
Stream<int> get queueIndexStream => _audioManager.queueIndexStream;
|
||||
|
||||
@override
|
||||
Stream<ShuffleMode> get shuffleModeStream => _audioManager.shuffleModeStream;
|
||||
|
||||
@override
|
||||
Stream<int> get currentPositionStream => _audioManager.currentPositionStream;
|
||||
|
||||
|
@ -63,4 +67,10 @@ class AudioRepositoryImpl implements AudioRepository {
|
|||
await _audioManager.skipToPrevious();
|
||||
return const Right(null);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, void>> setShuffleMode(ShuffleMode shuffleMode) async {
|
||||
await _audioManager.setShuffleMode(shuffleMode);
|
||||
return const Right(null);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue