AlbumDetailsPage and queue preparations

This commit is contained in:
Moritz Weber 2020-04-16 08:04:34 +02:00
parent 9609a89c23
commit 4ada7ce70f
12 changed files with 176 additions and 22 deletions

View file

@ -6,6 +6,7 @@ import '../entities/song.dart';
abstract class MusicDataRepository { abstract class MusicDataRepository {
Future<Either<Failure, List<Song>>> getSongs(); Future<Either<Failure, List<Song>>> getSongs();
Future<Either<Failure, List<Song>>> getSongsFromAlbum(Album album);
Future<Either<Failure, List<Album>>> getAlbums(); Future<Either<Failure, List<Album>>> getAlbums();
Future<void> updateDatabase(); Future<void> updateDatabase();
} }

View file

@ -1,7 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:provider/provider.dart';
import '../../domain/entities/album.dart'; import '../../domain/entities/album.dart';
import '../../domain/entities/song.dart';
import '../state/audio_store.dart';
import '../state/music_data_store.dart';
import '../utils.dart' as utils; import '../utils.dart' as utils;
import '../widgets/album_art_list_tile.dart';
class AlbumDetailsPage extends StatelessWidget { class AlbumDetailsPage extends StatelessWidget {
const AlbumDetailsPage({Key key, @required this.album}) : super(key: key); const AlbumDetailsPage({Key key, @required this.album}) : super(key: key);
@ -10,12 +16,79 @@ class AlbumDetailsPage extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( final MusicDataStore musicDataStore = Provider.of<MusicDataStore>(context);
child: Column( final AudioStore audioStore = Provider.of<AudioStore>(context);
children: <Widget>[
Image( return Observer(
builder: (BuildContext context) => CustomScrollView(
slivers: <Widget>[
SliverAppBar(
brightness: Brightness.dark,
pinned: true,
expandedHeight: 250.0,
backgroundColor: Colors.grey[900],
iconTheme: IconThemeData(
color: Colors.white,
),
flexibleSpace: FlexibleSpaceBar(
centerTitle: true,
titlePadding: const EdgeInsets.only(
bottom: 16.0,
left: 16.0,
right: 16.0,
),
title: Text(
album.title,
style: TextStyle(
fontWeight: FontWeight.w300,
fontSize: 16.0,
color: Colors.white,
),
textAlign: TextAlign.center,
),
background: Stack(
children: [
Positioned(
left: 0,
right: 0,
child: Image(
image: utils.getAlbumImage(album.albumArtPath), image: utils.getAlbumImage(album.albumArtPath),
), ),
),
Container(
color: Colors.black45,
),
],
),
),
),
SliverList(
delegate: SliverChildBuilderDelegate(
(_, int index) {
if (index.isEven) {
final songIndex = (index / 2).round();
final Song song = musicDataStore.albumSongs[songIndex];
return AlbumArtListTile(
title: song.title,
subtitle: '${song.artist}',
albumArtPath: song.albumArtPath,
onTap: () => audioStore.playSong(songIndex, musicDataStore.albumSongs),
);
}
return const Divider(
height: 4.0,
);
},
semanticIndexCallback: (Widget widget, int localIndex) {
if (localIndex.isEven) {
return localIndex ~/ 2;
}
return null;
},
childCount: musicDataStore.albumSongs.length * 2,
),
),
], ],
), ),
); );

View file

@ -49,6 +49,7 @@ class AlbumsPage extends StatelessWidget {
subtitle: album.artist, subtitle: album.artist,
albumArtPath: album.albumArtPath, albumArtPath: album.albumArtPath,
onTap: () { onTap: () {
store.fetchSongsFromAlbum(album);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute<Widget>( MaterialPageRoute<Widget>(

View file

@ -12,8 +12,10 @@ class LibraryTabContainer extends StatelessWidget {
length: 3, length: 3,
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: const <Widget>[ children: <Widget>[
TabBar( Container(
color: Colors.grey[900],
child: TabBar(
tabs: <Tab>[ tabs: <Tab>[
Tab( Tab(
text: 'Artists', text: 'Artists',
@ -26,6 +28,7 @@ class LibraryTabContainer extends StatelessWidget {
), ),
], ],
), ),
),
Expanded( Expanded(
child: TabBarView( child: TabBarView(
children: <Widget>[ children: <Widget>[

View file

@ -18,11 +18,13 @@ class MusicDataStore extends _MusicDataStore with _$MusicDataStore {
} }
abstract class _MusicDataStore with Store { abstract class _MusicDataStore with Store {
_MusicDataStore(MusicDataRepository _musicDataRepository) _MusicDataStore(this._musicDataRepository)
: _updateDatabase = UpdateDatabase(_musicDataRepository), : _updateDatabase = UpdateDatabase(_musicDataRepository),
_getAlbums = GetAlbums(_musicDataRepository), _getAlbums = GetAlbums(_musicDataRepository),
_getSongs = GetSongs(_musicDataRepository); _getSongs = GetSongs(_musicDataRepository);
final MusicDataRepository _musicDataRepository;
bool _initialized = false; bool _initialized = false;
final UpdateDatabase _updateDatabase; final UpdateDatabase _updateDatabase;
@ -41,6 +43,9 @@ abstract class _MusicDataStore with Store {
@observable @observable
bool isUpdatingDatabase = false; bool isUpdatingDatabase = false;
@observable
ObservableList<Song> albumSongs = <Song>[].asObservable();
@action @action
Future<void> init() async { Future<void> init() async {
if (!_initialized) { if (!_initialized) {
@ -90,4 +95,14 @@ abstract class _MusicDataStore with Store {
isFetchingSongs = false; isFetchingSongs = false;
} }
@action
Future<void> fetchSongsFromAlbum(Album album) async {
final result = await _musicDataRepository.getSongsFromAlbum(album);
albumSongs.clear();
result.fold(
(_) => albumSongs = <Song>[].asObservable(),
(songList) => albumSongs.addAll(songList),
);
}
} }

View file

@ -79,6 +79,23 @@ mixin _$MusicDataStore on _MusicDataStore, Store {
}, _$isUpdatingDatabaseAtom, name: '${_$isUpdatingDatabaseAtom.name}_set'); }, _$isUpdatingDatabaseAtom, name: '${_$isUpdatingDatabaseAtom.name}_set');
} }
final _$albumSongsAtom = Atom(name: '_MusicDataStore.albumSongs');
@override
ObservableList<Song> get albumSongs {
_$albumSongsAtom.context.enforceReadPolicy(_$albumSongsAtom);
_$albumSongsAtom.reportObserved();
return super.albumSongs;
}
@override
set albumSongs(ObservableList<Song> value) {
_$albumSongsAtom.context.conditionallyRunInAction(() {
super.albumSongs = value;
_$albumSongsAtom.reportChanged();
}, _$albumSongsAtom, name: '${_$albumSongsAtom.name}_set');
}
final _$initAsyncAction = AsyncAction('init'); final _$initAsyncAction = AsyncAction('init');
@override @override
@ -107,10 +124,18 @@ mixin _$MusicDataStore on _MusicDataStore, Store {
return _$fetchSongsAsyncAction.run(() => super.fetchSongs()); return _$fetchSongsAsyncAction.run(() => super.fetchSongs());
} }
final _$fetchSongsFromAlbumAsyncAction = AsyncAction('fetchSongsFromAlbum');
@override
Future<void> fetchSongsFromAlbum(Album album) {
return _$fetchSongsFromAlbumAsyncAction
.run(() => super.fetchSongsFromAlbum(album));
}
@override @override
String toString() { String toString() {
final string = final string =
'albumsFuture: ${albumsFuture.toString()},songs: ${songs.toString()},isFetchingSongs: ${isFetchingSongs.toString()},isUpdatingDatabase: ${isUpdatingDatabase.toString()}'; 'albumsFuture: ${albumsFuture.toString()},songs: ${songs.toString()},isFetchingSongs: ${isFetchingSongs.toString()},isUpdatingDatabase: ${isUpdatingDatabase.toString()},albumSongs: ${albumSongs.toString()}';
return '{$string}'; return '{$string}';
} }
} }

View file

@ -13,9 +13,12 @@ ThemeData theme() => ThemeData(
accentColor: Colors.amberAccent, accentColor: Colors.amberAccent,
// https://api.flutter.dev/flutter/material/TextTheme-class.html // https://api.flutter.dev/flutter/material/TextTheme-class.html
textTheme: const TextTheme( textTheme: const TextTheme(
title: TextStyle(fontSize: 20.0), headline6: TextStyle(fontSize: 20.0),
), ),
tabBarTheme: TabBarTheme( tabBarTheme: TabBarTheme(
labelColor: Colors.white, labelColor: Colors.white,
), ),
iconTheme: IconThemeData(
color: Colors.white,
),
); );

View file

@ -37,7 +37,9 @@ class AudioManagerImpl implements AudioManager {
await _startAudioService(); 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.customAction(SET_QUEUE, queue);
// await AudioService.addQueueItem(queue[index]);
AudioService.playFromMediaId(queue[index].id); AudioService.playFromMediaId(queue[index].id);
} }

View file

@ -3,11 +3,14 @@ import 'dart:async';
import 'package:audio_service/audio_service.dart'; import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart'; import 'package:just_audio/just_audio.dart';
const String SET_QUEUE = 'SET_QUEUE';
class AudioPlayerTask extends BackgroundAudioTask { class AudioPlayerTask extends BackgroundAudioTask {
final _audioPlayer = AudioPlayer(); final _audioPlayer = AudioPlayer();
final _completer = Completer(); final _completer = Completer();
final _mediaItems = <String, MediaItem>{}; final _mediaItems = <String, MediaItem>{};
final _queue = <MediaItem>[];
Duration _position; Duration _position;
@ -68,6 +71,20 @@ class AudioPlayerTask extends BackgroundAudioTask {
); );
await _audioPlayer.pause(); await _audioPlayer.pause();
} }
@override
Future<void> onCustomAction(String name, arguments) async {
switch (name) {
case SET_QUEUE:
_setQueue(arguments as List<MediaItem>);
break;
default:
}
}
Future<void> _setQueue(List<MediaItem> queue) async {
}
} }
MediaControl playControl = const MediaControl( MediaControl playControl = const MediaControl(

View file

@ -69,6 +69,13 @@ class MoorMusicDataSource extends _$MoorMusicDataSource
.toList()); .toList());
} }
@override
Future<List<SongModel>> getSongsFromAlbum(AlbumModel album) {
return (select(songs)..where((tbl) => tbl.album.equals(album.title))).get().then((moorSongList) => moorSongList
.map((moorSong) => SongModel.fromMoorSong(moorSong))
.toList());
}
@override @override
Future<void> insertSong(SongModel songModel) async { Future<void> insertSong(SongModel songModel) async {
await into(songs).insert(songModel.toSongsCompanion()); await into(songs).insert(songModel.toSongsCompanion());

View file

@ -7,6 +7,7 @@ abstract class MusicDataSource {
Future<void> insertAlbum(AlbumModel albumModel); Future<void> insertAlbum(AlbumModel albumModel);
Future<List<SongModel>> getSongs(); Future<List<SongModel>> getSongs();
Future<List<SongModel>> getSongsFromAlbum(AlbumModel album);
Future<bool> songExists(SongModel songModel); Future<bool> songExists(SongModel songModel);
Future<void> insertSong(SongModel songModel); Future<void> insertSong(SongModel songModel);
} }

View file

@ -31,6 +31,12 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
(List<SongModel> songs) => Right<Failure, List<SongModel>>(songs)); (List<SongModel> songs) => Right<Failure, List<SongModel>>(songs));
} }
@override
Future<Either<Failure, List<Song>>> getSongsFromAlbum(Album album) async {
return musicDataSource.getSongsFromAlbum(album as AlbumModel).then(
(List<SongModel> songs) => Right<Failure, List<SongModel>>(songs));
}
// TODO: should remove albums that are not longer on the device // TODO: should remove albums that are not longer on the device
@override @override
Future<void> updateDatabase() async { Future<void> updateDatabase() async {