notification controls, AudioStore

This commit is contained in:
Moritz Weber 2020-04-07 22:06:43 +02:00
parent 5116e7ea1e
commit c425b63fc2
12 changed files with 143 additions and 124 deletions

View file

@ -1,28 +0,0 @@
import 'package:dartz/dartz.dart';
import 'package:equatable/equatable.dart';
import 'package:mosh/domain/repositories/audio_repository.dart';
import '../../core/error/failures.dart';
import '../../core/usecase.dart';
import '../entities/song.dart';
class PlaySong implements UseCase<void, Params> {
PlaySong(this.audioRepository);
final AudioRepository audioRepository;
@override
Future<Either<Failure, void>> call(Params params) async {
return audioRepository.playSong(params.index, params.songList);
}
}
class Params extends Equatable {
const Params(this.index, this.songList);
final int index;
final List<Song> songList;
@override
List<Object> get props => [index, songList];
}

View file

@ -62,7 +62,11 @@ class _RootPageState extends State<RootPage> {
_musicStore.fetchSongs();
// TODO: don't do this here...
AudioService.start(backgroundTaskEntrypoint: _backgroundTaskEntrypoint);
AudioService.start(
backgroundTaskEntrypoint: _backgroundTaskEntrypoint,
enableQueue: true,
androidStopOnRemoveTask: true,
);
super.didChangeDependencies();
}

View file

@ -3,11 +3,12 @@ import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:provider/provider.dart';
import '../../domain/entities/song.dart';
import '../state/audio_store.dart';
import '../state/music_data_store.dart';
import '../widgets/album_art_list_tile.dart';
class SongsPage extends StatefulWidget {
SongsPage({Key key}) : super(key: key);
const SongsPage({Key key}) : super(key: key);
@override
_SongsPageState createState() => _SongsPageState();
@ -19,12 +20,13 @@ class _SongsPageState extends State<SongsPage>
@override
Widget build(BuildContext context) {
print('SongsPage.build');
final MusicDataStore store = Provider.of<MusicDataStore>(context);
final MusicDataStore musicDataStore = Provider.of<MusicDataStore>(context);
final AudioStore audioStore = Provider.of<AudioStore>(context);
super.build(context);
return Observer(builder: (_) {
print('SongsPage.build -> Observer.builder');
final bool isFetching = store.isFetchingSongs;
final bool isFetching = musicDataStore.isFetchingSongs;
if (isFetching) {
return Column(
@ -35,7 +37,7 @@ class _SongsPageState extends State<SongsPage>
],
);
} else {
final List<Song> songs = store.songs;
final List<Song> songs = musicDataStore.songs;
return ListView.separated(
itemCount: songs.length,
itemBuilder: (_, int index) {
@ -44,7 +46,7 @@ class _SongsPageState extends State<SongsPage>
title: song.title,
subtitle: '${song.artist}${song.album}',
albumArtPath: song.albumArtPath,
onTap: () => store.playSong(index, songs),
onTap: () => audioStore.playSong(index, songs),
);
},
separatorBuilder: (BuildContext context, int index) => const Divider(

View file

@ -0,0 +1,26 @@
import 'package:meta/meta.dart';
import 'package:mobx/mobx.dart';
import '../../domain/entities/song.dart';
import '../../domain/repositories/audio_repository.dart';
import '../../domain/repositories/music_data_repository.dart';
part 'audio_store.g.dart';
class AudioStore extends _AudioStore with _$AudioStore {
AudioStore({@required MusicDataRepository musicDataRepository, @required AudioRepository audioRepository})
: super(musicDataRepository, audioRepository);
}
abstract class _AudioStore with Store {
_AudioStore(this._musicDataRepository, this._audioRepository);
final MusicDataRepository _musicDataRepository;
final AudioRepository _audioRepository;
@action
Future<void> playSong(int index, List<Song> songList) async {
_audioRepository.playSong(index, songList);
}
}

View file

@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'audio_store.dart';
// **************************************************************************
// StoreGenerator
// **************************************************************************
// ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
mixin _$AudioStore on _AudioStore, Store {
final _$playSongAsyncAction = AsyncAction('playSong');
@override
Future<void> playSong(int index, List<Song> songList) {
return _$playSongAsyncAction.run(() => super.playSong(index, songList));
}
@override
String toString() {
final string = '';
return '{$string}';
}
}

View file

@ -1,12 +1,10 @@
import 'package:dartz/dartz.dart';
import 'package:meta/meta.dart';
import 'package:mobx/mobx.dart';
import 'package:mosh/domain/usecases/play_song.dart';
import '../../core/error/failures.dart';
import '../../domain/entities/album.dart';
import '../../domain/entities/song.dart';
import '../../domain/repositories/audio_repository.dart';
import '../../domain/repositories/music_data_repository.dart';
import '../../domain/usecases/get_albums.dart';
import '../../domain/usecases/get_songs.dart';
@ -14,22 +12,20 @@ import '../../domain/usecases/update_database.dart';
part 'music_data_store.g.dart';
class MusicDataStore extends _MusicStore with _$MusicDataStore {
MusicDataStore({@required MusicDataRepository musicDataRepository, @required AudioRepository audioRepository})
: super(musicDataRepository, audioRepository);
class MusicDataStore extends _MusicDataStore with _$MusicDataStore {
MusicDataStore({@required MusicDataRepository musicDataRepository})
: super(musicDataRepository);
}
abstract class _MusicStore with Store {
_MusicStore(MusicDataRepository _musicDataRepository, AudioRepository _audioRepository)
abstract class _MusicDataStore with Store {
_MusicDataStore(MusicDataRepository _musicDataRepository)
: _updateDatabase = UpdateDatabase(_musicDataRepository),
_getAlbums = GetAlbums(_musicDataRepository),
_getSongs = GetSongs(_musicDataRepository),
_playSong = PlaySong(_audioRepository);
_getSongs = GetSongs(_musicDataRepository);
final UpdateDatabase _updateDatabase;
final GetAlbums _getAlbums;
final GetSongs _getSongs;
final PlaySong _playSong;
@observable
ObservableFuture<List<Album>> albumsFuture;
@ -81,9 +77,4 @@ abstract class _MusicStore with Store {
isFetchingSongs = false;
}
@action
Future<void> playSong(int index, List<Song> songList) async {
await _playSong(Params(index, songList));
}
}

View file

@ -8,8 +8,8 @@ part of 'music_data_store.dart';
// ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic
mixin _$MusicDataStore on _MusicStore, Store {
final _$albumsFutureAtom = Atom(name: '_MusicStore.albumsFuture');
mixin _$MusicDataStore on _MusicDataStore, Store {
final _$albumsFutureAtom = Atom(name: '_MusicDataStore.albumsFuture');
@override
ObservableFuture<List<Album>> get albumsFuture {
@ -26,7 +26,7 @@ mixin _$MusicDataStore on _MusicStore, Store {
}, _$albumsFutureAtom, name: '${_$albumsFutureAtom.name}_set');
}
final _$songsAtom = Atom(name: '_MusicStore.songs');
final _$songsAtom = Atom(name: '_MusicDataStore.songs');
@override
ObservableList<Song> get songs {
@ -43,7 +43,7 @@ mixin _$MusicDataStore on _MusicStore, Store {
}, _$songsAtom, name: '${_$songsAtom.name}_set');
}
final _$isFetchingSongsAtom = Atom(name: '_MusicStore.isFetchingSongs');
final _$isFetchingSongsAtom = Atom(name: '_MusicDataStore.isFetchingSongs');
@override
bool get isFetchingSongs {
@ -60,7 +60,8 @@ mixin _$MusicDataStore on _MusicStore, Store {
}, _$isFetchingSongsAtom, name: '${_$isFetchingSongsAtom.name}_set');
}
final _$isUpdatingDatabaseAtom = Atom(name: '_MusicStore.isUpdatingDatabase');
final _$isUpdatingDatabaseAtom =
Atom(name: '_MusicDataStore.isUpdatingDatabase');
@override
bool get isUpdatingDatabase {
@ -99,13 +100,6 @@ mixin _$MusicDataStore on _MusicStore, Store {
return _$fetchSongsAsyncAction.run(() => super.fetchSongs());
}
final _$playSongAsyncAction = AsyncAction('playSong');
@override
Future<void> playSong(int index, List<Song> songList) {
return _$playSongAsyncAction.run(() => super.playSong(index, songList));
}
@override
String toString() {
final string =

View file

@ -1,5 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_audio_query/flutter_audio_query.dart';
import 'package:mosh/domain/repositories/audio_repository.dart';
import 'package:mosh/domain/repositories/music_data_repository.dart';
import 'package:mosh/presentation/state/audio_store.dart';
import 'package:mosh/presentation/state/music_data_store.dart';
import 'package:mosh/system/datasources/audio_manager.dart';
import 'package:mosh/system/datasources/local_music_fetcher.dart';
@ -15,15 +18,29 @@ class InjectionWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Provider<MusicDataStore>(
final MusicDataRepository musicDataRepository = MusicDataRepositoryImpl(
localMusicFetcher: LocalMusicFetcherImpl(FlutterAudioQuery()),
musicDataSource: MoorMusicDataSource(),
);
final AudioRepository audioRepository =
AudioRepositoryImpl(AudioManagerImpl());
return MultiProvider(
child: child,
create: (BuildContext context) => MusicDataStore(
musicDataRepository: MusicDataRepositoryImpl(
localMusicFetcher: LocalMusicFetcherImpl(FlutterAudioQuery()),
musicDataSource: MoorMusicDataSource(),
providers: [
Provider<MusicDataStore>(
create: (BuildContext context) => MusicDataStore(
musicDataRepository: musicDataRepository,
),
),
audioRepository: AudioRepositoryImpl(AudioManagerImpl()),
),
Provider<AudioStore>(
create: (BuildContext context) => AudioStore(
musicDataRepository: musicDataRepository,
audioRepository: audioRepository,
),
),
],
);
}
}

View file

@ -11,7 +11,7 @@ class AudioManagerImpl implements AudioManager {
Future<void> playSong(int index, List<SongModel> songList) async {
final List<MediaItem> queue = songList.map((s) => s.toMediaItem()).toList();
// await AudioService.addQueueItem(queue[index]);
await AudioService.addQueueItem(queue[index]);
AudioService.playFromMediaId(queue[index].id);
}
}
@ -25,6 +25,12 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
Future<void> onStart() async {
print('onStart');
AudioServiceBackground.setState(
controls: [pauseControl, stopControl],
basicState: BasicPlaybackState.playing,
);
await _completer.future;
}
@ -41,8 +47,43 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override
Future<void> onPlayFromMediaId(String mediaId) async {
// _audioPlayer.setFilePath(_mediaItems[mediaId].id);
// await _audioPlayer.setFilePath(_mediaItems[mediaId]);
AudioServiceBackground.setMediaItem(_mediaItems[mediaId]);
await _audioPlayer.setFilePath(mediaId);
_audioPlayer.play();
}
@override
Future<void> onPlay() async {
AudioServiceBackground.setState(
controls: [pauseControl, stopControl],
basicState: BasicPlaybackState.playing);
_audioPlayer.play();
}
@override
Future<void> onPause() async {
AudioServiceBackground.setState(
controls: [playControl, stopControl],
basicState: BasicPlaybackState.paused);
await _audioPlayer.pause();
}
}
MediaControl playControl = const MediaControl(
androidIcon: 'drawable/ic_action_play_arrow',
label: 'Play',
action: MediaAction.play,
);
MediaControl pauseControl = const MediaControl(
androidIcon: 'drawable/ic_action_pause',
label: 'Pause',
action: MediaAction.pause,
);
MediaControl stopControl = const MediaControl(
androidIcon: 'drawable/ic_action_stop',
label: 'Stop',
action: MediaAction.stop,
);

View file

@ -57,12 +57,11 @@ class SongModel extends Song {
trackNumber: Value(trackNumber),
);
// TODO: test!
MediaItem toMediaItem() => MediaItem(
title: title,
album: album,
artist: artist,
artUri: albumArtPath,
artUri: 'file://$albumArtPath',
id: path,
);
}

View file

@ -1,51 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mosh/domain/entities/song.dart';
import 'package:mosh/domain/repositories/audio_repository.dart';
import 'package:mosh/domain/usecases/play_song.dart';
import '../../test_constants.dart';
class MockAudioRepository extends Mock implements AudioRepository {}
void main() {
PlaySong usecase;
MockAudioRepository mockAudioRepository;
setUp(() {
mockAudioRepository = MockAudioRepository();
usecase = PlaySong(mockAudioRepository);
});
const tIndex = 0;
final tSongList = <Song>[
Song(
album: ALBUM_TITLE_3,
artist: ARTIST_3,
title: SONG_TITLE_3,
path: PATH_3,
albumArtPath: ALBUM_ART_PATH_3,
trackNumber: TRACKNUMBER_3,
),
Song(
album: ALBUM_TITLE_4,
artist: ARTIST_4,
title: SONG_TITLE_4,
path: PATH_4,
albumArtPath: ALBUM_ART_PATH_4,
trackNumber: TRACKNUMBER_4,
),
];
test(
'should forward index and song list to repository',
() async {
// act
await usecase(Params(tIndex, tSongList));
// assert
verify(mockAudioRepository.playSong(tIndex, tSongList));
verifyNoMoreInteractions(mockAudioRepository);
},
);
}

View file

@ -74,7 +74,7 @@ void main() {
title: SONG_TITLE_3,
album: ALBUM_TITLE_3,
artist: ARTIST_3,
artUri: ALBUM_ART_PATH_3,
artUri: 'file://$ALBUM_ART_PATH_3',
);
final songModel = SongModel(