a whole lot of changes

This commit is contained in:
Moritz Weber 2020-07-12 18:25:34 +02:00
parent 1952433aa9
commit e463ab02c9
17 changed files with 439 additions and 94 deletions

View file

@ -7,9 +7,13 @@ import '../entities/song.dart';
abstract class AudioRepository {
Stream<Song> get currentSongStream;
Stream<PlaybackState> get playbackStateStream;
Stream<List<Song>> get queueStream;
Stream<int> get queueIndexStream;
Stream<int> get currentPositionStream;
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();
}

View file

@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mucke/presentation/widgets/next_indicator.dart';
import 'package:mucke/presentation/widgets/playback_control.dart';
import 'package:provider/provider.dart';
import '../../domain/entities/song.dart';
@ -26,6 +28,8 @@ class CurrentlyPlayingPage extends StatelessWidget {
print('CurrentlyPlayingPage.build -> Observer.build');
final Song song = audioStore.song;
print(audioStore.queueIndexStream.value);
return Padding(
padding: const EdgeInsets.only(
left: 12.0,
@ -91,56 +95,9 @@ class CurrentlyPlayingPage extends StatelessWidget {
const Spacer(
flex: 3,
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Icon(Icons.repeat, size: 20.0),
Icon(Icons.skip_previous, size: 32.0),
const PlayPauseButton(
circle: true,
iconSize: 52.0,
),
Icon(Icons.skip_next, size: 32.0),
Icon(Icons.shuffle, size: 20.0),
],
mainAxisAlignment: MainAxisAlignment.spaceBetween,
),
),
const PlaybackControl(),
const Spacer(),
Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.expand_less,
color: Colors.white70,
),
RichText(
text: TextSpan(
style: TextStyle(
fontSize: 14,
color: Colors.white70,
),
children: [
const TextSpan(text: 'Fire'),
const TextSpan(text: ''),
TextSpan(
text: 'Beartooth',
style: TextStyle(
fontWeight: FontWeight.w300,
),
),
],
),
),
],
),
),
),
const NextIndicator(),
],
),
);

View file

@ -35,6 +35,12 @@ abstract class _AudioStore with Store {
@observable
ObservableStream<int> currentPositionStream;
@observable
ObservableStream<List<Song>> queueStream;
@observable
ObservableStream<int> queueIndexStream;
@action
void init() {
if (!_initialized) {
@ -44,6 +50,10 @@ abstract class _AudioStore with Store {
currentPositionStream =
_audioRepository.currentPositionStream.asObservable(initialValue: 0);
queueStream = _audioRepository.queueStream.asObservable(initialValue: []);
queueIndexStream = _audioRepository.queueIndexStream.asObservable();
_disposers.add(autorun((_) {
updateSong(currentSong.value);
}));
@ -74,6 +84,16 @@ abstract class _AudioStore with Store {
_audioRepository.pause();
}
@action
Future<void> skipToNext() async {
_audioRepository.skipToNext();
}
@action
Future<void> skipToPrevious() async {
_audioRepository.skipToPrevious();
}
@action
Future<void> updateSong(Song streamValue) async {
print('updateSong');

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mucke/presentation/widgets/next_button.dart';
import 'package:provider/provider.dart';
import '../../domain/entities/song.dart';
@ -53,17 +54,10 @@ class CurrentlyPlayingBar extends StatelessWidget {
],
),
const Spacer(),
// IconButton(
// icon: Icon(Icons.favorite_border),
// onPressed: () {},
// ),
const PlayPauseButton(
circle: false,
),
// IconButton(
// icon: Icon(Icons.skip_next),
// onPressed: () {},
// ),
const NextButton(),
],
),
),

View file

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:provider/provider.dart';
import '../state/audio_store.dart';
class NextButton extends StatelessWidget {
const NextButton({Key key, this.iconSize = 24.0}) : super(key: key);
final double iconSize;
@override
Widget build(BuildContext context) {
final AudioStore audioStore = Provider.of<AudioStore>(context);
return Observer(
builder: (BuildContext context) {
final queue = audioStore.queueStream.value; //
final int index = audioStore.queueIndexStream.value; //
if (index != null && index < queue.length - 1) { //
return IconButton(
icon: Icon(Icons.skip_next), //
iconSize: iconSize,
onPressed: () {
audioStore.skipToNext(); //
},
);
}
return IconButton(
icon: Icon(
Icons.skip_next,
),
iconSize: iconSize,
onPressed: null,
);
},
);
}
}

View file

@ -0,0 +1,38 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mucke/presentation/state/audio_store.dart';
import 'package:mucke/presentation/widgets/next_song.dart';
import 'package:provider/provider.dart';
class NextIndicator extends StatelessWidget {
const NextIndicator({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final AudioStore audioStore = Provider.of<AudioStore>(context);
return Observer(
builder: (BuildContext context) {
final queue = audioStore.queueStream.value;
final int index = audioStore.queueIndexStream.value;
return Padding(
padding: const EdgeInsets.all(8.0),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
Icons.expand_less,
color: Colors.white70,
),
if (index < queue.length - 1) NextSong(song: queue[index + 1]),
],
),
),
);
},
);
}
}

