diff --git a/lib/domain/repositories/music_data_repository.dart b/lib/domain/repositories/music_data_repository.dart index 8e4b923..bb9fe7e 100644 --- a/lib/domain/repositories/music_data_repository.dart +++ b/lib/domain/repositories/music_data_repository.dart @@ -8,6 +8,7 @@ import '../entities/song.dart'; abstract class MusicDataRepository { Stream> get songStream; Stream> getAlbumSongStream(Album album); + Stream> getArtistAlbumStream(Artist artist); Future>> getSongs(); Future>> getSongsFromAlbum(Album album); diff --git a/lib/injection_container.dart b/lib/injection_container.dart index 3029885..450cce3 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -7,6 +7,7 @@ import 'package:just_audio/just_audio.dart' as ja; import 'domain/repositories/audio_repository.dart'; import 'domain/repositories/music_data_repository.dart'; import 'domain/repositories/persistent_player_state_repository.dart'; +import 'domain/repositories/settings_repository.dart'; import 'presentation/state/audio_store.dart'; import 'presentation/state/music_data_store.dart'; import 'presentation/state/navigation_store.dart'; @@ -21,9 +22,11 @@ import 'system/datasources/local_music_fetcher_contract.dart'; import 'system/datasources/moor_music_data_source.dart'; import 'system/datasources/music_data_source_contract.dart'; import 'system/datasources/player_state_data_source.dart'; +import 'system/datasources/settings_data_source.dart'; import 'system/repositories/audio_repository_impl.dart'; import 'system/repositories/music_data_repository_impl.dart'; import 'system/repositories/persistent_player_state_repository_impl.dart'; +import 'system/repositories/settings_repository_impl.dart'; final GetIt getIt = GetIt.instance; @@ -35,6 +38,7 @@ Future setupGetIt() async { () { final musicDataStore = MusicDataStore( musicDataRepository: getIt(), + settingsRepository: getIt(), ); musicDataStore.init(); return musicDataStore; @@ -71,15 +75,18 @@ Future setupGetIt() async { getIt.registerLazySingleton( () => PlayerStateRepositoryImpl(getIt()), ); + getIt.registerLazySingleton(() => SettingsRepositoryImpl(getIt())); // data sources final MoorMusicDataSource moorMusicDataSource = MoorMusicDataSource(); getIt.registerLazySingleton(() => moorMusicDataSource); getIt.registerLazySingleton(() => moorMusicDataSource.playerStateDao); + getIt.registerLazySingleton(() => moorMusicDataSource.settingsDao); getIt.registerLazySingleton( () => LocalMusicFetcherImpl( getIt(), getIt(), + getIt(), ), ); getIt.registerLazySingleton(() => AudioManagerImpl(getIt())); diff --git a/lib/presentation/pages/artist_details_page.dart b/lib/presentation/pages/artist_details_page.dart new file mode 100644 index 0000000..8ce1bb7 --- /dev/null +++ b/lib/presentation/pages/artist_details_page.dart @@ -0,0 +1,52 @@ +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/artist.dart'; +import '../state/music_data_store.dart'; +import '../widgets/album_art_list_tile.dart'; +import 'album_details_page.dart'; + +class ArtistDetailsPage extends StatelessWidget { + const ArtistDetailsPage({Key key, @required this.artist}) : super(key: key); + + final Artist artist; + + @override + Widget build(BuildContext context) { + final MusicDataStore musicDataStore = Provider.of(context); + + return Scaffold( + body: SafeArea( + child: Observer( + builder: (BuildContext context) => ListView.separated( + itemCount: musicDataStore.artistAlbumStream.value.length, + separatorBuilder: (BuildContext context, int index) => const Divider( + height: 4.0, + ), + itemBuilder: (_, int index) { + final Album album = musicDataStore.artistAlbumStream.value[index]; + return AlbumArtListTile( + title: album.title, + subtitle: album.pubYear.toString(), + albumArtPath: album.albumArtPath, + onTap: () { + musicDataStore.fetchSongsFromAlbum(album); + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => AlbumDetailsPage( + album: album, + ), + ), + ); + }, + ); + }, + ), + ), + ), + ); + } +} diff --git a/lib/presentation/pages/artists_page.dart b/lib/presentation/pages/artists_page.dart index f68e0a0..fa64ce5 100644 --- a/lib/presentation/pages/artists_page.dart +++ b/lib/presentation/pages/artists_page.dart @@ -4,6 +4,7 @@ import 'package:provider/provider.dart'; import '../../domain/entities/artist.dart'; import '../state/music_data_store.dart'; +import 'artist_details_page.dart'; class ArtistsPage extends StatefulWidget { const ArtistsPage({Key key}) : super(key: key); @@ -40,6 +41,17 @@ class _ArtistsPageState extends State final Artist artist = artists[index]; return ListTile( title: Text(artist.name), + onTap: () { + musicDataStore.fetchAlbumsFromArtist(artist); + Navigator.push( + context, + MaterialPageRoute( + builder: (BuildContext context) => ArtistDetailsPage( + artist: artist, + ), + ), + ); + }, ); }, separatorBuilder: (BuildContext context, int index) => const Divider( diff --git a/lib/presentation/state/music_data_store.dart b/lib/presentation/state/music_data_store.dart index 6681df8..a1f6a1d 100644 --- a/lib/presentation/state/music_data_store.dart +++ b/lib/presentation/state/music_data_store.dart @@ -32,6 +32,9 @@ abstract class _MusicDataStore with Store { @observable ObservableStream> albumSongStream; + @observable + ObservableStream> artistAlbumStream; + @observable ObservableList artists = [].asObservable(); @observable @@ -127,6 +130,11 @@ abstract class _MusicDataStore with Store { albumSongStream = _musicDataRepository.getAlbumSongStream(album).asObservable(initialValue: []); } + @action + Future fetchAlbumsFromArtist(Artist artist) async { + artistAlbumStream = _musicDataRepository.getArtistAlbumStream(artist).asObservable(initialValue: []); + } + Future setSongBlocked(Song song, bool blocked) async { await _musicDataRepository.setSongBlocked(song, blocked); } diff --git a/lib/presentation/state/music_data_store.g.dart b/lib/presentation/state/music_data_store.g.dart index 8e1aef4..6f93867 100644 --- a/lib/presentation/state/music_data_store.g.dart +++ b/lib/presentation/state/music_data_store.g.dart @@ -39,6 +39,22 @@ mixin _$MusicDataStore on _MusicDataStore, Store { }); } + final _$artistAlbumStreamAtom = + Atom(name: '_MusicDataStore.artistAlbumStream'); + + @override + ObservableStream> get artistAlbumStream { + _$artistAlbumStreamAtom.reportRead(); + return super.artistAlbumStream; + } + + @override + set artistAlbumStream(ObservableStream> value) { + _$artistAlbumStreamAtom.reportWrite(value, super.artistAlbumStream, () { + super.artistAlbumStream = value; + }); + } + final _$artistsAtom = Atom(name: '_MusicDataStore.artists'); @override @@ -199,11 +215,21 @@ mixin _$MusicDataStore on _MusicDataStore, Store { .run(() => super.fetchSongsFromAlbum(album)); } + final _$fetchAlbumsFromArtistAsyncAction = + AsyncAction('_MusicDataStore.fetchAlbumsFromArtist'); + + @override + Future fetchAlbumsFromArtist(Artist artist) { + return _$fetchAlbumsFromArtistAsyncAction + .run(() => super.fetchAlbumsFromArtist(artist)); + } + @override String toString() { return ''' songStream: ${songStream}, albumSongStream: ${albumSongStream}, +artistAlbumStream: ${artistAlbumStream}, artists: ${artists}, isFetchingArtists: ${isFetchingArtists}, albums: ${albums}, diff --git a/lib/system/datasources/local_music_fetcher.dart b/lib/system/datasources/local_music_fetcher.dart index 06f442c..e4b7f79 100644 --- a/lib/system/datasources/local_music_fetcher.dart +++ b/lib/system/datasources/local_music_fetcher.dart @@ -8,11 +8,13 @@ import '../models/album_model.dart'; import '../models/artist_model.dart'; import '../models/song_model.dart'; import 'local_music_fetcher_contract.dart'; +import 'settings_data_source.dart'; class LocalMusicFetcherImpl implements LocalMusicFetcher { - LocalMusicFetcherImpl(this._flutterAudioQuery, this._deviceInfo); + LocalMusicFetcherImpl(this._flutterAudioQuery, this._settingsDataSource, this._deviceInfo); final FlutterAudioQuery _flutterAudioQuery; + final SettingsDataSource _settingsDataSource; // CODESMELL: should probably encapsulate the deviceinfoplugin final DeviceInfoPlugin _deviceInfo; @@ -58,4 +60,50 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher { } return Uint8List(0); } + + @override + Future> getLocalMusic() async { + final musicDirectories = await _settingsDataSource.getLibraryFolders(); + + final songs = await _getFilteredSongs(musicDirectories); + final albumTitles = Set.from(songs.map((song) => song.album)); + final albums = await _getFilteredAlbums(albumTitles); + final artistNames = Set.from(albums.map((album) => album.artist)); + final artists = await _getFilteredArtists(artistNames); + + return { + 'SONGS': songs, + 'ALBUMS': albums, + 'ARTISTS': artists, + }; + } + + Future> _getFilteredSongs(Iterable musicDirectories) async { + final List songInfoList = await _flutterAudioQuery.getSongs(); + return songInfoList + .where( + (songInfo) => + songInfo.isMusic && + musicDirectories.any((element) => songInfo.filePath.startsWith(element)), + ) + .map((SongInfo songInfo) => SongModel.fromSongInfo(songInfo)) + .toList(); + } + + Future> _getFilteredAlbums(Iterable albumTitles) async { + final List albumInfoList = await _flutterAudioQuery.getAlbums(); + return albumInfoList + .where((albumInfo) => albumTitles.contains(albumInfo.title)) + .map((AlbumInfo albumInfo) => AlbumModel.fromAlbumInfo(albumInfo)) + .toList(); + } + + Future> _getFilteredArtists(Iterable artistNames) async { + final List artistInfoList = await _flutterAudioQuery.getArtists(); + return artistInfoList + .where((artistInfo) => artistNames.contains(artistInfo.name)) + .map((ArtistInfo artistInfo) => ArtistModel.fromArtistInfo(artistInfo)) + .toSet() + .toList(); + } } diff --git a/lib/system/datasources/local_music_fetcher_contract.dart b/lib/system/datasources/local_music_fetcher_contract.dart index dd1b186..6639fee 100644 --- a/lib/system/datasources/local_music_fetcher_contract.dart +++ b/lib/system/datasources/local_music_fetcher_contract.dart @@ -5,6 +5,8 @@ import '../models/artist_model.dart'; import '../models/song_model.dart'; abstract class LocalMusicFetcher { + Future> getLocalMusic(); + Future> getArtists(); Future> getAlbums(); Future> getSongs(); diff --git a/lib/system/datasources/moor_music_data_source.dart b/lib/system/datasources/moor_music_data_source.dart index fba4710..53e78b1 100644 --- a/lib/system/datasources/moor_music_data_source.dart +++ b/lib/system/datasources/moor_music_data_source.dart @@ -149,6 +149,18 @@ class MoorMusicDataSource extends _$MoorMusicDataSource implements MusicDataSour moorSongList.map((moorSong) => SongModel.fromMoorSong(moorSong)).toList()); } + @override + Stream> getArtistAlbumStream(ArtistModel artist) { + return (select(albums) + ..where((tbl) => tbl.artist.equals(artist.name)) + ..orderBy([ + (t) => OrderingTerm(expression: t.title), + ])) + .watch() + .map((moorAlbumList) => + moorAlbumList.map((moorAlbum) => AlbumModel.fromMoorAlbum(moorAlbum)).toList()); + } + @override Future> getSongsFromAlbum(AlbumModel album) { return (select(songs) diff --git a/lib/system/datasources/music_data_source_contract.dart b/lib/system/datasources/music_data_source_contract.dart index f4f07a8..f6861b0 100644 --- a/lib/system/datasources/music_data_source_contract.dart +++ b/lib/system/datasources/music_data_source_contract.dart @@ -7,6 +7,7 @@ abstract class MusicDataSource { Stream> get songStream; Stream> getAlbumSongStream(AlbumModel album); + Stream> getArtistAlbumStream(ArtistModel artist); /// Insert album into the database. Return the ID of the inserted album. Future insertAlbum(AlbumModel albumModel); diff --git a/lib/system/repositories/music_data_repository_impl.dart b/lib/system/repositories/music_data_repository_impl.dart index 28552e1..8f3b092 100644 --- a/lib/system/repositories/music_data_repository_impl.dart +++ b/lib/system/repositories/music_data_repository_impl.dart @@ -61,9 +61,11 @@ class MusicDataRepositoryImpl implements MusicDataRepository { Future updateDatabase() async { _log.info('updateDatabase called'); - await updateArtists(); - final albumIdMap = await updateAlbums(); - await updateSongs(albumIdMap); + final localMusic = await localMusicFetcher.getLocalMusic(); + + await updateArtists(localMusic['ARTISTS'] as List); + final albumIdMap = await updateAlbums(localMusic['ALBUMS'] as List); + await updateSongs(localMusic['SONGS'] as List, albumIdMap); _log.info('updateDatabase finished'); } @@ -73,18 +75,15 @@ class MusicDataRepositoryImpl implements MusicDataRepository { await musicDataSource.setSongBlocked(song as SongModel, blocked); } - Future updateArtists() async { + Future updateArtists(List artists) async { await musicDataSource.deleteAllArtists(); - final List artists = await localMusicFetcher.getArtists(); - for (final ArtistModel artist in artists) { await musicDataSource.insertArtist(artist); } } - Future> updateAlbums() async { + Future> updateAlbums(List albums) async { await musicDataSource.deleteAllAlbums(); - final List albums = await localMusicFetcher.getAlbums(); final Map albumIdMap = {}; final Directory dir = await getApplicationSupportDirectory(); @@ -111,9 +110,8 @@ class MusicDataRepositoryImpl implements MusicDataRepository { return albumIdMap; } - Future updateSongs(Map albumIdMap) async { + Future updateSongs(List songs, Map albumIdMap) async { final Directory dir = await getApplicationSupportDirectory(); - final List songs = await localMusicFetcher.getSongs(); final List songsToInsert = []; for (final SongModel song in songs) { @@ -133,4 +131,9 @@ class MusicDataRepositoryImpl implements MusicDataRepository { Future toggleNextSongLink(Song song) async { musicDataSource.toggleNextSongLink(song as SongModel); } + + @override + Stream> getArtistAlbumStream(Artist artist) { + return musicDataSource.getArtistAlbumStream(artist as ArtistModel); + } }