From eff0dc29a6871a31cebefc6c3b5d8c4c9768672e Mon Sep 17 00:00:00 2001 From: Moritz Weber Date: Fri, 10 Apr 2020 20:12:26 +0200 Subject: [PATCH] implemented play/pause --- lib/domain/entities/playback_state.dart | 6 ++ lib/domain/repositories/audio_repository.dart | 6 +- lib/main.dart | 6 +- lib/presentation/pages/currently_playing.dart | 7 ++- lib/presentation/pages/home_page.dart | 9 ++- lib/presentation/state/audio_store.dart | 43 ++++++++++---- lib/presentation/state/audio_store.g.dart | 36 +++++++++++- lib/presentation/state/music_data_store.dart | 13 +++++ .../state/music_data_store.g.dart | 7 +++ lib/presentation/widgets/album_art.dart | 4 ++ .../widgets/currently_playing_bar.dart | 22 +++---- .../widgets/injection_widget.dart | 20 ++++--- .../widgets/play_pause_button.dart | 43 ++++++++++++++ lib/system/datasources/audio_manager.dart | 57 ++++++++++++++++--- .../datasources/audio_manager_contract.dart | 6 +- lib/system/models/playback_state_model.dart | 26 +++++++++ lib/system/models/song_model.dart | 18 +++--- .../repositories/audio_repository_impl.dart | 19 ++++++- 18 files changed, 289 insertions(+), 59 deletions(-) create mode 100644 lib/domain/entities/playback_state.dart create mode 100644 lib/presentation/widgets/play_pause_button.dart create mode 100644 lib/system/models/playback_state_model.dart diff --git a/lib/domain/entities/playback_state.dart b/lib/domain/entities/playback_state.dart new file mode 100644 index 0000000..2806592 --- /dev/null +++ b/lib/domain/entities/playback_state.dart @@ -0,0 +1,6 @@ +enum PlaybackState { + none, + playing, + paused, + stopped, +} \ No newline at end of file diff --git a/lib/domain/repositories/audio_repository.dart b/lib/domain/repositories/audio_repository.dart index 038582e..bd567c2 100644 --- a/lib/domain/repositories/audio_repository.dart +++ b/lib/domain/repositories/audio_repository.dart @@ -1,10 +1,14 @@ import 'package:dartz/dartz.dart'; import '../../core/error/failures.dart'; +import '../entities/playback_state.dart'; import '../entities/song.dart'; abstract class AudioRepository { - Stream get watchCurrentSong; + Stream get currentSongStream; + Stream get playbackStateStream; Future> playSong(int index, List songList); + Future> play(); + Future> pause(); } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index cb88291..8a1038a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -50,7 +50,7 @@ class _RootPageState extends State { var navIndex = 1; final List _pages = [ - HomePage(), + const HomePage(), const LibraryPage( key: PageStorageKey('LibraryPage'), ), @@ -62,8 +62,7 @@ class _RootPageState extends State { @override void didChangeDependencies() { final MusicDataStore _musicStore = Provider.of(context); - _musicStore.fetchAlbums(); - _musicStore.fetchSongs(); + _musicStore.init(); final AudioStore _audioStore = Provider.of(context); _audioStore.init(); @@ -80,6 +79,7 @@ class _RootPageState extends State { @override Widget build(BuildContext context) { + print('RootPage.build'); return Scaffold( body: IndexedStack( index: navIndex, diff --git a/lib/presentation/pages/currently_playing.dart b/lib/presentation/pages/currently_playing.dart index 837a0fa..1c1fe47 100644 --- a/lib/presentation/pages/currently_playing.dart +++ b/lib/presentation/pages/currently_playing.dart @@ -6,6 +6,7 @@ import '../../domain/entities/song.dart'; import '../state/audio_store.dart'; import '../theming.dart'; import '../widgets/album_art.dart'; +import '../widgets/play_pause_button.dart'; import '../widgets/queue_card.dart'; import '../widgets/time_progress_indicator.dart'; @@ -99,9 +100,9 @@ class CurrentlyPlayingPage extends StatelessWidget { children: [ Icon(Icons.repeat, size: 20.0), Icon(Icons.skip_previous, size: 32.0), - Icon( - Icons.play_circle_filled, - size: 52.0, + const PlayPauseButton( + circle: true, + iconSize: 52.0, ), Icon(Icons.skip_next, size: 32.0), Icon(Icons.shuffle, size: 20.0), diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart index 8838b60..234687a 100644 --- a/lib/presentation/pages/home_page.dart +++ b/lib/presentation/pages/home_page.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class HomePage extends StatefulWidget { - HomePage({Key key}) : super(key: key); + const HomePage({Key key}) : super(key: key); @override _HomePageState createState() => _HomePageState(); @@ -10,8 +10,11 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { @override Widget build(BuildContext context) { + print('HomePage.build'); return Container( - child: Center(child: Text("Home Page"),), + child: const Center( + child: Text('Home Page'), + ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/state/audio_store.dart b/lib/presentation/state/audio_store.dart index 5246dd1..9b393d0 100644 --- a/lib/presentation/state/audio_store.dart +++ b/lib/presentation/state/audio_store.dart @@ -1,6 +1,7 @@ import 'package:meta/meta.dart'; import 'package:mobx/mobx.dart'; +import '../../domain/entities/playback_state.dart'; import '../../domain/entities/song.dart'; import '../../domain/repositories/audio_repository.dart'; import '../../domain/repositories/music_data_repository.dart'; @@ -8,7 +9,9 @@ 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}) + AudioStore( + {@required MusicDataRepository musicDataRepository, + @required AudioRepository audioRepository}) : super(musicDataRepository, audioRepository); } @@ -18,25 +21,36 @@ abstract class _AudioStore with Store { final MusicDataRepository _musicDataRepository; final AudioRepository _audioRepository; - ReactionDisposer _disposer; + bool _initialized = false; + final List _disposers = []; @observable ObservableStream currentSong; - @observable Song song; + @observable + ObservableStream playbackStateStream; + @action Future init() async { - currentSong = _audioRepository.watchCurrentSong.asObservable(); + if (!_initialized) { + print('AudioStore.init'); + currentSong = _audioRepository.currentSongStream.asObservable(); - _disposer = autorun((_) { - updateSong(currentSong.value); - }); + _disposers.add(autorun((_) { + updateSong(currentSong.value); + })); + + playbackStateStream = _audioRepository.playbackStateStream.asObservable(); + + _initialized = true; + } } void dispose() { - _disposer(); + print('AudioStore.dispose'); + _disposers.forEach((d) => d()); } @action @@ -44,6 +58,16 @@ abstract class _AudioStore with Store { _audioRepository.playSong(index, songList); } + @action + Future play() async { + _audioRepository.play(); + } + + @action + Future pause() async { + _audioRepository.pause(); + } + @action Future updateSong(Song streamValue) async { print('updateSong'); @@ -52,5 +76,4 @@ abstract class _AudioStore with Store { song = streamValue; } } - -} \ No newline at end of file +} diff --git a/lib/presentation/state/audio_store.g.dart b/lib/presentation/state/audio_store.g.dart index 317d07e..4ed4b47 100644 --- a/lib/presentation/state/audio_store.g.dart +++ b/lib/presentation/state/audio_store.g.dart @@ -43,6 +43,26 @@ mixin _$AudioStore on _AudioStore, Store { }, _$songAtom, name: '${_$songAtom.name}_set'); } + final _$playbackStateStreamAtom = + Atom(name: '_AudioStore.playbackStateStream'); + + @override + ObservableStream get playbackStateStream { + _$playbackStateStreamAtom.context + .enforceReadPolicy(_$playbackStateStreamAtom); + _$playbackStateStreamAtom.reportObserved(); + return super.playbackStateStream; + } + + @override + set playbackStateStream(ObservableStream value) { + _$playbackStateStreamAtom.context.conditionallyRunInAction(() { + super.playbackStateStream = value; + _$playbackStateStreamAtom.reportChanged(); + }, _$playbackStateStreamAtom, + name: '${_$playbackStateStreamAtom.name}_set'); + } + final _$initAsyncAction = AsyncAction('init'); @override @@ -57,6 +77,20 @@ mixin _$AudioStore on _AudioStore, Store { return _$playSongAsyncAction.run(() => super.playSong(index, songList)); } + final _$playAsyncAction = AsyncAction('play'); + + @override + Future play() { + return _$playAsyncAction.run(() => super.play()); + } + + final _$pauseAsyncAction = AsyncAction('pause'); + + @override + Future pause() { + return _$pauseAsyncAction.run(() => super.pause()); + } + final _$updateSongAsyncAction = AsyncAction('updateSong'); @override @@ -67,7 +101,7 @@ mixin _$AudioStore on _AudioStore, Store { @override String toString() { final string = - 'currentSong: ${currentSong.toString()},song: ${song.toString()}'; + 'currentSong: ${currentSong.toString()},song: ${song.toString()},playbackStateStream: ${playbackStateStream.toString()}'; return '{$string}'; } } diff --git a/lib/presentation/state/music_data_store.dart b/lib/presentation/state/music_data_store.dart index b5fa90b..78421c5 100644 --- a/lib/presentation/state/music_data_store.dart +++ b/lib/presentation/state/music_data_store.dart @@ -23,6 +23,8 @@ abstract class _MusicDataStore with Store { _getAlbums = GetAlbums(_musicDataRepository), _getSongs = GetSongs(_musicDataRepository); + bool _initialized = false; + final UpdateDatabase _updateDatabase; final GetAlbums _getAlbums; final GetSongs _getSongs; @@ -39,6 +41,17 @@ abstract class _MusicDataStore with Store { @observable bool isUpdatingDatabase = false; + @action + Future init() async { + if (!_initialized) { + print('MusicDataStore.init'); + fetchAlbums(); + fetchSongs(); + + _initialized = true; + } + } + @action Future updateDatabase() async { isUpdatingDatabase = true; diff --git a/lib/presentation/state/music_data_store.g.dart b/lib/presentation/state/music_data_store.g.dart index 12c48d2..4882f30 100644 --- a/lib/presentation/state/music_data_store.g.dart +++ b/lib/presentation/state/music_data_store.g.dart @@ -79,6 +79,13 @@ mixin _$MusicDataStore on _MusicDataStore, Store { }, _$isUpdatingDatabaseAtom, name: '${_$isUpdatingDatabaseAtom.name}_set'); } + final _$initAsyncAction = AsyncAction('init'); + + @override + Future init() { + return _$initAsyncAction.run(() => super.init()); + } + final _$updateDatabaseAsyncAction = AsyncAction('updateDatabase'); @override diff --git a/lib/presentation/widgets/album_art.dart b/lib/presentation/widgets/album_art.dart index 9d25389..5497bc0 100644 --- a/lib/presentation/widgets/album_art.dart +++ b/lib/presentation/widgets/album_art.dart @@ -16,10 +16,14 @@ class AlbumArt extends StatelessWidget { elevation: 2.0, clipBehavior: Clip.antiAlias, margin: const EdgeInsets.all(0), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(2.0), + ), child: Stack( children: [ Image( image: getAlbumImage(song.albumArtPath), + fit: BoxFit.cover, ), Positioned( bottom: 0, diff --git a/lib/presentation/widgets/currently_playing_bar.dart b/lib/presentation/widgets/currently_playing_bar.dart index f733732..480d77c 100644 --- a/lib/presentation/widgets/currently_playing_bar.dart +++ b/lib/presentation/widgets/currently_playing_bar.dart @@ -6,6 +6,7 @@ import '../../domain/entities/song.dart'; import '../pages/currently_playing.dart'; import '../state/audio_store.dart'; import '../utils.dart'; +import 'play_pause_button.dart'; class CurrentlyPlayingBar extends StatelessWidget { const CurrentlyPlayingBar({Key key}) : super(key: key); @@ -52,18 +53,17 @@ class CurrentlyPlayingBar extends StatelessWidget { ], ), const Spacer(), - IconButton( - icon: Icon(Icons.favorite_border), - onPressed: () {}, - ), - IconButton( - icon: Icon(Icons.pause), - onPressed: () {}, - ), - IconButton( - icon: Icon(Icons.skip_next), - onPressed: () {}, + // IconButton( + // icon: Icon(Icons.favorite_border), + // onPressed: () {}, + // ), + const PlayPauseButton( + circle: false, ), + // IconButton( + // icon: Icon(Icons.skip_next), + // onPressed: () {}, + // ), ], ), ), diff --git a/lib/presentation/widgets/injection_widget.dart b/lib/presentation/widgets/injection_widget.dart index 6afb9c8..c11c0a5 100644 --- a/lib/presentation/widgets/injection_widget.dart +++ b/lib/presentation/widgets/injection_widget.dart @@ -1,16 +1,17 @@ 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'; -import 'package:mosh/system/datasources/moor_music_data_source.dart'; -import 'package:mosh/system/repositories/audio_repository_impl.dart'; -import 'package:mosh/system/repositories/music_data_repository_impl.dart'; import 'package:provider/provider.dart'; +import '../../domain/repositories/audio_repository.dart'; +import '../../domain/repositories/music_data_repository.dart'; +import '../../system/datasources/audio_manager.dart'; +import '../../system/datasources/local_music_fetcher.dart'; +import '../../system/datasources/moor_music_data_source.dart'; +import '../../system/repositories/audio_repository_impl.dart'; +import '../../system/repositories/music_data_repository_impl.dart'; +import '../state/audio_store.dart'; +import '../state/music_data_store.dart'; + class InjectionWidget extends StatelessWidget { const InjectionWidget({Key key, this.child}) : super(key: key); @@ -18,6 +19,7 @@ class InjectionWidget extends StatelessWidget { @override Widget build(BuildContext context) { + print('InjectionWidget.build'); // TODO: this does not dispose correctly! use ProxyProvider final MusicDataRepository musicDataRepository = MusicDataRepositoryImpl( diff --git a/lib/presentation/widgets/play_pause_button.dart b/lib/presentation/widgets/play_pause_button.dart new file mode 100644 index 0000000..02f2a4a --- /dev/null +++ b/lib/presentation/widgets/play_pause_button.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:provider/provider.dart'; + +import '../../domain/entities/playback_state.dart'; +import '../state/audio_store.dart'; + +class PlayPauseButton extends StatelessWidget { + const PlayPauseButton({Key key, this.circle = false, this.iconSize = 24.0}) : super(key: key); + + final bool circle; + final double iconSize; + + @override + Widget build(BuildContext context) { + final AudioStore audioStore = Provider.of(context); + + return Observer( + builder: (BuildContext context) { + switch (audioStore.playbackStateStream.value) { + case PlaybackState.playing: + return IconButton( + icon: circle ? Icon(Icons.pause_circle_filled) : Icon(Icons.pause), + iconSize: iconSize, + onPressed: () { + audioStore.pause(); + }, + ); + case PlaybackState.paused: + return IconButton( + icon: circle ? Icon(Icons.play_circle_filled) : Icon(Icons.play_arrow), + iconSize: iconSize, + onPressed: () { + audioStore.play(); + }, + ); + default: + return Container(height: 0, width: 0,); + } + }, + ); + } +} diff --git a/lib/system/datasources/audio_manager.dart b/lib/system/datasources/audio_manager.dart index 94544a9..d31e1cb 100644 --- a/lib/system/datasources/audio_manager.dart +++ b/lib/system/datasources/audio_manager.dart @@ -3,16 +3,31 @@ import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; +import '../../domain/entities/playback_state.dart' as entity; +import '../models/playback_state_model.dart'; import '../models/song_model.dart'; import 'audio_manager_contract.dart'; +typedef Conversion = T Function(S); + class AudioManagerImpl implements AudioManager { + final Stream _currentMediaItemStream = + AudioService.currentMediaItemStream; + final Stream _sourcePlaybackStateStream = + AudioService.playbackStateStream; + @override - Stream watchCurrentSong = AudioService.currentMediaItemStream.map( - (currentMediaItem) { - return SongModel.fromMediaItem(currentMediaItem); - }, - ); + Stream get currentSongStream => + _filterStream( + _currentMediaItemStream, + (MediaItem mi) => SongModel.fromMediaItem(mi), + ); + + @override + Stream get playbackStateStream => _filterStream( + _sourcePlaybackStateStream, + (PlaybackState ps) => PlaybackStateModel.fromASPlaybackState(ps), + ); @override Future playSong(int index, List songList) async { @@ -23,6 +38,16 @@ class AudioManagerImpl implements AudioManager { AudioService.playFromMediaId(queue[index].id); } + @override + Future play() async { + await AudioService.play(); + } + + @override + Future pause() async { + await AudioService.pause(); + } + Future _startAudioService() async { if (!await AudioService.running) { await AudioService.start( @@ -31,6 +56,18 @@ class AudioManagerImpl implements AudioManager { ); } } + + Stream _filterStream(Stream stream, Conversion fn) async* { + T lastItem; + + await for (final S item in stream) { + final T newItem = fn(item); + if (newItem != lastItem) { + lastItem = newItem; + yield newItem; + } + } + } } void _backgroundTaskEntrypoint() { @@ -45,6 +82,10 @@ class AudioPlayerTask extends BackgroundAudioTask { @override Future onStart() async { + // AudioServiceBackground.setState( + // controls: [], + // basicState: BasicPlaybackState.none, + // ); await _completer.future; } @@ -66,10 +107,8 @@ class AudioPlayerTask extends BackgroundAudioTask { basicState: BasicPlaybackState.playing, ); - Future.wait([ - AudioServiceBackground.setMediaItem(_mediaItems[mediaId]), - _audioPlayer.setFilePath(mediaId), - ]); + await AudioServiceBackground.setMediaItem(_mediaItems[mediaId]); + await _audioPlayer.setFilePath(mediaId); _audioPlayer.play(); } diff --git a/lib/system/datasources/audio_manager_contract.dart b/lib/system/datasources/audio_manager_contract.dart index 12d2e20..b2c5547 100644 --- a/lib/system/datasources/audio_manager_contract.dart +++ b/lib/system/datasources/audio_manager_contract.dart @@ -1,7 +1,11 @@ +import '../../domain/entities/playback_state.dart'; import '../models/song_model.dart'; abstract class AudioManager { - Stream get watchCurrentSong; + Stream get currentSongStream; + Stream get playbackStateStream; Future playSong(int index, List songList); + Future play(); + Future pause(); } \ No newline at end of file diff --git a/lib/system/models/playback_state_model.dart b/lib/system/models/playback_state_model.dart new file mode 100644 index 0000000..7a136b3 --- /dev/null +++ b/lib/system/models/playback_state_model.dart @@ -0,0 +1,26 @@ +import 'package:audio_service/audio_service.dart'; + +import '../../domain/entities/playback_state.dart' as entity; + +class PlaybackStateModel { + + static entity.PlaybackState fromASPlaybackState(PlaybackState playbackState) { + if (playbackState == null) { + return null; + } + + switch (playbackState.basicState) { + case BasicPlaybackState.none: + return entity.PlaybackState.none; + case BasicPlaybackState.stopped: + return entity.PlaybackState.stopped; + case BasicPlaybackState.paused: + return entity.PlaybackState.paused; + case BasicPlaybackState.playing: + return entity.PlaybackState.playing; + default: + return entity.PlaybackState.none; + } + } + +} diff --git a/lib/system/models/song_model.dart b/lib/system/models/song_model.dart index cdcab1a..4c69aae 100644 --- a/lib/system/models/song_model.dart +++ b/lib/system/models/song_model.dart @@ -48,15 +48,19 @@ class SongModel extends Song { // TODO: test factory SongModel.fromMediaItem(MediaItem mediaItem) { - final String artUri = mediaItem.artUri.replaceFirst('file://', ''); + if (mediaItem == null) { + return null; + } + + final String artUri = mediaItem.artUri?.replaceFirst('file://', ''); return SongModel( - title: mediaItem.title, - album: mediaItem.album, - artist: mediaItem.artist, - path: mediaItem.id, - albumArtPath: artUri, - ); + title: mediaItem.title, + album: mediaItem.album, + artist: mediaItem.artist, + path: mediaItem.id, + albumArtPath: artUri, + ); } final int id; diff --git a/lib/system/repositories/audio_repository_impl.dart b/lib/system/repositories/audio_repository_impl.dart index 99a22f2..e67528f 100644 --- a/lib/system/repositories/audio_repository_impl.dart +++ b/lib/system/repositories/audio_repository_impl.dart @@ -1,6 +1,7 @@ import 'package:dartz/dartz.dart'; import '../../core/error/failures.dart'; +import '../../domain/entities/playback_state.dart'; import '../../domain/entities/song.dart'; import '../../domain/repositories/audio_repository.dart'; import '../datasources/audio_manager_contract.dart'; @@ -12,7 +13,11 @@ class AudioRepositoryImpl implements AudioRepository { final AudioManager _audioManager; @override - Stream get watchCurrentSong => _audioManager.watchCurrentSong; + Stream get currentSongStream => _audioManager.currentSongStream; + + @override + Stream get playbackStateStream => + _audioManager.playbackStateStream; @override Future> playSong(int index, List songList) async { @@ -25,4 +30,16 @@ class AudioRepositoryImpl implements AudioRepository { } return Left(IndexFailure()); } + + @override + Future> play() async { + await _audioManager.play(); + return Right(null); + } + + @override + Future> pause() async { + await _audioManager.pause(); + return Right(null); + } }