search page improvements

This commit is contained in:
Moritz Weber 2022-03-19 18:23:01 +01:00
parent 65cf69cde6
commit 964536c0f1
15 changed files with 495 additions and 306 deletions

View file

@ -35,6 +35,8 @@ abstract class MusicDataInfoRepository {
Future<List<Artist>> searchArtists(String searchText, {int? limit}); Future<List<Artist>> searchArtists(String searchText, {int? limit});
Future<List<Album>> searchAlbums(String searchText, {int? limit}); Future<List<Album>> searchAlbums(String searchText, {int? limit});
Future<List<Song>> searchSongs(String searchText, {int? limit}); Future<List<Song>> searchSongs(String searchText, {int? limit});
Future<List<SmartList>> searchSmartLists(String searchText, {int? limit});
Future<List<Playlist>> searchPlaylists(String searchText, {int? limit});
} }
abstract class MusicDataRepository extends MusicDataInfoRepository { abstract class MusicDataRepository extends MusicDataInfoRepository {

View file

@ -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<void> 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);
}
}

View file

@ -18,6 +18,7 @@ import 'domain/repositories/platform_integration_repository.dart';
import 'domain/repositories/settings_repository.dart'; import 'domain/repositories/settings_repository.dart';
import 'domain/usecases/play_album.dart'; import 'domain/usecases/play_album.dart';
import 'domain/usecases/play_artist.dart'; import 'domain/usecases/play_artist.dart';
import 'domain/usecases/play_playlist.dart';
import 'domain/usecases/play_smart_list.dart'; import 'domain/usecases/play_smart_list.dart';
import 'domain/usecases/play_songs.dart'; import 'domain/usecases/play_songs.dart';
import 'domain/usecases/seek_to_next.dart'; import 'domain/usecases/seek_to_next.dart';
@ -68,6 +69,7 @@ Future<void> setupGetIt() async {
playAlbum: getIt(), playAlbum: getIt(),
playArtist: getIt(), playArtist: getIt(),
playSmartList: getIt(), playSmartList: getIt(),
playPlayist: getIt(),
playSongs: getIt(), playSongs: getIt(),
seekToNext: getIt(), seekToNext: getIt(),
shuffleAll: getIt(), shuffleAll: getIt(),
@ -129,6 +131,12 @@ Future<void> setupGetIt() async {
getIt(), getIt(),
), ),
); );
getIt.registerLazySingleton<PlayPlaylist>(
() => PlayPlaylist(
getIt(),
getIt(),
),
);
getIt.registerLazySingleton<PlaySongs>( getIt.registerLazySingleton<PlaySongs>(
() => PlaySongs( () => PlaySongs(
getIt(), getIt(),

View file

@ -1,7 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import '../widgets/bottom_sheet/add_to_playlist.dart';
import '../../domain/entities/album.dart'; import '../../domain/entities/album.dart';
import '../../domain/entities/song.dart'; import '../../domain/entities/song.dart';
@ -10,6 +9,7 @@ import '../state/audio_store.dart';
import '../state/music_data_store.dart'; import '../state/music_data_store.dart';
import '../theming.dart'; import '../theming.dart';
import '../widgets/album_sliver_appbar.dart'; import '../widgets/album_sliver_appbar.dart';
import '../widgets/bottom_sheet/add_to_playlist.dart';
import '../widgets/custom_modal_bottom_sheet.dart'; import '../widgets/custom_modal_bottom_sheet.dart';
import '../widgets/exclude_level_options.dart'; import '../widgets/exclude_level_options.dart';
import '../widgets/like_count_options.dart'; import '../widgets/like_count_options.dart';

View file

@ -71,7 +71,7 @@ class _PlaylistsPageState extends State<PlaylistsPage> with AutomaticKeepAliveCl
), ),
trailing: IconButton( trailing: IconButton(
icon: const Icon(Icons.play_circle_fill_rounded, size: 32.0), icon: const Icon(Icons.play_circle_fill_rounded, size: 32.0),
onPressed: () => {}, onPressed: () => audioStore.playPlaylist(playlist),
), ),
); );
} }

View file

@ -13,6 +13,8 @@ import '../widgets/song_bottom_sheet.dart';
import '../widgets/song_list_tile.dart'; import '../widgets/song_list_tile.dart';
import 'album_details_page.dart'; import 'album_details_page.dart';
import 'artist_details_page.dart'; import 'artist_details_page.dart';
import 'playlist_page.dart';
import 'smart_list_page.dart';
class SearchPage extends StatefulWidget { class SearchPage extends StatefulWidget {
const SearchPage({Key? key}) : super(key: key); const SearchPage({Key? key}) : super(key: key);
@ -62,57 +64,151 @@ class _SearchPageState extends State<SearchPage> {
return SafeArea( return SafeArea(
child: Column( child: Column(
children: [ children: [
Padding( Container(
padding: const EdgeInsets.only( decoration: BoxDecoration(
top: 8.0, gradient: LinearGradient(
left: HORIZONTAL_PADDING - 8.0, begin: Alignment.topCenter,
right: HORIZONTAL_PADDING - 8.0, end: Alignment.bottomCenter,
bottom: 8.0, colors: [
DARK1,
// Colors.transparent,
Theme.of(context).scaffoldBackgroundColor,
],
),
), ),
child: StatefulBuilder(builder: (context, setState) { child: Column(
return TextField( children: [
controller: _textController, Padding(
decoration: InputDecoration( padding: const EdgeInsets.only(
hintText: 'Search', top: 8.0,
hintStyle: TEXT_HEADER.copyWith(color: Colors.white), left: HORIZONTAL_PADDING - 8.0,
fillColor: Colors.white10, right: HORIZONTAL_PADDING - 8.0,
filled: true, bottom: 8.0,
enabledBorder: ),
const OutlineInputBorder(borderSide: BorderSide.none, gapPadding: 0.0), child: StatefulBuilder(builder: (context, setState) {
focusedBorder: return TextField(
const OutlineInputBorder(borderSide: BorderSide.none, gapPadding: 0.0), controller: _textController,
contentPadding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0), decoration: InputDecoration(
suffixIcon: searchText.isNotEmpty hintText: 'Search',
? IconButton( hintStyle: TEXT_HEADER.copyWith(color: Colors.white),
icon: const Icon(Icons.clear_rounded), fillColor: Colors.white10,
color: Colors.white, filled: true,
onPressed: () { enabledBorder:
setState(() { const OutlineInputBorder(borderSide: BorderSide.none, gapPadding: 0.0),
searchText = ''; focusedBorder:
_textController.text = ''; const OutlineInputBorder(borderSide: BorderSide.none, gapPadding: 0.0),
}); contentPadding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 8.0),
searchStore.reset(); suffixIcon: searchText.isNotEmpty
}, ? IconButton(
) icon: const Icon(Icons.clear_rounded),
: const SizedBox.shrink(), 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) { Padding(
setState(() => searchText = text); padding: const EdgeInsets.only(
searchStore.search(text); left: HORIZONTAL_PADDING - 8.0,
}, right: HORIZONTAL_PADDING - 8.0,
focusNode: _searchFocus, 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( Expanded(
child: Observer(builder: (context) { child: Observer(builder: (context) {
final artists = searchStore.searchResultsArtists; final artists = searchStore.searchResultsArtists;
final albums = searchStore.searchResultsAlbums; final albums = searchStore.searchResultsAlbums;
final songs = searchStore.searchResultsSongs; final songs = searchStore.searchResultsSongs;
final smartlists = searchStore.searchResultsSmartLists;
final viewArtists = searchStore.viewArtists; final playlists = searchStore.searchResultsPlaylists;
final viewAlbums = searchStore.viewAlbums;
final viewSongs = searchStore.viewSongs;
return Scrollbar( return Scrollbar(
controller: _scrollController, controller: _scrollController,
@ -120,205 +216,192 @@ class _SearchPageState extends State<SearchPage> {
controller: _scrollController, controller: _scrollController,
slivers: [ slivers: [
if (artists.isNotEmpty) ...[ 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( SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
[ [
AnimatedSwitcher( ListTile(
duration: const Duration(milliseconds: 200), title: Text(
transitionBuilder: (child, animation) => SizeTransition( 'Artists',
sizeFactor: animation, style: TEXT_HEADER.underlined(
child: child, 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<Widget>(
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<Widget>(
builder: (BuildContext context) => ArtistDetailsPage(
artist: artist,
),
),
);
},
),
const SizedBox(height: 16.0),
], ],
), ),
), ),
], ],
if (albums.isNotEmpty) ...[ 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( SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
[ [
AnimatedSwitcher( ListTile(
duration: const Duration(milliseconds: 200), title: Text(
transitionBuilder: (child, animation) => SizeTransition( 'Albums',
sizeFactor: animation, style: TEXT_HEADER.underlined(
child: child, 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<Widget>(
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<Widget>(
builder: (BuildContext context) => AlbumDetailsPage(
album: album,
),
),
);
},
),
const SizedBox(height: 16.0),
], ],
), ),
), ),
], ],
if (songs.isNotEmpty) ...[ 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( SliverList(
delegate: SliverChildListDelegate( delegate: SliverChildListDelegate(
[ [
AnimatedSwitcher( ListTile(
duration: const Duration(milliseconds: 200), title: Text(
transitionBuilder: (child, animation) => SizeTransition( 'Songs',
sizeFactor: animation, style: TEXT_HEADER.underlined(
child: child, 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),
],
),
),
], ],
), ),
); );

View file

@ -4,12 +4,14 @@ import '../../domain/entities/album.dart';
import '../../domain/entities/artist.dart'; import '../../domain/entities/artist.dart';
import '../../domain/entities/loop_mode.dart'; import '../../domain/entities/loop_mode.dart';
import '../../domain/entities/playable.dart'; import '../../domain/entities/playable.dart';
import '../../domain/entities/playlist.dart';
import '../../domain/entities/shuffle_mode.dart'; import '../../domain/entities/shuffle_mode.dart';
import '../../domain/entities/smart_list.dart'; import '../../domain/entities/smart_list.dart';
import '../../domain/entities/song.dart'; import '../../domain/entities/song.dart';
import '../../domain/repositories/audio_player_repository.dart'; import '../../domain/repositories/audio_player_repository.dart';
import '../../domain/usecases/play_album.dart'; import '../../domain/usecases/play_album.dart';
import '../../domain/usecases/play_artist.dart'; import '../../domain/usecases/play_artist.dart';
import '../../domain/usecases/play_playlist.dart';
import '../../domain/usecases/play_smart_list.dart'; import '../../domain/usecases/play_smart_list.dart';
import '../../domain/usecases/play_songs.dart'; import '../../domain/usecases/play_songs.dart';
import '../../domain/usecases/seek_to_next.dart'; import '../../domain/usecases/seek_to_next.dart';
@ -23,6 +25,7 @@ class AudioStore extends _AudioStore with _$AudioStore {
required PlayArtist playArtist, required PlayArtist playArtist,
required PlaySongs playSongs, required PlaySongs playSongs,
required PlaySmartList playSmartList, required PlaySmartList playSmartList,
required PlayPlaylist playPlayist,
required SeekToNext seekToNext, required SeekToNext seekToNext,
required ShuffleAll shuffleAll, required ShuffleAll shuffleAll,
required AudioPlayerRepository audioPlayerRepository, required AudioPlayerRepository audioPlayerRepository,
@ -32,6 +35,7 @@ class AudioStore extends _AudioStore with _$AudioStore {
playAlbum, playAlbum,
playArtist, playArtist,
playSmartList, playSmartList,
playPlayist,
seekToNext, seekToNext,
shuffleAll, shuffleAll,
); );
@ -44,6 +48,7 @@ abstract class _AudioStore with Store {
this._playAlbum, this._playAlbum,
this._playArtist, this._playArtist,
this._playSmartList, this._playSmartList,
this._playPlaylist,
this._seekToNext, this._seekToNext,
this._shuffleAll, this._shuffleAll,
); );
@ -53,6 +58,7 @@ abstract class _AudioStore with Store {
final PlayAlbum _playAlbum; final PlayAlbum _playAlbum;
final PlayArtist _playArtist; final PlayArtist _playArtist;
final PlaySmartList _playSmartList; final PlaySmartList _playSmartList;
final PlayPlaylist _playPlaylist;
final PlaySongs _playSongs; final PlaySongs _playSongs;
final SeekToNext _seekToNext; final SeekToNext _seekToNext;
final ShuffleAll _shuffleAll; final ShuffleAll _shuffleAll;
@ -86,7 +92,8 @@ abstract class _AudioStore with Store {
_audioPlayerRepository.loopModeStream.asObservable(); _audioPlayerRepository.loopModeStream.asObservable();
@computed @computed
bool get hasNext => (queueIndexStream.value != null && bool get hasNext =>
(queueIndexStream.value != null &&
queueStream.value != null && queueStream.value != null &&
queueIndexStream.value! < queueStream.value!.length - 1) || queueIndexStream.value! < queueStream.value!.length - 1) ||
(loopModeStream.value ?? LoopMode.off) != LoopMode.off; (loopModeStream.value ?? LoopMode.off) != LoopMode.off;
@ -130,5 +137,7 @@ abstract class _AudioStore with Store {
Future<void> playSmartList(SmartList smartList) async => _playSmartList(smartList); Future<void> playSmartList(SmartList smartList) async => _playSmartList(smartList);
Future<void> playPlaylist(Playlist playlist) async => _playPlaylist(playlist);
Future<void> shuffleArtist(Artist artist) async => _playArtist(artist); Future<void> shuffleArtist(Artist artist) async => _playArtist(artist);
} }

View file

@ -2,6 +2,8 @@ import 'package:mobx/mobx.dart';
import '../../domain/entities/album.dart'; import '../../domain/entities/album.dart';
import '../../domain/entities/artist.dart'; import '../../domain/entities/artist.dart';
import '../../domain/entities/playlist.dart';
import '../../domain/entities/smart_list.dart';
import '../../domain/entities/song.dart'; import '../../domain/entities/song.dart';
import '../../domain/repositories/music_data_repository.dart'; import '../../domain/repositories/music_data_repository.dart';
@ -29,15 +31,10 @@ abstract class _SearchPageStore with Store {
ObservableList<Album> searchResultsAlbums = <Album>[].asObservable(); ObservableList<Album> searchResultsAlbums = <Album>[].asObservable();
@observable @observable
ObservableList<Song> searchResultsSongs = <Song>[].asObservable(); ObservableList<Song> searchResultsSongs = <Song>[].asObservable();
@observable @observable
bool viewArtists = true; ObservableList<SmartList> searchResultsSmartLists = <SmartList>[].asObservable();
@observable @observable
bool viewAlbums = true; ObservableList<Playlist> searchResultsPlaylists = <Playlist>[].asObservable();
@observable
bool viewSongs = true;
@action @action
Future<void> search(String searchText) async { Future<void> search(String searchText) async {
@ -54,6 +51,10 @@ abstract class _SearchPageStore with Store {
(await _musicDataInfoRepository.searchAlbums(searchText, limit: limit)).asObservable(); (await _musicDataInfoRepository.searchAlbums(searchText, limit: limit)).asObservable();
searchResultsSongs = searchResultsSongs =
(await _musicDataInfoRepository.searchSongs(searchText, limit: limit)).asObservable(); (await _musicDataInfoRepository.searchSongs(searchText, limit: limit)).asObservable();
searchResultsSmartLists =
(await _musicDataInfoRepository.searchSmartLists(searchText, limit: limit)).asObservable();
searchResultsPlaylists =
(await _musicDataInfoRepository.searchPlaylists(searchText, limit: limit)).asObservable();
} }
@action @action
@ -62,21 +63,8 @@ abstract class _SearchPageStore with Store {
searchResultsArtists = <Artist>[].asObservable(); searchResultsArtists = <Artist>[].asObservable();
searchResultsAlbums = <Album>[].asObservable(); searchResultsAlbums = <Album>[].asObservable();
searchResultsSongs = <Song>[].asObservable(); searchResultsSongs = <Song>[].asObservable();
} searchResultsSmartLists = <SmartList>[].asObservable();
searchResultsPlaylists = <Playlist>[].asObservable();
@action
void toggleViewArtists() {
viewArtists = !viewArtists;
}
@action
void toggleViewAlbums() {
viewAlbums = !viewAlbums;
}
@action
void toggleViewSongs() {
viewSongs = !viewSongs;
} }
void dispose() {} void dispose() {}

View file

@ -73,48 +73,37 @@ mixin _$SearchPageStore on _SearchPageStore, Store {
}); });
} }
final _$viewArtistsAtom = Atom(name: '_SearchPageStore.viewArtists'); final _$searchResultsSmartListsAtom =
Atom(name: '_SearchPageStore.searchResultsSmartLists');
@override @override
bool get viewArtists { ObservableList<SmartList> get searchResultsSmartLists {
_$viewArtistsAtom.reportRead(); _$searchResultsSmartListsAtom.reportRead();
return super.viewArtists; return super.searchResultsSmartLists;
} }
@override @override
set viewArtists(bool value) { set searchResultsSmartLists(ObservableList<SmartList> value) {
_$viewArtistsAtom.reportWrite(value, super.viewArtists, () { _$searchResultsSmartListsAtom
super.viewArtists = value; .reportWrite(value, super.searchResultsSmartLists, () {
super.searchResultsSmartLists = value;
}); });
} }
final _$viewAlbumsAtom = Atom(name: '_SearchPageStore.viewAlbums'); final _$searchResultsPlaylistsAtom =
Atom(name: '_SearchPageStore.searchResultsPlaylists');
@override @override
bool get viewAlbums { ObservableList<Playlist> get searchResultsPlaylists {
_$viewAlbumsAtom.reportRead(); _$searchResultsPlaylistsAtom.reportRead();
return super.viewAlbums; return super.searchResultsPlaylists;
} }
@override @override
set viewAlbums(bool value) { set searchResultsPlaylists(ObservableList<Playlist> value) {
_$viewAlbumsAtom.reportWrite(value, super.viewAlbums, () { _$searchResultsPlaylistsAtom
super.viewAlbums = value; .reportWrite(value, super.searchResultsPlaylists, () {
}); super.searchResultsPlaylists = 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;
}); });
} }
@ -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 @override
String toString() { String toString() {
return ''' return '''
@ -179,9 +135,8 @@ query: ${query},
searchResultsArtists: ${searchResultsArtists}, searchResultsArtists: ${searchResultsArtists},
searchResultsAlbums: ${searchResultsAlbums}, searchResultsAlbums: ${searchResultsAlbums},
searchResultsSongs: ${searchResultsSongs}, searchResultsSongs: ${searchResultsSongs},
viewArtists: ${viewArtists}, searchResultsSmartLists: ${searchResultsSmartLists},
viewAlbums: ${viewAlbums}, searchResultsPlaylists: ${searchResultsPlaylists}
viewSongs: ${viewSongs}
'''; ''';
} }
} }

View file

@ -116,3 +116,30 @@ const TextStyle TEXT_SMALL_SUBTITLE = TextStyle(
fontSize: 12.0, fontSize: 12.0,
fontWeight: FontWeight.w300, 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,
);
}
}

View file

@ -1,9 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import '../../state/music_data_store.dart';
import '../../../domain/entities/playlist.dart'; import '../../../domain/entities/playlist.dart';
import '../../../domain/entities/song.dart'; import '../../../domain/entities/song.dart';
import '../../state/music_data_store.dart';
import '../../theming.dart'; import '../../theming.dart';
class AddToPlaylistTile extends StatelessWidget { class AddToPlaylistTile extends StatelessWidget {

View file

@ -1,11 +1,9 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:get_it/get_it.dart'; import 'package:get_it/get_it.dart';
import 'bottom_sheet/add_to_playlist.dart';
import '../../domain/entities/album.dart'; import '../../domain/entities/album.dart';
import '../../domain/entities/artist.dart'; import '../../domain/entities/artist.dart';
import '../../domain/entities/playlist.dart';
import '../../domain/entities/song.dart'; import '../../domain/entities/song.dart';
import '../pages/album_details_page.dart'; import '../pages/album_details_page.dart';
import '../pages/artist_details_page.dart'; import '../pages/artist_details_page.dart';
@ -15,6 +13,7 @@ import '../state/navigation_store.dart';
import '../state/song_store.dart'; import '../state/song_store.dart';
import '../theming.dart'; import '../theming.dart';
import '../utils.dart' as utils; import '../utils.dart' as utils;
import 'bottom_sheet/add_to_playlist.dart';
import 'custom_modal_bottom_sheet.dart'; import 'custom_modal_bottom_sheet.dart';
import 'exclude_level_options.dart'; import 'exclude_level_options.dart';
import 'like_button.dart'; import 'like_button.dart';

View file

@ -298,6 +298,64 @@ class PlaylistDao extends DatabaseAccessor<MoorDatabase>
return query.watch().map( return query.watch().map(
(moorSongList) => moorSongList.map((moorSong) => SongModel.fromMoor(moorSong)).toList()); (moorSongList) => moorSongList.map((moorSong) => SongModel.fromMoor(moorSong)).toList());
} }
@override
Future<List<PlaylistModel>> 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<PlaylistModel> 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<List<SmartListModel>> searchSmartLists(String searchText, {int? limit}) async {
final slArtists = await (select(smartListArtists).join(
[innerJoin(artists, artists.name.equalsExp(smartListArtists.artistName))],
)).get();
final List<SmartListModel> 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<OrderingTerm Function($SongsTable)> _generateOrderingTerms(sl.OrderBy orderBy) { List<OrderingTerm Function($SongsTable)> _generateOrderingTerms(sl.OrderBy orderBy) {

View file

@ -13,6 +13,7 @@ abstract class PlaylistDataSource {
Future<void> addSongsToPlaylist(PlaylistModel playlist, List<SongModel> songs); Future<void> addSongsToPlaylist(PlaylistModel playlist, List<SongModel> songs);
Future<void> removeIndex(int playlistId, int index); Future<void> removeIndex(int playlistId, int index);
Future<void> moveEntry(int playlistId, int oldIndex, int newIndex); Future<void> moveEntry(int playlistId, int oldIndex, int newIndex);
Future<List<PlaylistModel>> searchPlaylists(String searchText, {int? limit});
Stream<List<SmartListModel>> get smartListsStream; Stream<List<SmartListModel>> get smartListsStream;
Stream<SmartListModel> getSmartListStream(int smartListId); Stream<SmartListModel> getSmartListStream(int smartListId);
@ -25,4 +26,5 @@ abstract class PlaylistDataSource {
Future<void> updateSmartList(SmartListModel smartListModel); Future<void> updateSmartList(SmartListModel smartListModel);
Future<void> removeSmartList(SmartListModel smartListModel); Future<void> removeSmartList(SmartListModel smartListModel);
Stream<List<SongModel>> getSmartListSongStream(SmartListModel smartList); Stream<List<SongModel>> getSmartListSongStream(SmartListModel smartList);
Future<List<SmartListModel>> searchSmartLists(String searchText, {int? limit});
} }

View file

@ -324,6 +324,38 @@ class MusicDataRepositoryImpl implements MusicDataRepository {
return dbResult; return dbResult;
} }
@override
Future<List<Playlist>> 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<List<SmartList>> 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) { double _similarity(String value, String searchText) {
return value.startsWith(searchText) return value.startsWith(searchText)
? value.similarityTo(searchText) + 1 ? value.similarityTo(searchText) + 1