implemented currentSong

This commit is contained in:
Moritz Weber 2020-04-09 17:50:28 +02:00
parent c425b63fc2
commit 7fbd4a07a2
13 changed files with 406 additions and 212 deletions

View file

@ -4,5 +4,7 @@ import '../../core/error/failures.dart';
import '../entities/song.dart'; import '../entities/song.dart';
abstract class AudioRepository { abstract class AudioRepository {
Stream<Song> get watchCurrentSong;
Future<Either<Failure, void>> playSong(int index, List<Song> songList); Future<Either<Failure, void>> playSong(int index, List<Song> songList);
} }

View file

@ -1,17 +1,17 @@
import 'package:audio_service/audio_service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:mosh/presentation/widgets/injection_widget.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'presentation/pages/currently_playing.dart';
import 'presentation/pages/home_page.dart'; import 'presentation/pages/home_page.dart';
import 'presentation/pages/library_page.dart'; import 'presentation/pages/library_page.dart';
import 'presentation/pages/settings_page.dart'; import 'presentation/pages/settings_page.dart';
import 'presentation/state/audio_store.dart';
import 'presentation/state/music_data_store.dart'; import 'presentation/state/music_data_store.dart';
import 'presentation/theming.dart'; import 'presentation/theming.dart';
import 'presentation/widgets/audio_service_widget.dart'; import 'presentation/widgets/audio_service_widget.dart';
import 'presentation/widgets/injection_widget.dart';
import 'presentation/widgets/navbar.dart'; import 'presentation/widgets/navbar.dart';
import 'system/datasources/audio_manager.dart';
void main() => runApp(MyApp()); void main() => runApp(MyApp());
@ -23,12 +23,16 @@ class MyApp extends StatelessWidget {
DeviceOrientation.portraitUp, DeviceOrientation.portraitUp,
]); ]);
return MaterialApp( return InjectionWidget(
title: 'mucke', child: AudioServiceWidget(
theme: theme(), child: MaterialApp(
home: const AudioServiceWidget( title: 'mucke',
child: InjectionWidget( theme: theme(),
child: RootPage(), initialRoute: '/',
routes: {
'/': (context) => const RootPage(),
'/playing': (context) => const CurrentlyPlayingPage(),
},
), ),
), ),
); );
@ -43,7 +47,7 @@ class RootPage extends StatefulWidget {
} }
class _RootPageState extends State<RootPage> { class _RootPageState extends State<RootPage> {
var navIndex = 0; var navIndex = 1;
final List<Widget> _pages = <Widget>[ final List<Widget> _pages = <Widget>[
HomePage(), HomePage(),
@ -61,16 +65,19 @@ class _RootPageState extends State<RootPage> {
_musicStore.fetchAlbums(); _musicStore.fetchAlbums();
_musicStore.fetchSongs(); _musicStore.fetchSongs();
// TODO: don't do this here... final AudioStore _audioStore = Provider.of<AudioStore>(context);
AudioService.start( _audioStore.init();
backgroundTaskEntrypoint: _backgroundTaskEntrypoint,
enableQueue: true,
androidStopOnRemoveTask: true,
);
super.didChangeDependencies(); super.didChangeDependencies();
} }
@override
void dispose() {
final AudioStore _audioStore = Provider.of<AudioStore>(context);
_audioStore.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -89,7 +96,3 @@ class _RootPageState extends State<RootPage> {
); );
} }
} }
void _backgroundTaskEntrypoint() {
AudioServiceBackground.run(() => AudioPlayerTask());
}

View file