View file

@ -0,0 +1,31 @@
import 'package:flutter/material.dart';
import '../../domain/entities/song.dart';
class NextSong extends StatelessWidget {
const NextSong({Key key, this.song}) : super(key: key);
final Song song;
@override
Widget build(BuildContext context) {
return RichText(
text: TextSpan(
style: TextStyle(
fontSize: 14,
color: Colors.white70,
),
children: [
TextSpan(text: '${song.title}'),
const TextSpan(text: ''),
TextSpan(
text: '${song.artist}',
style: TextStyle(
fontWeight: FontWeight.w300,
),
),
],
),
);
}
}

View file

@ -0,0 +1,29 @@
import 'package:flutter/material.dart';
import 'next_button.dart';
import 'play_pause_button.dart';
import 'previous_button.dart';
class PlaybackControl extends StatelessWidget {
const PlaybackControl({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Row(
children: [
Icon(Icons.repeat, size: 20.0),
const PreviousButton(iconSize: 32.0),
const PlayPauseButton(
circle: true,
iconSize: 52.0,
),
const NextButton(iconSize: 32.0),
Icon(Icons.shuffle, size: 20.0),
],
mainAxisAlignment: MainAxisAlignment.spaceBetween,
),
);
}
}

View file

@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:provider/provider.dart';
import '../state/audio_store.dart';
class PreviousButton extends StatelessWidget {
const PreviousButton({Key key, this.iconSize = 24.0}) : super(key: key);
final double iconSize;
@override
Widget build(BuildContext context) {
final AudioStore audioStore = Provider.of<AudioStore>(context);
return Observer(
builder: (BuildContext context) {
final int index = audioStore.queueIndexStream.value; //
if (index > 0) { //
return IconButton(
icon: Icon(Icons.skip_previous), //
iconSize: iconSize,
onPressed: () {
audioStore.skipToPrevious(); //
},
);
}
return IconButton(
icon: Icon(
Icons.skip_previous,
),
iconSize: iconSize,
onPressed: null,
);
},
);
}
}

View file

@ -10,11 +10,30 @@ 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() {
AudioService.customEventStream.listen((event) {
final data = event as Map<String, dynamic>;
if (data.containsKey(KEY_INDEX)) {
_queueIndex = data[KEY_INDEX] as int;
}
});
}
final Stream<MediaItem> _currentMediaItemStream =
AudioService.currentMediaItemStream;
final Stream<PlaybackState> _sourcePlaybackStateStream =
AudioService.playbackStateStream;
final Stream<List<MediaItem>> _queue = AudioService.queueStream;
@override
final Stream customEventStream = AudioService.customEventStream;
int _queueIndex;
@override
Stream<SongModel> get currentSongStream =>
@ -29,6 +48,30 @@ class AudioManagerImpl implements AudioManager {
(PlaybackState ps) => PlaybackStateModel.fromASPlaybackState(ps),
);
// TODO: test
@override
Stream<List<SongModel>> get queueStream {
return _queue.map((mediaItems) =>
mediaItems.map((m) => SongModel.fromMediaItem(m)).toList());
}
@override
Stream<int> get queueIndexStream =>
_queueIndexStream(AudioService.customEventStream.cast());
// TODO: test
Stream<int> _queueIndexStream(Stream<Map<String, dynamic>> source) async* {
if (_queueIndex != null) {
yield _queueIndex;
}
await for (final data in source) {
if (data.containsKey(KEY_INDEX)) {
yield data[KEY_INDEX] as int;
}
}
}
@override
Stream<int> get currentPositionStream => _position().distinct();
@ -36,10 +79,7 @@ class AudioManagerImpl implements AudioManager {
Future<void> playSong(int index, List<SongModel> songList) async {
await _startAudioService();
final List<String> queue = songList.map((s) => s.path).toList();
await AudioService.customAction(SET_QUEUE, queue);
AudioService.playFromMediaId(queue[index]);
await AudioService.customAction(PLAY_WITH_CONTEXT, [queue, index]);
}
@override
@ -58,9 +98,20 @@ class AudioManagerImpl implements AudioManager {
backgroundTaskEntrypoint: _backgroundTaskEntrypoint,
androidEnableQueue: true,
);
await AudioService.customAction(INIT);
}
}
@override
Future<void> skipToNext() async {
await AudioService.skipToNext();
}
@override
Future<void> skipToPrevious() async {
await AudioService.skipToPrevious();
}
Stream<T> _filterStream<S, T>(Stream<S> stream, Conversion<S, T> fn) async* {
T lastItem;
@ -89,7 +140,8 @@ class AudioManagerImpl implements AudioManager {
if (statePosition != null && updateTime != null && state != null) {
if (state.playing) {
yield statePosition.inMilliseconds +
(DateTime.now().millisecondsSinceEpoch - updateTime.inMilliseconds);
(DateTime.now().millisecondsSinceEpoch -
updateTime.inMilliseconds);
} else {
yield statePosition.inMilliseconds;
}

View file

@ -4,10 +4,15 @@ import '../models/song_model.dart';
abstract class AudioManager {
Stream<SongModel> get currentSongStream;
Stream<PlaybackState> get playbackStateStream;
Stream<List<SongModel>> get queueStream;
Stream get customEventStream;
Stream<int> get queueIndexStream;
/// Current position in the song in milliseconds.
Stream<int> get currentPositionStream;
Future<void> playSong(int index, List<SongModel> songList);
Future<void> play();
Future<void> pause();
Future<void> skipToNext();
Future<void> skipToPrevious();
}

View file

@ -8,7 +8,10 @@ import 'package:moor/moor.dart';
import 'moor_music_data_source.dart';
const String SET_QUEUE = 'SET_QUEUE';
const String INIT = 'INIT';
const String PLAY_WITH_CONTEXT = 'PLAY_WITH_CONTEXT';
const String KEY_INDEX = 'INDEX';
class AudioPlayerTask extends BackgroundAudioTask {
final _audioPlayer = AudioPlayer();
@ -16,19 +19,20 @@ class AudioPlayerTask extends BackgroundAudioTask {
MoorMusicDataSource _moorMusicDataSource;
final _mediaItems = <String, MediaItem>{};
final _queue = <MediaItem>[];
List<MediaItem> _originalPlaybackContext = <MediaItem>[];
List<MediaItem> _playbackContext = <MediaItem>[];
int _index = -1;
int get playbackIndex => _index;
set playbackIndex(int i) {
print('setting index');
_index = i;
AudioServiceBackground.sendCustomEvent({KEY_INDEX: _index});
}
Duration _position;
@override
Future<void> onStart(Map<String, dynamic> params) async {
_audioPlayer.getPositionStream().listen((duration) => _position = duration);
final connectPort = IsolateNameServer.lookupPortByName(MOOR_ISOLATE);
final MoorIsolate moorIsolate = MoorIsolate.fromConnectPort(connectPort);
final DatabaseConnection databaseConnection = await moorIsolate.connect();
_moorMusicDataSource = MoorMusicDataSource.connect(databaseConnection);
await _completer.future;
}
@ -81,17 +85,125 @@ class AudioPlayerTask extends BackgroundAudioTask {
await _audioPlayer.pause();
}
@override
Future<void> onSkipToNext() async {
if (_incrementIndex()) {
await _audioPlayer.stop();
_startPlayback(_index);
}
}
@override
Future<void> onSkipToPrevious() async {
if (_decrementIndex()) {
await _audioPlayer.stop();
_startPlayback(_index);
}
}
@override
Future<void> onCustomAction(String name, arguments) async {
switch (name) {
case SET_QUEUE:
return _setQueue(List<String>.from(arguments as List<dynamic>));
case INIT:
return _init();
case PLAY_WITH_CONTEXT:
// arguments: [List<String>, int]
final args = arguments as List<dynamic>;
final _context = List<String>.from(args[0] as List<dynamic>);
final index = args[1] as int;
return _playWithContext(_context, index);
default:
}
}
Future<void> _setQueue(List<String> queue) async {
Future<void> _init() async {
print('AudioPlayerTask._init');
_audioPlayer.getPositionStream().listen((duration) => _position = duration);
final connectPort = IsolateNameServer.lookupPortByName(MOOR_ISOLATE);
final MoorIsolate moorIsolate = MoorIsolate.fromConnectPort(connectPort);
final DatabaseConnection databaseConnection = await moorIsolate.connect();
_moorMusicDataSource = MoorMusicDataSource.connect(databaseConnection);
}
Future<void> _playWithContext(List<String> context, int index) async {
print('AudioPlayerTask._playWithContext');
final _mediaItems = await _getMediaItemsFromPaths(context);
final permutation = _generateSongPermutation(_mediaItems);
_playbackContext = _getPermutatedSongs(_mediaItems, permutation);
playbackIndex = index;
AudioServiceBackground.setQueue(_playbackContext);
_startPlayback(index);
}
// TODO: test
// TODO: optimize -> too slow for whole library
Future<List<MediaItem>> _getMediaItemsFromPaths(List<String> paths) async {
final mediaItems = <MediaItem>[];
for (final path in paths) {
final song = await _moorMusicDataSource.getSongByPath(path);
mediaItems.add(song.toMediaItem());
}
_originalPlaybackContext = mediaItems;
return mediaItems;
}
// TODO: test
// TODO: needs implementation for shuffle mode
List<int> _generateSongPermutation(List<MediaItem> songs) {
// permutation[i] = j; => song j is on the i-th position in the permutated list
final permutation = <int>[];
for (var i = 0; i < songs.length; i++) {
permutation.add(i);
}
return permutation;
}
List<MediaItem> _getPermutatedSongs(
List<MediaItem> songs, List<int> permutation) {
return List.generate(
permutation.length, (index) => songs[permutation[index]]);
}
// TODO: cleanup and test
Future<void> _startPlayback(int index) async {
// TODO: DRY
AudioServiceBackground.setState(
controls: [pauseControl, stopControl],
playing: true,
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);
// exploration: this works, but has to be used every time play() is called; maybe stateStream is the better option
_audioPlayer.play().then((_) {
print(_audioPlayer.playbackState);
if (_audioPlayer.playbackState == AudioPlaybackState.completed)
onSkipToNext();
});
}
bool _incrementIndex() {
if (playbackIndex < _playbackContext.length - 1) {
playbackIndex++;
return true;
}
return false;
}
bool _decrementIndex() {
if (playbackIndex > 0) {
playbackIndex--;
return true;
}
return false;
}
}

View file

@ -88,7 +88,9 @@ class MoorMusicDataSource extends _$MoorMusicDataSource
@override
Future<List<SongModel>> getSongsFromAlbum(AlbumModel album) {
return (select(songs)..where((tbl) => tbl.albumTitle.equals(album.title)))
return (select(songs)
..where((tbl) => tbl.albumTitle.equals(album.title))
..orderBy([(t) => OrderingTerm(expression: t.trackNumber)]))
.get()
.then((moorSongList) => moorSongList
.map((moorSong) => SongModel.fromMoorSong(moorSong))

View file

@ -82,6 +82,11 @@ class SongModel extends Song {
final int albumId;
@override
String toString() {
return '$title';
}
SongModel copyWith({
String title,
String album,
@ -115,15 +120,14 @@ class SongModel extends Song {
);
MediaItem toMediaItem() => MediaItem(
id: path,
title: title,
album: album,
artist: artist,
duration: Duration(milliseconds: duration),
artUri: 'file://$albumArtPath',
extras: {
'albumId': albumId,
'trackNumber': trackNumber,
}
);
id: path,
title: title,
album: album,
artist: artist,
duration: Duration(milliseconds: duration),
artUri: 'file://$albumArtPath',
extras: {
'albumId': albumId,
'trackNumber': trackNumber,
});
}

View file

@ -20,9 +20,13 @@ class AudioRepositoryImpl implements AudioRepository {
_audioManager.playbackStateStream;
@override
Stream<int> get currentPositionStream {
return _audioManager.currentPositionStream;
}
Stream<List<Song>> get queueStream => _audioManager.queueStream;
@override
Stream<int> get queueIndexStream => _audioManager.queueIndexStream;
@override
Stream<int> get currentPositionStream => _audioManager.currentPositionStream;
@override
Future<Either<Failure, void>> playSong(int index, List<Song> songList) async {
@ -31,7 +35,7 @@ class AudioRepositoryImpl implements AudioRepository {
if (0 <= index && index < songList.length) {
await _audioManager.playSong(index, songModelList);
return Right(null);
return const Right(null);
}
return Left(IndexFailure());
}
@ -39,12 +43,24 @@ class AudioRepositoryImpl implements AudioRepository {
@override
Future<Either<Failure, void>> play() async {
await _audioManager.play();
return Right(null);
return const Right(null);
}
@override
Future<Either<Failure, void>> pause() async {
await _audioManager.pause();
return Right(null);
return const Right(null);
}
@override
Future<Either<Failure, void>> skipToNext() async {
await _audioManager.skipToNext();
return const Right(null);
}
@override
Future<Either<Failure, void>> skipToPrevious() async {
await _audioManager.skipToPrevious();
return const Right(null);
}
}

View file

@ -414,7 +414,7 @@ packages:
name: moor_ffi
url: "https://pub.dartlang.org"
source: hosted
version: "0.6.0"
version: "0.5.0"
moor_generator:
dependency: "direct dev"
description:

View file

@ -23,7 +23,7 @@ dependencies:
get_it: ^4.0.2
provider: ^4.0.4
moor: ^3.0.2
moor_ffi: ^0.6.0
moor_ffi: ^0.5.0
path_provider:
path: