implemented play/pause

This commit is contained in:
Moritz Weber 2020-04-10 20:12:26 +02:00
parent 7fbd4a07a2
commit eff0dc29a6
18 changed files with 289 additions and 59 deletions

View file

@ -0,0 +1,6 @@
enum PlaybackState {
none,
playing,
paused,
stopped,
}

View file

@ -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<Song> get watchCurrentSong;
Stream<Song> get currentSongStream;
Stream<PlaybackState> get playbackStateStream;
Future<Either<Failure, void>> playSong(int index, List<Song> songList);
Future<Either<Failure, void>> play();
Future<Either<Failure, void>> pause();
}

View file

@ -50,7 +50,7 @@ class _RootPageState extends State<RootPage> {
var navIndex = 1;
final List<Widget> _pages = <Widget>[
HomePage(),
const HomePage(),
const LibraryPage(
key: PageStorageKey('LibraryPage'),
),
@ -62,8 +62,7 @@ class _RootPageState extends State<RootPage> {
@override
void didChangeDependencies() {
final MusicDataStore _musicStore = Provider.of<MusicDataStore>(context);
_musicStore.fetchAlbums();
_musicStore.fetchSongs();
_musicStore.init();
final AudioStore _audioStore = Provider.of<AudioStore>(context);
_audioStore.init();
@ -80,6 +79,7 @@ class _RootPageState extends State<RootPage> {
@override
Widget build(BuildContext context) {
print('RootPage.build');
return Scaffold(
body: IndexedStack(
index: navIndex,

View file

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

View file

@ -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<HomePage> {
@override
Widget build(BuildContext context) {
print('HomePage.build');
return Container(
child: Center(child: Text("Home Page"),),
child: const Center(
child: Text('Home Page'),
),
);
}
}
}

View file

@ -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<ReactionDisposer> _disposers = [];
@observable
ObservableStream<Song> currentSong;
@observable
Song song;
@observable
ObservableStream<PlaybackState> playbackStateStream;
@action
Future<void> 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<void> play() async {
_audioRepository.play();
}
@action
Future<void> pause() async {
_audioRepository.pause();
}
@action
Future<void> updateSong(Song streamValue) async {
print('updateSong');
@ -52,5 +76,4 @@ abstract class _AudioStore with Store {
song = streamValue;
}
}
}
}

View file

@ -43,6 +43,26 @@ mixin _$AudioStore on _AudioStore, Store {
}, _$songAtom, name: '${_$songAtom.name}_set');
}
final _$playbackStateStreamAtom =
Atom(name: '_AudioStore.playbackStateStream');
@override
ObservableStream<PlaybackState> get playbackStateStream {
_$playbackStateStreamAtom.context
.enforceReadPolicy(_$playbackStateStreamAtom);
_$playbackStateStreamAtom.reportObserved();
return super.playbackStateStream;
}
@override
set playbackStateStream(ObservableStream<PlaybackState> 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<void> play() {
return _$playAsyncAction.run(() => super.play());
}
final _$pauseAsyncAction = AsyncAction('pause');
@override
Future<void> 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}';
}
}

View file

@ -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<void> init() async {
if (!_initialized) {
print('MusicDataStore.init');
fetchAlbums();
fetchSongs();
_initialized = true;
}
}
@action
Future<void> updateDatabase() async {
isUpdatingDatabase = true;

View file

@ -79,6 +79,13 @@ mixin _$MusicDataStore on _MusicDataStore, Store {
}, _$isUpdatingDatabaseAtom, name: '${_$isUpdatingDatabaseAtom.name}_set');
}
final _$initAsyncAction = AsyncAction('init');
@override
Future<void> init() {
return _$initAsyncAction.run(() => super.init());
}
final _$updateDatabaseAsyncAction = AsyncAction('updateDatabase');
@override

View file

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

View file

@ -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: () {},
// ),
],
),
),

View file

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

View file

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

View file

@ -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<S, T> = T Function(S);
class AudioManagerImpl implements AudioManager {
final Stream<MediaItem> _currentMediaItemStream =
AudioService.currentMediaItemStream;
final Stream<PlaybackState> _sourcePlaybackStateStream =
AudioService.playbackStateStream;
@override
Stream<SongModel> watchCurrentSong = AudioService.currentMediaItemStream.map(
(currentMediaItem) {
return SongModel.fromMediaItem(currentMediaItem);
},
);
Stream<SongModel> get currentSongStream =>
_filterStream<MediaItem, SongModel>(
_currentMediaItemStream,
(MediaItem mi) => SongModel.fromMediaItem(mi),
);
@override
Stream<entity.PlaybackState> get playbackStateStream => _filterStream(
_sourcePlaybackStateStream,
(PlaybackState ps) => PlaybackStateModel.fromASPlaybackState(ps),
);
@override
Future<void> playSong(int index, List<SongModel> songList) async {
@ -23,6 +38,16 @@ class AudioManagerImpl implements AudioManager {
AudioService.playFromMediaId(queue[index].id);
}
@override
Future<void> play() async {
await AudioService.play();
}
@override
Future<void> pause() async {
await AudioService.pause();
}
Future<void> _startAudioService() async {
if (!await AudioService.running) {
await AudioService.start(
@ -31,6 +56,18 @@ class AudioManagerImpl implements AudioManager {
);
}
}
Stream<T> _filterStream<S, T>(Stream<S> stream, Conversion<S, T> 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<void> 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();
}

View file

@ -1,7 +1,11 @@
import '../../domain/entities/playback_state.dart';
import '../models/song_model.dart';
abstract class AudioManager {
Stream<SongModel> get watchCurrentSong;
Stream<SongModel> get currentSongStream;
Stream<PlaybackState> get playbackStateStream;
Future<void> playSong(int index, List<SongModel> songList);
Future<void> play();
Future<void> pause();
}

View file

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

View file

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

View file

@ -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<Song> get watchCurrentSong => _audioManager.watchCurrentSong;
Stream<Song> get currentSongStream => _audioManager.currentSongStream;
@override
Stream<PlaybackState> get playbackStateStream =>
_audioManager.playbackStateStream;
@override
Future<Either<Failure, void>> playSong(int index, List<Song> songList) async {
@ -25,4 +30,16 @@ class AudioRepositoryImpl implements AudioRepository {
}
return Left(IndexFailure());
}
@override
Future<Either<Failure, void>> play() async {
await _audioManager.play();
return Right(null);
}
@override
Future<Either<Failure, void>> pause() async {
await _audioManager.pause();
return Right(null);
}
}