@ -1,114 +1,126 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mosh/presentation/theming.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:provider/provider.dart';
import '../../domain/entities/song.dart';
import '../state/audio_store.dart';
import '../theming.dart';
import '../widgets/album_art.dart'; import '../widgets/album_art.dart';
import '../widgets/queue_card.dart'; import '../widgets/queue_card.dart';
import '../widgets/time_progress_indicator.dart'; import '../widgets/time_progress_indicator.dart';
class CurrentlyPlayingPage extends StatefulWidget { class CurrentlyPlayingPage extends StatelessWidget {
CurrentlyPlayingPage({Key key}) : super(key: key); const CurrentlyPlayingPage({Key key}) : super(key: key);
@override
_CurrentlyPlayingPageState createState() => _CurrentlyPlayingPageState();
}
class _CurrentlyPlayingPageState extends State<CurrentlyPlayingPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
print('CurrentlyPlayingPage.build');
final AudioStore audioStore = Provider.of<AudioStore>(context);
return Scaffold( return Scaffold(
body: SafeArea( body: SafeArea(
child: LayoutBuilder( child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) => Stack( builder: (BuildContext context, BoxConstraints constraints) =>
children: <Widget>[ Observer(
Padding( builder: (BuildContext context) {
padding: const EdgeInsets.only( print('CurrentlyPlayingPage.build -> Observer.build');
left: 12.0, final Song song = audioStore.song;
right: 12.0,
top: 8.0, return Stack(
), children: <Widget>[
child: Column( Padding(
crossAxisAlignment: CrossAxisAlignment.start, padding: const EdgeInsets.only(
children: [ left: 12.0,
Row( right: 12.0,
top: 8.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
IconButton( Row(
icon: Icon(Icons.expand_more), children: [
onPressed: () { IconButton(
Navigator.pop(context); icon: Icon(Icons.expand_more),
}, onPressed: () {
Navigator.pop(context);
},
),
IconButton(
icon: Icon(Icons.more_vert),
onPressed: () {},
)
],
mainAxisAlignment: MainAxisAlignment.spaceBetween,
), ),
IconButton( const Spacer(),
icon: Icon(Icons.more_vert), Padding(
onPressed: () {}, padding: const EdgeInsets.symmetric(horizontal: 16.0),
) child: AlbumArt(
], song: song,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
),
const Spacer(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: AlbumArt(),
),
const Spacer(
flex: 4,
),
Row(
children: [
Icon(
Icons.link,
size: 20.0,
),
Container(
width: 40,
),
Icon(
Icons.favorite,
size: 20.0,
color: RASPBERRY,
),
Container(
width: 40,
),
Icon(
Icons.remove_circle_outline,
size: 20.0,
),
],
mainAxisAlignment: MainAxisAlignment.center,
),
const Spacer(
flex: 3,
),
TimeProgressIndicator(),
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),
Icon(
Icons.play_circle_filled,
size: 52.0,
), ),
Icon(Icons.skip_next, size: 32.0), ),
Icon(Icons.shuffle, size: 20.0), const Spacer(
], flex: 4,
mainAxisAlignment: MainAxisAlignment.spaceBetween, ),
), Row(
children: [
Icon(
Icons.link,
size: 20.0,
),
Container(
width: 40,
),
Icon(
Icons.favorite,
size: 20.0,
color: RASPBERRY,
),
Container(
width: 40,
),
Icon(
Icons.remove_circle_outline,
size: 20.0,
),
],
mainAxisAlignment: MainAxisAlignment.center,
),
const Spacer(
flex: 3,
),
TimeProgressIndicator(),
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),
Icon(
Icons.play_circle_filled,
size: 52.0,
),
Icon(Icons.skip_next, size: 32.0),
Icon(Icons.shuffle, size: 20.0),
],
mainAxisAlignment: MainAxisAlignment.spaceBetween,
),
),
Container(
height: 64,
),
],
), ),
Container( ),
height: 64, QueueCard(
), boxConstraints: constraints,
], ),
), ],
), );
QueueCard( },
boxConstraints: constraints,
),
],
), ),
), ),
), ),

View file

@ -18,9 +18,39 @@ abstract class _AudioStore with Store {
final MusicDataRepository _musicDataRepository; final MusicDataRepository _musicDataRepository;
final AudioRepository _audioRepository; final AudioRepository _audioRepository;
ReactionDisposer _disposer;
@observable
ObservableStream<Song> currentSong;
@observable
Song song;
@action
Future<void> init() async {
currentSong = _audioRepository.watchCurrentSong.asObservable();
_disposer = autorun((_) {
updateSong(currentSong.value);
});
}
void dispose() {
_disposer();
}
@action @action
Future<void> playSong(int index, List<Song> songList) async { Future<void> playSong(int index, List<Song> songList) async {
_audioRepository.playSong(index, songList); _audioRepository.playSong(index, songList);
} }
@action
Future<void> updateSong(Song streamValue) async {
print('updateSong');
if (streamValue != null && streamValue != song) {
print('actually updating...');
song = streamValue;
}
}
} }

View file

