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<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 {
|
||||
|
|
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/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(),
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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() {}
|
||||
|
|
|
@ -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}
|
||||
''';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue