diff --git a/lib/domain/repositories/music_data_repository.dart b/lib/domain/repositories/music_data_repository.dart index 9dfdf20..0078e52 100644 --- a/lib/domain/repositories/music_data_repository.dart +++ b/lib/domain/repositories/music_data_repository.dart @@ -35,6 +35,8 @@ abstract class MusicDataInfoRepository { Future> searchArtists(String searchText, {int? limit}); Future> searchAlbums(String searchText, {int? limit}); Future> searchSongs(String searchText, {int? limit}); + Future> searchSmartLists(String searchText, {int? limit}); + Future> searchPlaylists(String searchText, {int? limit}); } abstract class MusicDataRepository extends MusicDataInfoRepository { diff --git a/lib/domain/usecases/play_playlist.dart b/lib/domain/usecases/play_playlist.dart new file mode 100644 index 0000000..9c038bc --- /dev/null +++ b/lib/domain/usecases/play_playlist.dart @@ -0,0 +1,26 @@ +import 'dart:math'; + +import '../entities/playlist.dart'; +import '../entities/shuffle_mode.dart'; +import '../repositories/audio_player_repository.dart'; +import 'play_songs.dart'; + +class PlayPlaylist { + PlayPlaylist( + this._audioPlayerRepository, + this._playSongs, + ); + + final PlaySongs _playSongs; + + final AudioPlayerRepository _audioPlayerRepository; + + Future call(Playlist playlist) async { + final songs = playlist.songs; + final shuffleMode = await _audioPlayerRepository.shuffleModeStream.first; + final rng = Random(); + final initialIndex = shuffleMode == ShuffleMode.none ? 0 : rng.nextInt(songs.length); + + _playSongs(songs: songs, initialIndex: initialIndex, playable: playlist, keepInitialIndex: false); + } +} diff --git a/lib/injection_container.dart b/lib/injection_container.dart index 93294a8..f58c703 100644 --- a/lib/injection_container.dart +++ b/lib/injection_container.dart @@ -18,6 +18,7 @@ import 'domain/repositories/platform_integration_repository.dart'; import 'domain/repositories/settings_repository.dart'; import 'domain/usecases/play_album.dart'; import 'domain/usecases/play_artist.dart'; +import 'domain/usecases/play_playlist.dart'; import 'domain/usecases/play_smart_list.dart'; import 'domain/usecases/play_songs.dart'; import 'domain/usecases/seek_to_next.dart'; @@ -68,6 +69,7 @@ Future setupGetIt() async { playAlbum: getIt(), playArtist: getIt(), playSmartList: getIt(), + playPlayist: getIt(), playSongs: getIt(), seekToNext: getIt(), shuffleAll: getIt(), @@ -129,6 +131,12 @@ Future setupGetIt() async { getIt(), ), ); + getIt.registerLazySingleton( + () => PlayPlaylist( + getIt(), + getIt(), + ), + ); getIt.registerLazySingleton( () => PlaySongs( getIt(), diff --git a/lib/presentation/pages/album_details_page.dart b/lib/presentation/pages/album_details_page.dart index 668cc9f..8f14c96 100644 --- a/lib/presentation/pages/album_details_page.dart +++ b/lib/presentation/pages/album_details_page.dart @@ -1,7 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:get_it/get_it.dart'; -import '../widgets/bottom_sheet/add_to_playlist.dart'; import '../../domain/entities/album.dart'; import '../../domain/entities/song.dart'; @@ -10,6 +9,7 @@ import '../state/audio_store.dart'; import '../state/music_data_store.dart'; import '../theming.dart'; import '../widgets/album_sliver_appbar.dart'; +import '../widgets/bottom_sheet/add_to_playlist.dart'; import '../widgets/custom_modal_bottom_sheet.dart'; import '../widgets/exclude_level_options.dart'; import '../widgets/like_count_options.dart'; diff --git a/lib/presentation/pages/playlists_page.dart b/lib/presentation/pages/playlists_page.dart index b1fe9ce..30b4c49 100644 --- a/lib/presentation/pages/playlists_page.dart +++ b/lib/presentation/pages/playlists_page.dart @@ -71,7 +71,7 @@ class _PlaylistsPageState extends State with AutomaticKeepAliveCl ), trailing: IconButton( icon: const Icon(Icons.play_circle_fill_rounded, size: 32.0), - onPressed: () => {}, + onPressed: () => audioStore.playPlaylist(playlist), ), ); } diff --git a/lib/presentation/pages/search_page.dart b/lib/presentation/pages/search_page.dart index cc5c38c..4e282bc 100644 --- a/lib/presentation/pages/search_page.dart +++ b/lib/presentation/pages/search_page.dart @@ -13,6 +13,8 @@ import '../widgets/song_bottom_sheet.dart'; import '../widgets/song_list_tile.dart'; import 'album_details_page.dart'; import 'artist_details_page.dart'; +import 'playlist_page.dart'; +import 'smart_list_page.dart'; class SearchPage extends StatefulWidget { const SearchPage({Key? key}) : super(key: key); @@ -62,57 +64,151 @@ class _SearchPageState extends State { return SafeArea( child: Column( children: [ - Padding( - padding: const EdgeInsets.only( - top: 8.0, - left: HORIZONTAL_PADDING - 8.0, - right: HORIZONTAL_PADDING - 8.0, - bottom: 8.0, + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + DARK1, + // Colors.transparent, + Theme.of(context).scaffoldBackgroundColor, + ], + ), ), - child: StatefulBuilder(builder: (context, setState) { - return TextField( - controller: _textController, - decoration: InputDecoration( - hintText: 'Search', - hintStyle: TEXT_HEADER.copyWith(color: Colors.white), - fillColor: Colors.white10, - filled: true, - enabledBorder: - const OutlineInputBorder(borderSide: BorderSide.none, gapPadding: 0.0), - focusedBorder: - const OutlineInputBorder(borderSide: BorderSide.none, gapPadding: 0.0), - contentPadding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), - suffixIcon: searchText.isNotEmpty - ? IconButton( - icon: const Icon(Icons.clear_rounded), - color: Colors.white, - onPressed: () { - setState(() { - searchText = ''; - _textController.text = ''; - }); - searchStore.reset(); - }, - ) - : const SizedBox.shrink(), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + top: 8.0, + left: HORIZONTAL_PADDING - 8.0, + right: HORIZONTAL_PADDING - 8.0, + bottom: 8.0, + ), + child: StatefulBuilder(builder: (context, setState) { + return TextField( + controller: _textController, + decoration: InputDecoration( + hintText: 'Search', + hintStyle: TEXT_HEADER.copyWith(color: Colors.white), + fillColor: Colors.white10, + filled: true, + enabledBorder: + const OutlineInputBorder(borderSide: BorderSide.none, gapPadding: 0.0), + focusedBorder: + const OutlineInputBorder(borderSide: BorderSide.none, gapPadding: 0.0), + contentPadding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), + suffixIcon: searchText.isNotEmpty + ? IconButton( + icon: const Icon(Icons.clear_rounded), + color: Colors.white, + onPressed: () { + setState(() { + searchText = ''; + _textController.text = ''; + }); + searchStore.reset(); + }, + ) + : const SizedBox.shrink(), + ), + onChanged: (text) { + setState(() => searchText = text); + searchStore.search(text); + }, + focusNode: _searchFocus, + ); + }), ), - onChanged: (text) { - setState(() => searchText = text); - searchStore.search(text); - }, - focusNode: _searchFocus, - ); - }), + Padding( + padding: const EdgeInsets.only( + left: HORIZONTAL_PADDING - 8.0, + right: HORIZONTAL_PADDING - 8.0, + bottom: 16.0, + ), + child: Observer(builder: (context) { + final artists = searchStore.searchResultsArtists; + final albums = searchStore.searchResultsAlbums; + final songs = searchStore.searchResultsSongs; + final smartlists = searchStore.searchResultsSmartLists; + final playlists = searchStore.searchResultsPlaylists; + + final artistHeight = + artists.length * 56.0 + artists.isNotEmpty.toDouble() * (16.0 + 56.0); + final albumsHeight = + albums.length * 72.0 + albums.isNotEmpty.toDouble() * (16.0 + 56.0); + final songsHeight = + songs.length * 56.0 + songs.isNotEmpty.toDouble() * (16.0 + 56.0); + final smartListsHeight = + smartlists.length * 56.0 + smartlists.isNotEmpty.toDouble() * (16.0 + 56.0); + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + onPressed: artists.isEmpty + ? null + : () => _scrollController.animateTo( + 0, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + icon: const Icon(Icons.person_rounded), + ), + IconButton( + onPressed: albums.isEmpty + ? null + : () => _scrollController.animateTo( + artistHeight, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + icon: const Icon(Icons.album_rounded), + ), + IconButton( + onPressed: songs.isEmpty + ? null + : () => _scrollController.animateTo( + artistHeight + albumsHeight, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + icon: const Icon(Icons.audiotrack_rounded), + ), + IconButton( + onPressed: smartlists.isEmpty + ? null + : () => _scrollController.animateTo( + artistHeight + albumsHeight + songsHeight, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + icon: const Icon(Icons.auto_awesome_rounded), + ), + IconButton( + onPressed: playlists.isEmpty + ? null + : () => _scrollController.animateTo( + artistHeight + albumsHeight + songsHeight + smartListsHeight, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ), + icon: const Icon(Icons.queue_music_rounded), + ), + ], + ); + }), + ), + ], + ), ), Expanded( child: Observer(builder: (context) { final artists = searchStore.searchResultsArtists; final albums = searchStore.searchResultsAlbums; final songs = searchStore.searchResultsSongs; - - final viewArtists = searchStore.viewArtists; - final viewAlbums = searchStore.viewAlbums; - final viewSongs = searchStore.viewSongs; + final smartlists = searchStore.searchResultsSmartLists; + final playlists = searchStore.searchResultsPlaylists; return Scrollbar( controller: _scrollController, @@ -120,205 +216,192 @@ class _SearchPageState extends State { controller: _scrollController, slivers: [ if (artists.isNotEmpty) ...[ - SliverAppBar( - title: GestureDetector( - onTap: () => _scrollController.animateTo( - 0, - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - ), - child: Container( - width: double.infinity, - color: Colors.transparent, - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 10.0), - child: Text('Artists', style: TEXT_HEADER), - ), - ), - ), - actions: [ - IconButton( - onPressed: () => searchStore.toggleViewArtists(), - icon: Icon(viewArtists - ? Icons.expand_less_rounded - : Icons.expand_more_rounded), - ), - ], - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - elevation: 0.0, - pinned: true, - ), SliverList( delegate: SliverChildListDelegate( [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (child, animation) => SizeTransition( - sizeFactor: animation, - child: child, + ListTile( + title: Text( + 'Artists', + style: TEXT_HEADER.underlined( + textColor: Colors.white, + underlineColor: LIGHT1, + thickness: 4, + distance: 8, + ), ), - child: viewArtists - ? Column( - children: [ - for (final artist in artists) - ListTile( - title: Text(artist.name), - leading: const SizedBox( - child: Icon(Icons.person_rounded), - width: 56.0, - height: 56.0, - ), - onTap: () { - _navStore.pushOnLibrary( - MaterialPageRoute( - builder: (BuildContext context) => - ArtistDetailsPage( - artist: artist, - ), - ), - ); - }, - ) - ], - ) - : const SizedBox.shrink(), ), + for (final artist in artists) + ListTile( + title: Text(artist.name), + leading: const SizedBox( + child: Icon(Icons.person_rounded), + width: 56.0, + height: 56.0, + ), + onTap: () { + _navStore.pushOnLibrary( + MaterialPageRoute( + builder: (BuildContext context) => ArtistDetailsPage( + artist: artist, + ), + ), + ); + }, + ), + const SizedBox(height: 16.0), ], ), ), ], if (albums.isNotEmpty) ...[ - SliverAppBar( - title: GestureDetector( - onTap: () { - _scrollController.animateTo( - artists.length * 56.0 * viewArtists.toDouble(), - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - ); - }, - child: Container( - width: double.infinity, - color: Colors.transparent, - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 10.0), - child: Text('Albums', style: TEXT_HEADER), - ), - ), - ), - actions: [ - IconButton( - onPressed: () => searchStore.toggleViewAlbums(), - icon: Icon( - viewAlbums ? Icons.expand_less_rounded : Icons.expand_more_rounded, - ), - ), - ], - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - elevation: 0.0, - pinned: true, - ), SliverList( delegate: SliverChildListDelegate( [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (child, animation) => SizeTransition( - sizeFactor: animation, - child: child, + ListTile( + title: Text( + 'Albums', + style: TEXT_HEADER.underlined( + textColor: Colors.white, + underlineColor: LIGHT1, + thickness: 4, + distance: 8, + ), ), - child: viewAlbums - ? Column(children: [ - for (final album in albums) - AlbumArtListTile( - title: album.title, - subtitle: album.artist, - albumArtPath: album.albumArtPath, - onTap: () { - _navStore.pushOnLibrary( - MaterialPageRoute( - builder: (BuildContext context) => AlbumDetailsPage( - album: album, - ), - ), - ); - }, - ), - ]) - : const SizedBox.shrink(), ), + for (final album in albums) + AlbumArtListTile( + title: album.title, + subtitle: album.artist, + albumArtPath: album.albumArtPath, + onTap: () { + _navStore.pushOnLibrary( + MaterialPageRoute( + builder: (BuildContext context) => AlbumDetailsPage( + album: album, + ), + ), + ); + }, + ), + const SizedBox(height: 16.0), ], ), ), ], if (songs.isNotEmpty) ...[ - SliverAppBar( - title: GestureDetector( - onTap: () { - _scrollController.animateTo( - artists.length * 56.0 * viewArtists.toDouble() + - albums.length * 72.0 * viewAlbums.toDouble(), - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - ); - }, - child: Container( - width: double.infinity, - color: Colors.transparent, - child: const Padding( - padding: EdgeInsets.symmetric(vertical: 10.0), - child: Text('Songs', style: TEXT_HEADER), - ), - ), - ), - actions: [ - IconButton( - onPressed: () => searchStore.toggleViewSongs(), - icon: Icon( - viewSongs ? Icons.expand_less_rounded : Icons.expand_more_rounded, - ), - ), - ], - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - elevation: 0.0, - pinned: true, - ), SliverList( delegate: SliverChildListDelegate( [ - AnimatedSwitcher( - duration: const Duration(milliseconds: 200), - transitionBuilder: (child, animation) => SizeTransition( - sizeFactor: animation, - child: child, + ListTile( + title: Text( + 'Songs', + style: TEXT_HEADER.underlined( + textColor: Colors.white, + underlineColor: LIGHT1, + thickness: 4, + distance: 8, + ), ), - child: viewSongs - ? Column(children: [ - for (final song in songs) - SongListTile( - song: song, - onTap: () => audioStore.playSong( - 0, - [song], - SearchQuery(searchText), - ), - onTapMore: () => showModalBottomSheet( - context: context, - useRootNavigator: true, - isScrollControlled: true, - backgroundColor: Colors.transparent, - builder: (context) => SongBottomSheet( - song: song, - ), - ), - ), - ]) - : const SizedBox.shrink(), ), + for (int i in songs.asMap().keys) + SongListTile( + song: songs[i], + onTap: () => audioStore.playSong( + i, + songs, + SearchQuery(searchText), + ), + onTapMore: () => showModalBottomSheet( + context: context, + useRootNavigator: true, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (context) => SongBottomSheet( + song: songs[i], + ), + ), + ), + const SizedBox(height: 16.0), ], ), ), ], + if (smartlists.isNotEmpty) + SliverList( + delegate: SliverChildListDelegate( + [ + ListTile( + title: Text( + 'Smartlists', + style: TEXT_HEADER.underlined( + textColor: Colors.white, + underlineColor: LIGHT1, + thickness: 4, + distance: 8, + ), + ), + ), + for (int i in smartlists.asMap().keys) + ListTile( + title: Text(smartlists[i].name), + leading: const SizedBox( + child: Icon(Icons.auto_awesome_rounded), + width: 56.0, + height: 56.0, + ), + onTap: () => _navStore.pushOnLibrary( + MaterialPageRoute( + builder: (BuildContext context) => + SmartListPage(smartList: smartlists[i]), + ), + ), + trailing: IconButton( + icon: const Icon(Icons.play_circle_fill_rounded, size: 32.0), + onPressed: () => audioStore.playSmartList(smartlists[i]), + ), + ), + const SizedBox(height: 16.0), + ], + ), + ), + if (playlists.isNotEmpty) + SliverList( + delegate: SliverChildListDelegate( + [ + ListTile( + title: Text( + 'Playlists', + style: TEXT_HEADER.underlined( + textColor: Colors.white, + underlineColor: LIGHT1, + thickness: 4, + distance: 8, + ), + ), + ), + for (int i in playlists.asMap().keys) + ListTile( + title: Text(playlists[i].name), + leading: const SizedBox( + child: Icon(Icons.queue_music_rounded), + width: 56.0, + height: 56.0, + ), + onTap: () => _navStore.pushOnLibrary( + MaterialPageRoute( + builder: (BuildContext context) => + PlaylistPage(playlist: playlists[i]), + ), + ), + trailing: IconButton( + icon: const Icon(Icons.play_circle_fill_rounded, size: 32.0), + onPressed: () => audioStore.playPlaylist(playlists[i]), + ), + ), + const SizedBox(height: 16.0), + ], + ), + ), ], ), ); diff --git a/lib/presentation/state/audio_store.dart b/lib/presentation/state/audio_store.dart index 059eebf..2649474 100644 --- a/lib/presentation/state/audio_store.dart +++ b/lib/presentation/state/audio_store.dart @@ -4,12 +4,14 @@ import '../../domain/entities/album.dart'; import '../../domain/entities/artist.dart'; import '../../domain/entities/loop_mode.dart'; import '../../domain/entities/playable.dart'; +import '../../domain/entities/playlist.dart'; import '../../domain/entities/shuffle_mode.dart'; import '../../domain/entities/smart_list.dart'; import '../../domain/entities/song.dart'; import '../../domain/repositories/audio_player_repository.dart'; import '../../domain/usecases/play_album.dart'; import '../../domain/usecases/play_artist.dart'; +import '../../domain/usecases/play_playlist.dart'; import '../../domain/usecases/play_smart_list.dart'; import '../../domain/usecases/play_songs.dart'; import '../../domain/usecases/seek_to_next.dart'; @@ -23,6 +25,7 @@ class AudioStore extends _AudioStore with _$AudioStore { required PlayArtist playArtist, required PlaySongs playSongs, required PlaySmartList playSmartList, + required PlayPlaylist playPlayist, required SeekToNext seekToNext, required ShuffleAll shuffleAll, required AudioPlayerRepository audioPlayerRepository, @@ -32,6 +35,7 @@ class AudioStore extends _AudioStore with _$AudioStore { playAlbum, playArtist, playSmartList, + playPlayist, seekToNext, shuffleAll, ); @@ -44,6 +48,7 @@ abstract class _AudioStore with Store { this._playAlbum, this._playArtist, this._playSmartList, + this._playPlaylist, this._seekToNext, this._shuffleAll, ); @@ -53,6 +58,7 @@ abstract class _AudioStore with Store { final PlayAlbum _playAlbum; final PlayArtist _playArtist; final PlaySmartList _playSmartList; + final PlayPlaylist _playPlaylist; final PlaySongs _playSongs; final SeekToNext _seekToNext; final ShuffleAll _shuffleAll; @@ -86,7 +92,8 @@ abstract class _AudioStore with Store { _audioPlayerRepository.loopModeStream.asObservable(); @computed - bool get hasNext => (queueIndexStream.value != null && + bool get hasNext => + (queueIndexStream.value != null && queueStream.value != null && queueIndexStream.value! < queueStream.value!.length - 1) || (loopModeStream.value ?? LoopMode.off) != LoopMode.off; @@ -130,5 +137,7 @@ abstract class _AudioStore with Store { Future playSmartList(SmartList smartList) async => _playSmartList(smartList); + Future playPlaylist(Playlist playlist) async => _playPlaylist(playlist); + Future shuffleArtist(Artist artist) async => _playArtist(artist); } diff --git a/lib/presentation/state/search_page_store.dart b/lib/presentation/state/search_page_store.dart index ba7711f..dc31c58 100644 --- a/lib/presentation/state/search_page_store.dart +++ b/lib/presentation/state/search_page_store.dart @@ -2,6 +2,8 @@ import 'package:mobx/mobx.dart'; import '../../domain/entities/album.dart'; import '../../domain/entities/artist.dart'; +import '../../domain/entities/playlist.dart'; +import '../../domain/entities/smart_list.dart'; import '../../domain/entities/song.dart'; import '../../domain/repositories/music_data_repository.dart'; @@ -29,15 +31,10 @@ abstract class _SearchPageStore with Store { ObservableList searchResultsAlbums = [].asObservable(); @observable ObservableList searchResultsSongs = [].asObservable(); - @observable - bool viewArtists = true; - + ObservableList searchResultsSmartLists = [].asObservable(); @observable - bool viewAlbums = true; - - @observable - bool viewSongs = true; + ObservableList searchResultsPlaylists = [].asObservable(); @action Future search(String searchText) async { @@ -54,6 +51,10 @@ abstract class _SearchPageStore with Store { (await _musicDataInfoRepository.searchAlbums(searchText, limit: limit)).asObservable(); searchResultsSongs = (await _musicDataInfoRepository.searchSongs(searchText, limit: limit)).asObservable(); + searchResultsSmartLists = + (await _musicDataInfoRepository.searchSmartLists(searchText, limit: limit)).asObservable(); + searchResultsPlaylists = + (await _musicDataInfoRepository.searchPlaylists(searchText, limit: limit)).asObservable(); } @action @@ -62,21 +63,8 @@ abstract class _SearchPageStore with Store { searchResultsArtists = [].asObservable(); searchResultsAlbums = [].asObservable(); searchResultsSongs = [].asObservable(); - } - - @action - void toggleViewArtists() { - viewArtists = !viewArtists; - } - - @action - void toggleViewAlbums() { - viewAlbums = !viewAlbums; - } - - @action - void toggleViewSongs() { - viewSongs = !viewSongs; + searchResultsSmartLists = [].asObservable(); + searchResultsPlaylists = [].asObservable(); } void dispose() {} diff --git a/lib/presentation/state/search_page_store.g.dart b/lib/presentation/state/search_page_store.g.dart index 4a26d8b..f710924 100644 --- a/lib/presentation/state/search_page_store.g.dart +++ b/lib/presentation/state/search_page_store.g.dart @@ -73,48 +73,37 @@ mixin _$SearchPageStore on _SearchPageStore, Store { }); } - final _$viewArtistsAtom = Atom(name: '_SearchPageStore.viewArtists'); + final _$searchResultsSmartListsAtom = + Atom(name: '_SearchPageStore.searchResultsSmartLists'); @override - bool get viewArtists { - _$viewArtistsAtom.reportRead(); - return super.viewArtists; + ObservableList get searchResultsSmartLists { + _$searchResultsSmartListsAtom.reportRead(); + return super.searchResultsSmartLists; } @override - set viewArtists(bool value) { - _$viewArtistsAtom.reportWrite(value, super.viewArtists, () { - super.viewArtists = value; + set searchResultsSmartLists(ObservableList value) { + _$searchResultsSmartListsAtom + .reportWrite(value, super.searchResultsSmartLists, () { + super.searchResultsSmartLists = value; }); } - final _$viewAlbumsAtom = Atom(name: '_SearchPageStore.viewAlbums'); + final _$searchResultsPlaylistsAtom = + Atom(name: '_SearchPageStore.searchResultsPlaylists'); @override - bool get viewAlbums { - _$viewAlbumsAtom.reportRead(); - return super.viewAlbums; + ObservableList get searchResultsPlaylists { + _$searchResultsPlaylistsAtom.reportRead(); + return super.searchResultsPlaylists; } @override - set viewAlbums(bool value) { - _$viewAlbumsAtom.reportWrite(value, super.viewAlbums, () { - super.viewAlbums = value; - }); - } - - final _$viewSongsAtom = Atom(name: '_SearchPageStore.viewSongs'); - - @override - bool get viewSongs { - _$viewSongsAtom.reportRead(); - return super.viewSongs; - } - - @override - set viewSongs(bool value) { - _$viewSongsAtom.reportWrite(value, super.viewSongs, () { - super.viewSongs = value; + set searchResultsPlaylists(ObservableList value) { + _$searchResultsPlaylistsAtom + .reportWrite(value, super.searchResultsPlaylists, () { + super.searchResultsPlaylists = value; }); } @@ -139,39 +128,6 @@ mixin _$SearchPageStore on _SearchPageStore, Store { } } - @override - void toggleViewArtists() { - final _$actionInfo = _$_SearchPageStoreActionController.startAction( - name: '_SearchPageStore.toggleViewArtists'); - try { - return super.toggleViewArtists(); - } finally { - _$_SearchPageStoreActionController.endAction(_$actionInfo); - } - } - - @override - void toggleViewAlbums() { - final _$actionInfo = _$_SearchPageStoreActionController.startAction( - name: '_SearchPageStore.toggleViewAlbums'); - try { - return super.toggleViewAlbums(); - } finally { - _$_SearchPageStoreActionController.endAction(_$actionInfo); - } - } - - @override - void toggleViewSongs() { - final _$actionInfo = _$_SearchPageStoreActionController.startAction( - name: '_SearchPageStore.toggleViewSongs'); - try { - return super.toggleViewSongs(); - } finally { - _$_SearchPageStoreActionController.endAction(_$actionInfo); - } - } - @override String toString() { return ''' @@ -179,9 +135,8 @@ query: ${query}, searchResultsArtists: ${searchResultsArtists}, searchResultsAlbums: ${searchResultsAlbums}, searchResultsSongs: ${searchResultsSongs}, -viewArtists: ${viewArtists}, -viewAlbums: ${viewAlbums}, -viewSongs: ${viewSongs} +searchResultsSmartLists: ${searchResultsSmartLists}, +searchResultsPlaylists: ${searchResultsPlaylists} '''; } } diff --git a/lib/presentation/theming.dart b/lib/presentation/theming.dart index b9c75c8..b6952f3 100644 --- a/lib/presentation/theming.dart +++ b/lib/presentation/theming.dart @@ -116,3 +116,30 @@ const TextStyle TEXT_SMALL_SUBTITLE = TextStyle( fontSize: 12.0, fontWeight: FontWeight.w300, ); + +extension TextStyleX on TextStyle { + /// A method to underline a text with a customizable [distance] between the text + /// and underline. The [color], [thickness] and [style] can be set + /// as the decorations of a [TextStyle]. + TextStyle underlined({ + Color? underlineColor, + Color? textColor, + double distance = 1, + double thickness = 1, + TextDecorationStyle style = TextDecorationStyle.solid, + }) { + return copyWith( + shadows: [ + Shadow( + color: textColor ?? (color ?? Colors.black), + offset: Offset(0, -distance), + ) + ], + color: Colors.transparent, + decoration: TextDecoration.underline, + decorationThickness: thickness, + decorationColor: underlineColor ?? color, + decorationStyle: style, + ); + } +} \ No newline at end of file diff --git a/lib/presentation/widgets/bottom_sheet/add_to_playlist.dart b/lib/presentation/widgets/bottom_sheet/add_to_playlist.dart index dfabc4a..672f12d 100644 --- a/lib/presentation/widgets/bottom_sheet/add_to_playlist.dart +++ b/lib/presentation/widgets/bottom_sheet/add_to_playlist.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; -import '../../state/music_data_store.dart'; import '../../../domain/entities/playlist.dart'; import '../../../domain/entities/song.dart'; +import '../../state/music_data_store.dart'; import '../../theming.dart'; class AddToPlaylistTile extends StatelessWidget { diff --git a/lib/presentation/widgets/song_bottom_sheet.dart b/lib/presentation/widgets/song_bottom_sheet.dart index e247734..54e047e 100644 --- a/lib/presentation/widgets/song_bottom_sheet.dart +++ b/lib/presentation/widgets/song_bottom_sheet.dart @@ -1,11 +1,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:get_it/get_it.dart'; -import 'bottom_sheet/add_to_playlist.dart'; import '../../domain/entities/album.dart'; import '../../domain/entities/artist.dart'; -import '../../domain/entities/playlist.dart'; import '../../domain/entities/song.dart'; import '../pages/album_details_page.dart'; import '../pages/artist_details_page.dart'; @@ -15,6 +13,7 @@ import '../state/navigation_store.dart'; import '../state/song_store.dart'; import '../theming.dart'; import '../utils.dart' as utils; +import 'bottom_sheet/add_to_playlist.dart'; import 'custom_modal_bottom_sheet.dart'; import 'exclude_level_options.dart'; import 'like_button.dart'; diff --git a/lib/system/datasources/moor/playlist_dao.dart b/lib/system/datasources/moor/playlist_dao.dart index 58e703e..4710979 100644 --- a/lib/system/datasources/moor/playlist_dao.dart +++ b/lib/system/datasources/moor/playlist_dao.dart @@ -298,6 +298,64 @@ class PlaylistDao extends DatabaseAccessor return query.watch().map( (moorSongList) => moorSongList.map((moorSong) => SongModel.fromMoor(moorSong)).toList()); } + + @override + Future> searchPlaylists(String searchText, {int? limit}) async { + final plSongs = await (select(playlistEntries) + ..orderBy([(t) => OrderingTerm(expression: t.position)])) + .join( + [innerJoin(songs, songs.path.equalsExp(playlistEntries.songPath))], + ).get(); + + final List result = await (select(playlists) + ..where((tbl) => tbl.name.regexp(searchText, dotAll: true, caseSensitive: false))) + .get() + .then( + (moorList) { + return moorList.map((moorPlaylist) { + final moorSongs = (plSongs.where( + (element) => element.readTable(playlistEntries).playlistId == moorPlaylist.id)) + .map((e) => e.readTable(songs)) + .toList(); + return PlaylistModel.fromMoor(moorPlaylist, moorSongs); + }).toList(); + }, + ); + + if (limit != null) { + if (limit < 0) return []; + return result.take(limit).toList(); + } + return result; + } + + @override + Future> searchSmartLists(String searchText, {int? limit}) async { + final slArtists = await (select(smartListArtists).join( + [innerJoin(artists, artists.name.equalsExp(smartListArtists.artistName))], + )).get(); + + final List result = await (select(smartLists) + ..where((tbl) => tbl.name.regexp(searchText, dotAll: true, caseSensitive: false))) + .get() + .then( + (moorList) { + return moorList.map((moorSmartList) { + final moorArtists = (slArtists.where( + (element) => element.readTable(smartListArtists).smartListId == moorSmartList.id)) + .map((e) => e.readTable(artists)) + .toList(); + return SmartListModel.fromMoor(moorSmartList, moorArtists); + }).toList(); + }, + ); + + if (limit != null) { + if (limit < 0) return []; + return result.take(limit).toList(); + } + return result; + } } List _generateOrderingTerms(sl.OrderBy orderBy) { diff --git a/lib/system/datasources/playlist_data_source.dart b/lib/system/datasources/playlist_data_source.dart index 02c5919..921299f 100644 --- a/lib/system/datasources/playlist_data_source.dart +++ b/lib/system/datasources/playlist_data_source.dart @@ -13,6 +13,7 @@ abstract class PlaylistDataSource { Future addSongsToPlaylist(PlaylistModel playlist, List songs); Future removeIndex(int playlistId, int index); Future moveEntry(int playlistId, int oldIndex, int newIndex); + Future> searchPlaylists(String searchText, {int? limit}); Stream> get smartListsStream; Stream getSmartListStream(int smartListId); @@ -25,4 +26,5 @@ abstract class PlaylistDataSource { Future updateSmartList(SmartListModel smartListModel); Future removeSmartList(SmartListModel smartListModel); Stream> getSmartListSongStream(SmartListModel smartList); + Future> searchSmartLists(String searchText, {int? limit}); } diff --git a/lib/system/repositories/music_data_repository_impl.dart b/lib/system/repositories/music_data_repository_impl.dart index b2315c4..980e715 100644 --- a/lib/system/repositories/music_data_repository_impl.dart +++ b/lib/system/repositories/music_data_repository_impl.dart @@ -324,6 +324,38 @@ class MusicDataRepositoryImpl implements MusicDataRepository { return dbResult; } + @override + Future> searchPlaylists(String searchText, {int? limit}) async { + if (searchText == '') return []; + + final searchTextLower = searchText.toLowerCase(); + + // TODO: need to clean the string? sql injection? + final dbResult = await _playlistDataSource.searchPlaylists(_fuzzy(searchTextLower)); + + dbResult.sort((a, b) => -_similarity(a.name.toLowerCase(), searchTextLower) + .compareTo(_similarity(b.name.toLowerCase(), searchTextLower))); + + if (limit != null) return dbResult.take(limit).toList(); + return dbResult; + } + + @override + Future> searchSmartLists(String searchText, {int? limit}) async { + if (searchText == '') return []; + + final searchTextLower = searchText.toLowerCase(); + + // TODO: need to clean the string? sql injection? + final dbResult = await _playlistDataSource.searchSmartLists(_fuzzy(searchTextLower)); + + dbResult.sort((a, b) => -_similarity(a.name.toLowerCase(), searchTextLower) + .compareTo(_similarity(b.name.toLowerCase(), searchTextLower))); + + if (limit != null) return dbResult.take(limit).toList(); + return dbResult; + } + double _similarity(String value, String searchText) { return value.startsWith(searchText) ? value.similarityTo(searchText) + 1