@ -9,6 +9,47 @@ part of 'audio_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 // 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 { mixin _$AudioStore on _AudioStore, Store {
final _$currentSongAtom = Atom(name: '_AudioStore.currentSong');
@override
ObservableStream<Song> get currentSong {
_$currentSongAtom.context.enforceReadPolicy(_$currentSongAtom);
_$currentSongAtom.reportObserved();
return super.currentSong;
}
@override
set currentSong(ObservableStream<Song> value) {
_$currentSongAtom.context.conditionallyRunInAction(() {
super.currentSong = value;
_$currentSongAtom.reportChanged();
}, _$currentSongAtom, name: '${_$currentSongAtom.name}_set');
}
final _$songAtom = Atom(name: '_AudioStore.song');
@override
Song get song {
_$songAtom.context.enforceReadPolicy(_$songAtom);
_$songAtom.reportObserved();
return super.song;
}
@override
set song(Song value) {
_$songAtom.context.conditionallyRunInAction(() {
super.song = value;
_$songAtom.reportChanged();
}, _$songAtom, name: '${_$songAtom.name}_set');
}
final _$initAsyncAction = AsyncAction('init');
@override
Future<void> init() {
return _$initAsyncAction.run(() => super.init());
}
final _$playSongAsyncAction = AsyncAction('playSong'); final _$playSongAsyncAction = AsyncAction('playSong');
@override @override
@ -16,9 +57,17 @@ mixin _$AudioStore on _AudioStore, Store {
return _$playSongAsyncAction.run(() => super.playSong(index, songList)); return _$playSongAsyncAction.run(() => super.playSong(index, songList));
} }
final _$updateSongAsyncAction = AsyncAction('updateSong');
@override
Future<void> updateSong(Song streamValue) {
return _$updateSongAsyncAction.run(() => super.updateSong(streamValue));
}
@override @override
String toString() { String toString() {
final string = ''; final string =
'currentSong: ${currentSong.toString()},song: ${song.toString()}';
return '{$string}'; return '{$string}';
} }
} }

View file

