search page improvements
This commit is contained in:
parent
65cf69cde6
commit
964536c0f1
15 changed files with 495 additions and 306 deletions
|
@ -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 {
|
||||||
|
|
26
lib/domain/usecases/play_playlist.dart
Normal file
26
lib/domain/usecases/play_playlist.dart
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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(),
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {}
|
||||||
|
|
|
@ -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}
|
|
||||||
''';
|
''';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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';
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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});
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue