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<Album>> searchAlbums(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 {

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/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<void> setupGetIt() async {
playAlbum: getIt(),
playArtist: getIt(),
playSmartList: getIt(),
playPlayist: getIt(),
playSongs: getIt(),
seekToNext: getIt(),
shuffleAll: getIt(),
@ -129,6 +131,12 @@ Future<void> setupGetIt() async {
getIt(),
),
);
getIt.registerLazySingleton<PlayPlaylist>(
() => PlayPlaylist(
getIt(),
getIt(),
),
);
getIt.registerLazySingleton<PlaySongs>(
() => PlaySongs(
getIt(),

View file

@ -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';

View file

@ -71,7 +71,7 @@ class _PlaylistsPageState extends State<PlaylistsPage> with AutomaticKeepAliveCl
),
trailing: IconButton(
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 '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<SearchPage> {
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<SearchPage> {
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<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) ...[
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<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) ...[
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),
],
),
),
],
),
);

View file

@ -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<void> playSmartList(SmartList smartList) async => _playSmartList(smartList);
Future<void> playPlaylist(Playlist playlist) async => _playPlaylist(playlist);
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/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<Album> searchResultsAlbums = <Album>[].asObservable();
@observable
ObservableList<Song> searchResultsSongs = <Song>[].asObservable();
@observable
bool viewArtists = true;
ObservableList<SmartList> searchResultsSmartLists = <SmartList>[].asObservable();
@observable
bool viewAlbums = true;
@observable
bool viewSongs = true;
ObservableList<Playlist> searchResultsPlaylists = <Playlist>[].asObservable();
@action
Future<void> 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 = <Artist>[].asObservable();
searchResultsAlbums = <Album>[].asObservable();
searchResultsSongs = <Song>[].asObservable();
}
@action
void toggleViewArtists() {
viewArtists = !viewArtists;
}
@action
void toggleViewAlbums() {
viewAlbums = !viewAlbums;
}
@action
void toggleViewSongs() {
viewSongs = !viewSongs;
searchResultsSmartLists = <SmartList>[].asObservable();
searchResultsPlaylists = <Playlist>[].asObservable();
}
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
bool get viewArtists {
_$viewArtistsAtom.reportRead();
return super.viewArtists;
ObservableList<SmartList> get searchResultsSmartLists {
_$searchResultsSmartListsAtom.reportRead();
return super.searchResultsSmartLists;
}
@override
set viewArtists(bool value) {
_$viewArtistsAtom.reportWrite(value, super.viewArtists, () {
super.viewArtists = value;
set searchResultsSmartLists(ObservableList<SmartList> 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<Playlist> 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<Playlist> 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}
''';
}
}

View file

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

View file

@ -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 {

View file

@ -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';

View file

@ -298,6 +298,64 @@ class PlaylistDao extends DatabaseAccessor<MoorDatabase>
return query.watch().map(
(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) {

View file

@ -13,6 +13,7 @@ abstract class PlaylistDataSource {
Future<void> addSongsToPlaylist(PlaylistModel playlist, List<SongModel> songs);
Future<void> removeIndex(int playlistId, int index);
Future<void> moveEntry(int playlistId, int oldIndex, int newIndex);
Future<List<PlaylistModel>> searchPlaylists(String searchText, {int? limit});
Stream<List<SmartListModel>> get smartListsStream;
Stream<SmartListModel> getSmartListStream(int smartListId);
@ -25,4 +26,5 @@ abstract class PlaylistDataSource {
Future<void> updateSmartList(SmartListModel smartListModel);
Future<void> removeSmartList(SmartListModel smartListModel);
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;
}
@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) {
return value.startsWith(searchText)
? value.similarityTo(searchText) + 1