@ -1,7 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../../domain/entities/song.dart';
import '../utils.dart';
class AlbumArt extends StatelessWidget { class AlbumArt extends StatelessWidget {
const AlbumArt({Key key}) : super(key: key); const AlbumArt({Key key, this.song}) : super(key: key);
final Song song;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -10,10 +15,12 @@ class AlbumArt extends StatelessWidget {
child: Card( child: Card(
elevation: 2.0, elevation: 2.0,
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
margin: EdgeInsets.all(0), margin: const EdgeInsets.all(0),
child: Stack( child: Stack(
children: [ children: [
Image.asset('assets/twilight.jpg'), Image(
image: getAlbumImage(song.albumArtPath),
),
Positioned( Positioned(
bottom: 0, bottom: 0,
left: 0, left: 0,
@ -24,13 +31,13 @@ class AlbumArt extends StatelessWidget {
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [ colors: const [
const Color(0x00555555), Color(0x00555555),
const Color(0x77333333), Color(0x77333333),
const Color(0xCC111111), Color(0xCC111111),
const Color(0xEE000000) Color(0xEE000000)
], ],
stops: [ stops: const [
0.0, 0.0,
0.6, 0.6,
0.8, 0.8,
@ -48,20 +55,20 @@ class AlbumArt extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Text(
'Guardians of Asgaard', song.title,
style: Theme.of(context).textTheme.title, style: Theme.of(context).textTheme.title,
), ),
Container( Container(
height: 4.0, height: 4.0,
), ),
const Text( Text(
'Amon Amarth', song.artist,
style: TextStyle( style: TextStyle(
color: Colors.white70, color: Colors.white70,
), ),
), ),
const Text( Text(
'Twilight of the Thunder God', song.album,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w300, fontWeight: FontWeight.w300,
color: Colors.white70, color: Colors.white70,

View file

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:provider/provider.dart';
import '../../domain/entities/song.dart';
import '../pages/currently_playing.dart';
import '../state/audio_store.dart';
import '../utils.dart';
class CurrentlyPlayingBar extends StatelessWidget {
const CurrentlyPlayingBar({Key key}) : super(key: key);
@override
Widget build(BuildContext context) {
final AudioStore audioStore = Provider.of<AudioStore>(context);
return Observer(
builder: (BuildContext context) {
if (audioStore.currentSong.value != null) {
final Song song = audioStore.currentSong.value;
return Column(
children: <Widget>[
Container(
child: const LinearProgressIndicator(
value: 0.42,
),
height: 2,
),
GestureDetector(
onTap: () => _onTap(context),
child: Row(
children: <Widget>[
Image(
image: getAlbumImage(song.albumArtPath),
height: 64.0,
),
Container(
width: 10.0,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(song.title),
Text(
song.artist,
style: TextStyle(
fontSize: 12.0,
color: Colors.white70,
),
)
],
),
const Spacer(),
IconButton(
icon: Icon(Icons.favorite_border),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.pause),
onPressed: () {},
),
IconButton(
icon: Icon(Icons.skip_next),
onPressed: () {},
),
],
),
),
],
);
}
return Container(
height: 0,
);
},
);
}
Future<void> _onTap(BuildContext context) async {
Navigator.push(
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) => const CurrentlyPlayingPage(),
),
);
}
}

View file

@ -18,6 +18,8 @@ class InjectionWidget extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
// TODO: this does not dispose correctly! use ProxyProvider
final MusicDataRepository musicDataRepository = MusicDataRepositoryImpl( final MusicDataRepository musicDataRepository = MusicDataRepositoryImpl(
localMusicFetcher: LocalMusicFetcherImpl(FlutterAudioQuery()), localMusicFetcher: LocalMusicFetcherImpl(FlutterAudioQuery()),
musicDataSource: MoorMusicDataSource(), musicDataSource: MoorMusicDataSource(),

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:mosh/presentation/pages/currently_playing.dart';
import 'currently_playing_bar.dart';
class NavBar extends StatefulWidget { class NavBar extends StatefulWidget {
const NavBar({Key key, @required this.onTap, @required this.currentIndex}) const NavBar({Key key, @required this.onTap, @required this.currentIndex})
@ -24,77 +25,39 @@ class _NavBarState extends State<NavBar> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
Container( const CurrentlyPlayingBar(),
child: const LinearProgressIndicator(
value: 0.42,
),
height: 2,
),
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute<Widget>(
builder: (BuildContext context) => CurrentlyPlayingPage()),
);
},
child: Row(
children: <Widget>[
const Image(
image: AssetImage('assets/twilight.jpg'),
height: 64.0,
),
Container(
width: 10.0,
),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Guardians of Asgaard'),
Text(
"Amon Amarth",
style: TextStyle(
fontSize: 12.0,
color: Colors.white70,
),
)
],
),
Spacer(),
IconButton(icon: Icon(Icons.favorite_border), onPressed: () {}),
IconButton(icon: Icon(Icons.pause), onPressed: () {}),
IconButton(icon: Icon(Icons.skip_next), onPressed: () {}),
],
),
),
Container( Container(
color: Colors.grey[850], color: Colors.grey[850],
height: 1.0, height: 1.0,
), ),
BottomNavigationBar( BottomNavigationBar(
backgroundColor: Colors.grey[900], backgroundColor: Colors.grey[900],
currentIndex: widget.currentIndex, currentIndex: widget.currentIndex,
onTap: widget.onTap, onTap: widget.onTap,
items: [ items: [
BottomNavigationBarItem( BottomNavigationBarItem(
icon: Icon(Icons.home), icon: Icon(Icons.home),
title: Text( title: Text(
"Home", 'Home',
style: _optionTextStyle, style: _optionTextStyle,
)), ),
BottomNavigationBarItem( ),
icon: Icon(Icons.library_music), BottomNavigationBarItem(
title: Text( icon: Icon(Icons.library_music),
"Library", title: Text(
style: _optionTextStyle, 'Library',
)), style: _optionTextStyle,
BottomNavigationBarItem( ),
icon: Icon(Icons.settings), ),
title: Text( BottomNavigationBarItem(
"Settings", icon: Icon(Icons.settings),
style: _optionTextStyle, title: Text(
)), 'Settings',
]) style: _optionTextStyle,
),
),
],
)
], ],
), ),
); );

View file

@ -7,13 +7,34 @@ import '../models/song_model.dart';
import 'audio_manager_contract.dart'; import 'audio_manager_contract.dart';
class AudioManagerImpl implements AudioManager { class AudioManagerImpl implements AudioManager {
@override
Stream<SongModel> watchCurrentSong = AudioService.currentMediaItemStream.map(
(currentMediaItem) {
return SongModel.fromMediaItem(currentMediaItem);
},
);
@override @override
Future<void> playSong(int index, List<SongModel> songList) async { Future<void> playSong(int index, List<SongModel> songList) async {
await _startAudioService();
final List<MediaItem> queue = songList.map((s) => s.toMediaItem()).toList(); 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); AudioService.playFromMediaId(queue[index].id);
} }
Future<void> _startAudioService() async {
if (!await AudioService.running) {
await AudioService.start(
backgroundTaskEntrypoint: _backgroundTaskEntrypoint,
enableQueue: true,
);
}
}
}
void _backgroundTaskEntrypoint() {
AudioServiceBackground.run(() => AudioPlayerTask());
} }
class AudioPlayerTask extends BackgroundAudioTask { class AudioPlayerTask extends BackgroundAudioTask {
@ -24,13 +45,6 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override @override
Future<void> onStart() async { Future<void> onStart() async {
print('onStart');
AudioServiceBackground.setState(
controls: [pauseControl, stopControl],
basicState: BasicPlaybackState.playing,
);
await _completer.future; await _completer.future;
} }
@ -47,11 +61,16 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override @override
Future<void> onPlayFromMediaId(String mediaId) async { Future<void> onPlayFromMediaId(String mediaId) async {
AudioServiceBackground.setState(
controls: [pauseControl, stopControl],
basicState: BasicPlaybackState.playing,
);
// await _audioPlayer.setFilePath(_mediaItems[mediaId]); Future.wait([
AudioServiceBackground.setMediaItem(_mediaItems[mediaId]); AudioServiceBackground.setMediaItem(_mediaItems[mediaId]),
_audioPlayer.setFilePath(mediaId),
]);
await _audioPlayer.setFilePath(mediaId);
_audioPlayer.play(); _audioPlayer.play();
} }

View file

@ -1,5 +1,7 @@
import '../models/song_model.dart'; import '../models/song_model.dart';
abstract class AudioManager { abstract class AudioManager {
Stream<SongModel> get watchCurrentSong;
Future<void> playSong(int index, List<SongModel> songList); Future<void> playSong(int index, List<SongModel> songList);
} }

View file

@ -46,6 +46,19 @@ class SongModel extends Song {
); );
} }
// TODO: test
factory SongModel.fromMediaItem(MediaItem mediaItem) {
final String artUri = mediaItem.artUri.replaceFirst('file://', '');
return SongModel(
title: mediaItem.title,
album: mediaItem.album,
artist: mediaItem.artist,
path: mediaItem.id,
albumArtPath: artUri,
);
}
final int id; final int id;
SongsCompanion toSongsCompanion() => SongsCompanion( SongsCompanion toSongsCompanion() => SongsCompanion(

View file

@ -1,10 +1,10 @@
import 'package:dartz/dartz.dart'; import 'package:dartz/dartz.dart';
import 'package:mosh/system/models/song_model.dart';
import '../../core/error/failures.dart'; import '../../core/error/failures.dart';
import '../../domain/entities/song.dart'; import '../../domain/entities/song.dart';
import '../../domain/repositories/audio_repository.dart'; import '../../domain/repositories/audio_repository.dart';
import '../datasources/audio_manager_contract.dart'; import '../datasources/audio_manager_contract.dart';
import '../models/song_model.dart';
class AudioRepositoryImpl implements AudioRepository { class AudioRepositoryImpl implements AudioRepository {
AudioRepositoryImpl(this._audioManager); AudioRepositoryImpl(this._audioManager);
@ -12,12 +12,16 @@ class AudioRepositoryImpl implements AudioRepository {
final AudioManager _audioManager; final AudioManager _audioManager;
@override @override
Future<Either<Failure, void>> playSong(int index, List<Song> songList) async { Stream<Song> get watchCurrentSong => _audioManager.watchCurrentSong;
final List<SongModel> songModelList = songList.map((song) => song as SongModel).toList();
@override
Future<Either<Failure, void>> playSong(int index, List<Song> songList) async {
final List<SongModel> songModelList =
songList.map((song) => song as SongModel).toList();
if (0 <= index && index < songList.length) { if (0 <= index && index < songList.length) {
await _audioManager.playSong(index, songModelList); await _audioManager.playSong(index, songModelList);
return Right(null); return Right(null);
} }
return Left(IndexFailure()); return Left(IndexFailure());
} }