From 90a77385b53dd47e100eee4af90037d7776ba018 Mon Sep 17 00:00:00 2001 From: Moritz Weber Date: Thu, 21 Jan 2021 21:49:29 +0100 Subject: [PATCH] artist page, currently playing cosmetics, new playback functions --- lib/domain/repositories/audio_repository.dart | 5 + .../pages/artist_details_page.dart | 61 +++++- lib/presentation/pages/currently_playing.dart | 206 +++++++++--------- lib/presentation/pages/home_page.dart | 1 + lib/presentation/pages/queue_page.dart | 98 +++++---- lib/presentation/state/audio_store.dart | 10 + lib/presentation/state/music_data_store.dart | 8 +- lib/presentation/theming.dart | 4 +- .../widgets/album_background.dart | 32 ++- .../widgets/album_list_tile_extended.dart | 80 +++---- lib/presentation/widgets/artist_albums.dart | 30 +-- lib/presentation/widgets/artist_header.dart | 72 ++++++ lib/presentation/widgets/header.dart | 3 +- .../widgets/shuffle_all_button.dart | 8 +- lib/system/audio/audio_handler.dart | 29 ++- lib/system/audio/audio_manager.dart | 15 ++ lib/system/audio/audio_manager_contract.dart | 5 + lib/system/audio/stream_constants.dart | 4 +- .../datasources/local_music_fetcher.dart | 4 +- .../datasources/moor/music_data_dao.dart | 16 +- .../music_data_source_contract.dart | 1 + .../repositories/audio_repository_impl.dart | 14 ++ test/coverage_helper_test.dart | 4 +- 23 files changed, 466 insertions(+), 244 deletions(-) create mode 100644 lib/presentation/widgets/artist_header.dart diff --git a/lib/domain/repositories/audio_repository.dart b/lib/domain/repositories/audio_repository.dart index a3ef191..c631e45 100644 --- a/lib/domain/repositories/audio_repository.dart +++ b/lib/domain/repositories/audio_repository.dart @@ -1,3 +1,5 @@ +import '../entities/album.dart'; +import '../entities/artist.dart'; import '../entities/loop_mode.dart'; import '../entities/playback_state.dart'; import '../entities/shuffle_mode.dart'; @@ -15,6 +17,9 @@ abstract class AudioRepository { Future skipToPrevious(); Future setIndex(int index); + Future playAlbum(Album album); + Future playArtist(Artist artist, {ShuffleMode shuffleMode = ShuffleMode.none}); + Future setShuffleMode(ShuffleMode shuffleMode); Future setLoopMode(LoopMode loopMode); diff --git a/lib/presentation/pages/artist_details_page.dart b/lib/presentation/pages/artist_details_page.dart index f94fbd6..7f75bd8 100644 --- a/lib/presentation/pages/artist_details_page.dart +++ b/lib/presentation/pages/artist_details_page.dart @@ -4,8 +4,11 @@ import 'package:provider/provider.dart'; import '../../domain/entities/album.dart'; import '../../domain/entities/artist.dart'; +import '../state/audio_store.dart'; import '../state/music_data_store.dart'; +import '../theming.dart'; import '../widgets/artist_albums.dart'; +import '../widgets/artist_header.dart'; import 'album_details_page.dart'; class ArtistDetailsPage extends StatelessWidget { @@ -16,17 +19,57 @@ class ArtistDetailsPage extends StatelessWidget { @override Widget build(BuildContext context) { final MusicDataStore musicDataStore = Provider.of(context); + final AudioStore audioStore = Provider.of(context); return Observer( - builder: (BuildContext context) => Scaffold( - appBar: AppBar( - title: Text(artist.name), - ), - body: SafeArea( - child: ArtistAlbumList( - albums: musicDataStore.sortedArtistAlbums, - onTap: (Album album) => _tapAlbum(album, context, musicDataStore), - ), + builder: (BuildContext context) => SafeArea( + child: CustomScrollView( + slivers: [ + ArtistHeader(artist: artist), + SliverList( + delegate: SliverChildListDelegate( + [ + Padding( + padding: const EdgeInsets.only( + left: HORIZONTAL_PADDING, + right: HORIZONTAL_PADDING, + bottom: 8.0, + ), + child: ElevatedButton( + child: const Text('SHUFFLE'), + onPressed: () => audioStore.shuffleArtist(artist), + ), + ), + const Padding( + padding: EdgeInsets.only( + left: HORIZONTAL_PADDING + 2, + right: HORIZONTAL_PADDING + 2, + bottom: 4.0, + ), + child: Text( + 'Albums', + style: TEXT_HEADER, + ), + ), + Padding( + padding: const EdgeInsets.symmetric( + horizontal: HORIZONTAL_PADDING, + vertical: 4.0, + ), + child: Container( + height: 1.0, + color: Colors.white10, + ), + ), + ], + ), + ), + ArtistAlbumSliverList( + albums: musicDataStore.sortedArtistAlbums, + onTap: (Album album) => _tapAlbum(album, context, musicDataStore), + onTapPlay: (Album album) => audioStore.playAlbum(album), + ), + ], ), ), ); diff --git a/lib/presentation/pages/currently_playing.dart b/lib/presentation/pages/currently_playing.dart index 254587d..5637870 100644 --- a/lib/presentation/pages/currently_playing.dart +++ b/lib/presentation/pages/currently_playing.dart @@ -39,115 +39,119 @@ class CurrentlyPlayingPage extends StatelessWidget { _log.info('Observer.build'); final Song song = audioStore.currentSong; - return AlbumBackground( - song: song, - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0x55000000), - Color(0x22FFFFFF), - Color(0x22FFFFFF), - Color(0x88000000), - Color(0xBB000000), - ], - stops: [ - 0.0, - 0.1, - 0.5, - 0.65, - 1.0, - ], - ), - child: Padding( - padding: const EdgeInsets.only( - left: 12.0, - right: 12.0, - top: 8.0, + return Stack( + children: [ + AlbumBackground( + song: song, + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0x55000000), + Color(0x22FFFFFF), + Color(0x22FFFFFF), + Color(0x88000000), + Color(0xBB000000), + ], + stops: [ + 0.0, + 0.1, + 0.55, + 0.75, + 1.0, + ], + ), ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - IconButton( - icon: const Icon(Icons.expand_more), - onPressed: () { - Navigator.pop(context); - }, - ), - Expanded( - child: GestureDetector( - onTap: () => _openQueue(context), - child: Container( - color: Colors.transparent, - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - 'Next up'.toUpperCase(), - style: TEXT_SMALL_HEADLINE, - ), - NextSong( - queue: audioStore.queueStream.value, - index: audioStore.queueIndexStream.value, - ) - ], + Padding( + padding: const EdgeInsets.only( + left: 12.0, + right: 12.0, + top: 8.0, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + IconButton( + icon: const Icon(Icons.expand_more), + onPressed: () { + Navigator.pop(context); + }, + ), + Expanded( + child: GestureDetector( + onTap: () => _openQueue(context), + child: Container( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + 'Next up'.toUpperCase(), + style: TEXT_SMALL_HEADLINE, + ), + NextSong( + queue: audioStore.queueStream.value, + index: audioStore.queueIndexStream.value, + ) + ], + ), ), ), ), - ), - IconButton( - icon: const Icon(Icons.more_vert), - onPressed: () {}, - ) - ], - mainAxisAlignment: MainAxisAlignment.spaceBetween, - ), - const Spacer( - flex: 1, - ), - Expanded( - flex: 1000, - child: Center( - child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 8.0, - vertical: 0.0, - ), - child: AlbumArt( - song: song, + IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () {}, + ) + ], + mainAxisAlignment: MainAxisAlignment.spaceBetween, + ), + const Spacer( + flex: 1, + ), + Expanded( + flex: 1000, + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 0.0, + ), + child: AlbumArt( + song: song, + ), ), ), ), - ), - const Spacer( - flex: 60, - ), - const Padding( - padding: EdgeInsets.only(left: 2.0, right: 2.0), - child: SongCustomizationButtons(), - ), - const Spacer( - flex: 30, - ), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 2.0), - child: PlaybackControl(), - ), - const Spacer( - flex: 30, - ), - const Padding( - padding: EdgeInsets.only(left: 16.0, right: 16.0, top: 10.0), - child: TimeProgressIndicator(), - ), - const Spacer( - flex: 100, - ), - ], + const Spacer( + flex: 60, + ), + const Padding( + padding: EdgeInsets.only(left: 2.0, right: 2.0), + child: SongCustomizationButtons(), + ), + const Spacer( + flex: 30, + ), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 2.0), + child: PlaybackControl(), + ), + const Spacer( + flex: 30, + ), + const Padding( + padding: EdgeInsets.only(left: 16.0, right: 16.0, top: 10.0), + child: TimeProgressIndicator(), + ), + const Spacer( + flex: 100, + ), + ], + ), ), - ), + ], ); }, ), diff --git a/lib/presentation/pages/home_page.dart b/lib/presentation/pages/home_page.dart index d4822d4..823411e 100644 --- a/lib/presentation/pages/home_page.dart +++ b/lib/presentation/pages/home_page.dart @@ -14,6 +14,7 @@ class HomePage extends StatefulWidget { class _HomePageState extends State { @override Widget build(BuildContext context) { + print('HomePage.build'); return SafeArea( child: Padding( diff --git a/lib/presentation/pages/queue_page.dart b/lib/presentation/pages/queue_page.dart index 503961a..e5e80bf 100644 --- a/lib/presentation/pages/queue_page.dart +++ b/lib/presentation/pages/queue_page.dart @@ -34,53 +34,57 @@ class QueuePage extends StatelessWidget { switch (queueStream.status) { case StreamStatus.active: final int activeIndex = queueIndexStream.value; - return AlbumBackground( - song: audioStore.currentSong, - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - Color(0x55000000), - Color(0x55000000), - ], - stops: [ - 0.0, - 1.0, - ], - ), - child: CustomScrollView( - controller: _scrollController, - slivers: [ - ReorderableSliverList( - delegate: ReorderableSliverChildBuilderDelegate( - (context, int index) { - final song = queueStream.value[index]; - return Dismissible( - key: ValueKey(song.path), - child: AlbumArtListTile( - title: song.title, - subtitle: '${song.artist}', - albumArtPath: song.albumArtPath, - highlight: index == activeIndex, - onTap: () => audioStore.setIndex(index), - ), - onDismissed: (direction) { - audioStore.removeQueueIndex(index); - Scaffold.of(context).showSnackBar( - SnackBar( - content: Text('${song.title} removed'), - ), - ); - }, - ); - }, - childCount: queueStream.value.length, - ), - onReorder: (oldIndex, newIndex) => - audioStore.moveQueueItem(oldIndex, newIndex), - ) - ], - ), + return Stack( + children: [ + AlbumBackground( + song: audioStore.currentSong, + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0x55000000), + Color(0x55000000), + ], + stops: [ + 0.0, + 1.0, + ], + ), + ), + CustomScrollView( + controller: _scrollController, + slivers: [ + ReorderableSliverList( + delegate: ReorderableSliverChildBuilderDelegate( + (context, int index) { + final song = queueStream.value[index]; + return Dismissible( + key: ValueKey(song.path), + child: AlbumArtListTile( + title: song.title, + subtitle: '${song.artist}', + albumArtPath: song.albumArtPath, + highlight: index == activeIndex, + onTap: () => audioStore.setIndex(index), + ), + onDismissed: (direction) { + audioStore.removeQueueIndex(index); + Scaffold.of(context).showSnackBar( + SnackBar( + content: Text('${song.title} removed'), + ), + ); + }, + ); + }, + childCount: queueStream.value.length, + ), + onReorder: (oldIndex, newIndex) => + audioStore.moveQueueItem(oldIndex, newIndex), + ) + ], + ), + ], ); case StreamStatus.waiting: case StreamStatus.done: diff --git a/lib/presentation/state/audio_store.dart b/lib/presentation/state/audio_store.dart index c6f8078..48a664e 100644 --- a/lib/presentation/state/audio_store.dart +++ b/lib/presentation/state/audio_store.dart @@ -1,6 +1,8 @@ import 'package:meta/meta.dart'; import 'package:mobx/mobx.dart'; +import '../../domain/entities/album.dart'; +import '../../domain/entities/artist.dart'; import '../../domain/entities/loop_mode.dart'; import '../../domain/entities/playback_state.dart'; import '../../domain/entities/shuffle_mode.dart'; @@ -112,4 +114,12 @@ abstract class _AudioStore with Store { Future removeQueueIndex(int index) async { _audioRepository.removeQueueIndex(index); } + + Future playAlbum(Album album) async { + _audioRepository.playAlbum(album); + } + + Future shuffleArtist(Artist artist) async { + _audioRepository.playArtist(artist, shuffleMode: ShuffleMode.plus); + } } diff --git a/lib/presentation/state/music_data_store.dart b/lib/presentation/state/music_data_store.dart index 0c98f3c..26bd4e8 100644 --- a/lib/presentation/state/music_data_store.dart +++ b/lib/presentation/state/music_data_store.dart @@ -49,7 +49,13 @@ abstract class _MusicDataStore with Store { bool isUpdatingDatabase = false; @computed - List get sortedArtistAlbums => artistAlbumStream.value.toList()..sort((a, b) => -a.pubYear.compareTo(b.pubYear)); + List get sortedArtistAlbums => artistAlbumStream.value.toList()..sort((a, b) { + if (b.pubYear == null) + return -1; + if (a.pubYear == null) + return 1; + return -a.pubYear.compareTo(b.pubYear); + }); @action Future updateDatabase() async { diff --git a/lib/presentation/theming.dart b/lib/presentation/theming.dart index 800614f..84cf537 100644 --- a/lib/presentation/theming.dart +++ b/lib/presentation/theming.dart @@ -82,4 +82,6 @@ const TextStyle TEXT_SMALL_HEADLINE = TextStyle( const TextStyle TEXT_SMALL_SUBTITLE = TextStyle( fontSize: 12.0, fontWeight: FontWeight.w300, -); \ No newline at end of file +); + +const double HORIZONTAL_PADDING = 16.0; \ No newline at end of file diff --git a/lib/presentation/widgets/album_background.dart b/lib/presentation/widgets/album_background.dart index ca8f2ae..fa05b8d 100644 --- a/lib/presentation/widgets/album_background.dart +++ b/lib/presentation/widgets/album_background.dart @@ -14,25 +14,19 @@ class AlbumBackground extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - image: DecorationImage( - image: getAlbumImage(song.albumArtPath), - fit: BoxFit.cover, - ), - ), - child: Container( - height: double.infinity, - decoration: BoxDecoration( - gradient: gradient, - ), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 96.0, sigmaY: 96.0), - child: Container( - child: child, - color: Colors.white.withOpacity(0.0), + return ImageFiltered( + imageFilter: ImageFilter.blur(sigmaX: 96.0, sigmaY: 96.0), + child: ShaderMask( + shaderCallback: (Rect bounds) => gradient.createShader(bounds), + blendMode: BlendMode.srcATop, + child: Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + image: DecorationImage( + image: getAlbumImage(song.albumArtPath), + fit: BoxFit.cover, + ), ), ), ), diff --git a/lib/presentation/widgets/album_list_tile_extended.dart b/lib/presentation/widgets/album_list_tile_extended.dart index b9a030f..915182e 100644 --- a/lib/presentation/widgets/album_list_tile_extended.dart +++ b/lib/presentation/widgets/album_list_tile_extended.dart @@ -5,7 +5,7 @@ import '../utils.dart' as utils; class AlbumListTileExtended extends StatelessWidget { const AlbumListTileExtended( - {Key key, this.title, this.subtitle, this.albumArtPath, this.onTap, this.highlight = false}) + {Key key, this.title, this.subtitle, this.albumArtPath, this.onTap, this.highlight = false, this.onTapPlay}) : super(key: key); final String title; @@ -13,52 +13,56 @@ class AlbumListTileExtended extends StatelessWidget { final String albumArtPath; final Function onTap; final bool highlight; + final Function onTapPlay; @override Widget build(BuildContext context) { return GestureDetector( onTap: () => onTap(), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), - child: Row( - children: [ - SizedBox( - height: 72, - width: 72, - child: Container( - decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0)), - clipBehavior: Clip.antiAlias, - child: Image( - image: utils.getAlbumImage(albumArtPath), - fit: BoxFit.cover, + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0), + child: Row( + children: [ + SizedBox( + height: 72, + width: 72, + child: Container( + decoration: BoxDecoration(borderRadius: BorderRadius.circular(4.0)), + clipBehavior: Clip.antiAlias, + child: Image( + image: utils.getAlbumImage(albumArtPath), + fit: BoxFit.cover, + ), ), ), - ), - Expanded( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - title, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0), - maxLines: 2, - ), - Text( - subtitle, - style: TEXT_SMALL_SUBTITLE, - ), - ], + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16.0), + maxLines: 2, + ), + Text( + subtitle, + style: TEXT_SMALL_SUBTITLE, + ), + ], + ), ), ), - ), - IconButton( - icon: Icon(Icons.play_circle_fill_rounded), - iconSize: 40.0, - onPressed: () => null, - ), - ], + IconButton( + icon: Icon(Icons.play_circle_fill_rounded), + iconSize: 40.0, + onPressed: () => onTapPlay(), + ), + ], + ), ), ), ); diff --git a/lib/presentation/widgets/artist_albums.dart b/lib/presentation/widgets/artist_albums.dart index 8eb67c5..cc0cac9 100644 --- a/lib/presentation/widgets/artist_albums.dart +++ b/lib/presentation/widgets/artist_albums.dart @@ -3,25 +3,29 @@ import 'package:flutter/material.dart'; import '../../domain/entities/album.dart'; import 'album_list_tile_extended.dart'; -class ArtistAlbumList extends StatelessWidget { - const ArtistAlbumList({Key key, this.albums, this.onTap}) : super(key: key); +class ArtistAlbumSliverList extends StatelessWidget { + const ArtistAlbumSliverList({Key key, this.albums, this.onTap, this.onTapPlay}) : super(key: key); final List albums; final Function onTap; + final Function onTapPlay; @override Widget build(BuildContext context) { - return ListView.builder( - itemCount: albums.length, - itemBuilder: (_, int index) { - final Album album = albums[index]; - return AlbumListTileExtended( - title: album.title, - subtitle: album.pubYear.toString(), - albumArtPath: album.albumArtPath, - onTap: () => onTap(album), - ); - }, + return SliverList( + delegate: SliverChildBuilderDelegate( + (_, int index) { + final Album album = albums[index]; + return AlbumListTileExtended( + title: album.title, + subtitle: album.pubYear.toString(), + albumArtPath: album.albumArtPath, + onTap: () => onTap(album), + onTapPlay: () => onTapPlay(album), + ); + }, + childCount: albums.length, + ), ); } } diff --git a/lib/presentation/widgets/artist_header.dart b/lib/presentation/widgets/artist_header.dart new file mode 100644 index 0000000..57fbc77 --- /dev/null +++ b/lib/presentation/widgets/artist_header.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; + +import '../../domain/entities/artist.dart'; + +class ArtistHeader extends StatelessWidget { + const ArtistHeader({Key key, this.artist}) : super(key: key); + + final Artist artist; + + @override + Widget build(BuildContext context) { + const double height = 144.0; + return SliverAppBar( + brightness: Brightness.dark, + pinned: true, + expandedHeight: height, + backgroundColor: Theme.of(context).primaryColor, + iconTheme: const IconThemeData( + color: Colors.white, + ), + leading: IconButton( + icon: const Icon(Icons.chevron_left), + onPressed: () => Navigator.pop(context), + ), + actions: [ + IconButton( + icon: const Icon(Icons.more_vert), + onPressed: () => Navigator.pop(context), + ), + ], + titleSpacing: 48.0, + flexibleSpace: FlexibleSpaceBar( + centerTitle: true, + titlePadding: const EdgeInsets.only( + bottom: 0.0, + top: 0.0, + left: 48.0, + right: 48.0, + ), + title: Container( + alignment: Alignment.center, + height: height * 0.66, + // color: Colors.red, + child: Text( + artist.name, + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 24.0, + color: Colors.white, + ), + textAlign: TextAlign.center, + maxLines: 3, + ), + ), + background: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + // Colors.deepPurpleAccent, + Colors.transparent, + Theme.of(context).scaffoldBackgroundColor, + // Colors.green, + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/presentation/widgets/header.dart b/lib/presentation/widgets/header.dart index 3018673..4c5441c 100644 --- a/lib/presentation/widgets/header.dart +++ b/lib/presentation/widgets/header.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:mucke/presentation/theming.dart'; + +import '../theming.dart'; class Header extends StatelessWidget { const Header({Key key}) : super(key: key); diff --git a/lib/presentation/widgets/shuffle_all_button.dart b/lib/presentation/widgets/shuffle_all_button.dart index a6bf8e8..e0eba75 100644 --- a/lib/presentation/widgets/shuffle_all_button.dart +++ b/lib/presentation/widgets/shuffle_all_button.dart @@ -20,14 +20,12 @@ class ShuffleAllButton extends StatelessWidget { vertical: verticalPad, horizontal: horizontalPad, ), - child: RaisedButton.icon( + child: ElevatedButton.icon( icon: const Icon(Icons.shuffle), label: const Text('SHUFFLE ALL'), onPressed: () => audioStore.shuffleAll(), - color: Theme.of(context).accentColor, - highlightColor: Theme.of(context).highlightColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(24.0), + style: ElevatedButton.styleFrom( + primary: Theme.of(context).accentColor, ), ), ), diff --git a/lib/system/audio/audio_handler.dart b/lib/system/audio/audio_handler.dart index 90010f1..aab1fdb 100644 --- a/lib/system/audio/audio_handler.dart +++ b/lib/system/audio/audio_handler.dart @@ -8,6 +8,8 @@ import '../../domain/entities/playback_event.dart'; import '../../domain/entities/shuffle_mode.dart'; import '../datasources/music_data_source_contract.dart'; import '../datasources/player_state_data_source.dart'; +import '../models/album_model.dart'; +import '../models/artist_model.dart'; import '../models/playback_event_model.dart'; import '../models/queue_item_model.dart'; import '../models/song_model.dart'; @@ -123,6 +125,13 @@ class MyAudioHandler extends BaseAudioHandler { return moveQueueItem(arguments['OLD_INDEX'] as int, arguments['NEW_INDEX'] as int); case SET_INDEX: return setIndex(arguments['INDEX'] as int); + case PLAY_ALBUM: + return playAlbum(arguments['ALBUM'] as AlbumModel); + case PLAY_ARTIST: + return playArtist( + arguments['ARTIST'] as ArtistModel, + arguments['SHUFFLE_MODE'] as ShuffleMode, + ); default: } } @@ -163,6 +172,23 @@ class MyAudioHandler extends BaseAudioHandler { _audioPlayer.setIndex(index); } + Future playAlbum(AlbumModel album) async { + _audioPlayer.setShuffleMode(ShuffleMode.none, false); + final List songs = await _musicDataSource.getAlbumSongStream(album).first; + + _audioPlayer.playSongList(songs, 0); + } + + Future playArtist(ArtistModel artist, ShuffleMode shuffleMode) async { + _audioPlayer.setShuffleMode(shuffleMode, false); + final List songs = await _musicDataSource.getArtistSongStream(artist).first; + + final rng = Random(); + final index = rng.nextInt(songs.length); + + _audioPlayer.playSongList(songs, index); + } + void _handleSetQueue(List queueItems) { _playerStateDataSource.setQueue(queueItems); @@ -195,8 +221,7 @@ class MyAudioHandler extends BaseAudioHandler { } void _handlePosition(Duration position, SongModel song) { - if (song == null || position == null) - return; + if (song == null || position == null) return; final int pos = position.inMilliseconds; diff --git a/lib/system/audio/audio_manager.dart b/lib/system/audio/audio_manager.dart index 4646a57..7d76ca4 100644 --- a/lib/system/audio/audio_manager.dart +++ b/lib/system/audio/audio_manager.dart @@ -5,6 +5,8 @@ import 'package:audio_service/audio_service.dart'; import '../../domain/entities/loop_mode.dart'; import '../../domain/entities/playback_state.dart' as entity; import '../../domain/entities/shuffle_mode.dart'; +import '../models/album_model.dart'; +import '../models/artist_model.dart'; import '../models/playback_state_model.dart'; import '../models/song_model.dart'; import 'audio_manager_contract.dart'; @@ -134,4 +136,17 @@ class AudioManagerImpl implements AudioManager { Future removeQueueIndex(int index) async { await _audioHandler.removeQueueItemAt(index); } + + @override + Future playAlbum(AlbumModel albumModel) async { + await _audioHandler.customAction(PLAY_ALBUM, {'ALBUM': albumModel}); + } + + @override + Future playArtist(ArtistModel artistModel, ShuffleMode shuffleMode) async { + await _audioHandler.customAction(PLAY_ARTIST, { + 'ARTIST': artistModel, + 'SHUFFLE_MODE': shuffleMode, + }); + } } diff --git a/lib/system/audio/audio_manager_contract.dart b/lib/system/audio/audio_manager_contract.dart index 2dec28e..929740f 100644 --- a/lib/system/audio/audio_manager_contract.dart +++ b/lib/system/audio/audio_manager_contract.dart @@ -1,6 +1,8 @@ import '../../domain/entities/loop_mode.dart'; import '../../domain/entities/playback_state.dart'; import '../../domain/entities/shuffle_mode.dart'; +import '../models/album_model.dart'; +import '../models/artist_model.dart'; import '../models/song_model.dart'; abstract class AudioManager { @@ -22,4 +24,7 @@ abstract class AudioManager { Future addToQueue(SongModel songModel); Future moveQueueItem(int oldIndex, int newIndex); Future removeQueueIndex(int index); + + Future playAlbum(AlbumModel albumModel); + Future playArtist(ArtistModel artistModel, ShuffleMode shuffleMode); } diff --git a/lib/system/audio/stream_constants.dart b/lib/system/audio/stream_constants.dart index f88da8d..2a6013c 100644 --- a/lib/system/audio/stream_constants.dart +++ b/lib/system/audio/stream_constants.dart @@ -3,4 +3,6 @@ const String SHUFFLE_ALL = 'SHUFFLE_ALL'; const String SET_SHUFFLE_MODE = 'SET_SHUFFLE_MODE'; const String SET_LOOP_MODE = 'SET_LOOP_MODE'; const String SET_INDEX = 'SET_INDEX'; -const String MOVE_QUEUE_ITEM = 'MOVE_QUEUE_ITEM'; \ No newline at end of file +const String MOVE_QUEUE_ITEM = 'MOVE_QUEUE_ITEM'; +const String PLAY_ALBUM = 'PLAY_ALBUM'; +const String PLAY_ARTIST = 'PLAY_ARTIST'; diff --git a/lib/system/datasources/local_music_fetcher.dart b/lib/system/datasources/local_music_fetcher.dart index e4b7f79..c14109a 100644 --- a/lib/system/datasources/local_music_fetcher.dart +++ b/lib/system/datasources/local_music_fetcher.dart @@ -55,7 +55,7 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher { return _flutterAudioQuery.getArtwork( type: ResourceType.ALBUM, id: id.toString(), - size: const Size(500.0, 500.0), + size: const Size(480.0, 480.0), ); } return Uint8List(0); @@ -71,6 +71,8 @@ class LocalMusicFetcherImpl implements LocalMusicFetcher { final artistNames = Set.from(albums.map((album) => album.artist)); final artists = await _getFilteredArtists(artistNames); + assert(false); + return { 'SONGS': songs, 'ALBUMS': albums, diff --git a/lib/system/datasources/moor/music_data_dao.dart b/lib/system/datasources/moor/music_data_dao.dart index 63b8e8a..1f26b1b 100644 --- a/lib/system/datasources/moor/music_data_dao.dart +++ b/lib/system/datasources/moor/music_data_dao.dart @@ -71,8 +71,20 @@ class MusicDataDao extends DatabaseAccessor (t) => OrderingTerm(expression: t.title), ])) .watch() - .map((moorAlbumList) => - moorAlbumList.map((moorAlbum) => AlbumModel.fromMoor(moorAlbum)).toList()); + .map((moorAlbumList) { + return moorAlbumList.map((moorAlbum) => AlbumModel.fromMoor(moorAlbum)).toList(); + }); + } + + @override + Stream> getArtistSongStream(ArtistModel artist) { + return (select(albums)..where((tbl) => tbl.artist.equals(artist.name))) + .join([innerJoin(songs, songs.albumId.equalsExp(albums.id))]) + .map((row) => row.readTable(songs)) + .watch() + .map( + (moorSongList) => moorSongList.map((moorSong) => SongModel.fromMoor(moorSong)).toList(), + ); } @override diff --git a/lib/system/datasources/music_data_source_contract.dart b/lib/system/datasources/music_data_source_contract.dart index 858feb3..af63784 100644 --- a/lib/system/datasources/music_data_source_contract.dart +++ b/lib/system/datasources/music_data_source_contract.dart @@ -5,6 +5,7 @@ import '../models/song_model.dart'; abstract class MusicDataSource { Stream> get songStream; Stream> getAlbumSongStream(AlbumModel album); + Stream> getArtistSongStream(ArtistModel artist); Future> getSongs(); Future getSongByPath(String path); diff --git a/lib/system/repositories/audio_repository_impl.dart b/lib/system/repositories/audio_repository_impl.dart index 4393a7e..b178fef 100644 --- a/lib/system/repositories/audio_repository_impl.dart +++ b/lib/system/repositories/audio_repository_impl.dart @@ -1,9 +1,13 @@ +import '../../domain/entities/album.dart'; +import '../../domain/entities/artist.dart'; import '../../domain/entities/loop_mode.dart'; import '../../domain/entities/playback_state.dart'; import '../../domain/entities/shuffle_mode.dart'; import '../../domain/entities/song.dart'; import '../../domain/repositories/audio_repository.dart'; import '../audio/audio_manager_contract.dart'; +import '../models/album_model.dart'; +import '../models/artist_model.dart'; import '../models/song_model.dart'; class AudioRepositoryImpl implements AudioRepository { @@ -83,4 +87,14 @@ class AudioRepositoryImpl implements AudioRepository { Future setIndex(int index) async { await _audioManager.setIndex(index); } + + @override + Future playAlbum(Album album) async { + await _audioManager.playAlbum(album as AlbumModel); + } + + @override + Future playArtist(Artist artist, {ShuffleMode shuffleMode = ShuffleMode.none}) async { + await _audioManager.playArtist(artist as ArtistModel, shuffleMode); + } } diff --git a/test/coverage_helper_test.dart b/test/coverage_helper_test.dart index ebff92c..8a4b17e 100644 --- a/test/coverage_helper_test.dart +++ b/test/coverage_helper_test.dart @@ -22,7 +22,7 @@ import 'package:mucke/presentation/widgets/next_indicator.dart'; import 'package:mucke/presentation/widgets/previous_button.dart'; import 'package:mucke/presentation/widgets/play_pause_button.dart'; import 'package:mucke/presentation/widgets/album_art.dart'; -import 'package:mucke/presentation/widgets/shuffle_all_button.dart'; +import 'package:mucke/presentation/widgets/primary_button.dart'; import 'package:mucke/presentation/widgets/shuffle_button.dart'; import 'package:mucke/presentation/widgets/injection_widget.dart'; import 'package:mucke/presentation/widgets/next_song.dart'; @@ -45,8 +45,6 @@ import 'package:mucke/presentation/pages/albums_page.dart'; import 'package:mucke/presentation/pages/album_details_page.dart'; import 'package:mucke/presentation/pages/currently_playing.dart'; import 'package:mucke/presentation/pages/artists_page.dart'; -import 'package:mucke/core/usecase.dart'; -import 'package:mucke/core/error/failures.dart'; import 'package:mucke/domain/repositories/audio_repository.dart'; import 'package:mucke/domain/repositories/music_data_repository.dart'; import 'package:mucke/domain/entities/shuffle_mode.dart';