implemented shuffle mode

This commit is contained in:
Moritz Weber 2020-08-24 16:43:22 +02:00
parent 3178f0515f
commit b7075aad27
12 changed files with 212 additions and 41 deletions

View 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;
}
}
}

View file

@ -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);
}

View file

@ -51,6 +51,7 @@ class QueuePage extends StatelessWidget {
);
case StreamStatus.waiting:
case StreamStatus.done:
default:
return Container();
}
},

View file

@ -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');

View file

@ -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,
),
],
),
),

View file

@ -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,
);
}
}
}

View file

@ -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,
),

View 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);
},
);
}
},
);
}
}

View file

@ -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;

View file

@ -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);
}

View file

@ -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);

View file

@ -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);
}
}