From d4f99ec89927ea78f070707509ff3222ec402942 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Sun, 10 Sep 2023 22:28:38 +0600 Subject: [PATCH] feat(player_queue): filtering track support --- lib/components/player/player_overlay.dart | 18 +- lib/components/player/player_queue.dart | 268 +++++++++++++++------- 2 files changed, 203 insertions(+), 83 deletions(-) diff --git a/lib/components/player/player_overlay.dart b/lib/components/player/player_overlay.dart index f4984ad26..889e66093 100644 --- a/lib/components/player/player_overlay.dart +++ b/lib/components/player/player_overlay.dart @@ -100,9 +100,13 @@ class PlayerOverlay extends HookConsumerWidget { child: GestureDetector( onTap: () => GoRouter.of(context).push("/player"), - child: PlayerTrackDetails( - albumArt: albumArt, - color: textColor, + child: Container( + width: double.infinity, + color: Colors.transparent, + child: PlayerTrackDetails( + albumArt: albumArt, + color: textColor, + ), ), ), ), @@ -114,7 +118,9 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipBack, color: textColor, ), - onPressed: playlistNotifier.previous, + onPressed: playlist.isFetching + ? null + : playlistNotifier.previous, ), Consumer( builder: (context, ref, _) { @@ -143,7 +149,9 @@ class PlayerOverlay extends HookConsumerWidget { SpotubeIcons.skipForward, color: textColor, ), - onPressed: playlistNotifier.next, + onPressed: playlist.isFetching + ? null + : playlistNotifier.next, ), ], ), diff --git a/lib/components/player/player_queue.dart b/lib/components/player/player_queue.dart index 599da26e9..a5dee8c9a 100644 --- a/lib/components/player/player_queue.dart +++ b/lib/components/player/player_queue.dart @@ -1,16 +1,21 @@ import 'dart:ui'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/track_table/track_tile.dart'; +import 'package:spotube/extensions/constrains.dart'; import 'package:spotube/extensions/context.dart'; import 'package:spotube/hooks/use_auto_scroll_controller.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; class PlayerQueue extends HookConsumerWidget { final bool floating; @@ -24,12 +29,11 @@ class PlayerQueue extends HookConsumerWidget { final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final controller = useAutoScrollController(); - final tracks = playlist.tracks; + final searchText = useState(''); - if (tracks.isEmpty) { - return const NotFound(vertical: true); - } + final isSearching = useState(false); + final tracks = playlist.tracks; final borderRadius = floating ? BorderRadius.circular(10) : const BorderRadius.only( @@ -39,6 +43,27 @@ class PlayerQueue extends HookConsumerWidget { final theme = Theme.of(context); final headlineColor = theme.textTheme.headlineSmall?.color; + final filteredTracks = useMemoized( + () { + if (searchText.value.isEmpty) { + return tracks; + } + return tracks + .map((e) => ( + weightedRatio( + '${e.name!} - ${TypeConversionUtils.artists_X_String(e.artists!)}', + searchText.value, + ), + e + )) + .sorted((a, b) => b.$1.compareTo(a.$1)) + .where((e) => e.$1 > 50) + .map((e) => e.$2) + .toList(); + }, + [tracks, searchText.value], + ); + useEffect(() { if (playlist.active == null) return null; @@ -50,6 +75,10 @@ class PlayerQueue extends HookConsumerWidget { return null; }, []); + if (tracks.isEmpty) { + return const NotFound(vertical: true); + } + return BackdropFilter( filter: ImageFilter.blur( sigmaX: 12.0, @@ -64,89 +93,172 @@ class PlayerQueue extends HookConsumerWidget { color: theme.scaffoldBackgroundColor.withOpacity(0.5), borderRadius: borderRadius, ), - child: Column( - children: [ - Container( - height: 5, - width: 100, - margin: const EdgeInsets.only(bottom: 5, top: 2), - decoration: BoxDecoration( - color: headlineColor, - borderRadius: BorderRadius.circular(20), - ), - ), - Row( + child: CallbackShortcuts( + bindings: { + LogicalKeySet(LogicalKeyboardKey.escape): () { + if (!isSearching.value) { + Navigator.of(context).pop(); + } + isSearching.value = false; + searchText.value = ''; + } + }, + child: LayoutBuilder(builder: (context, constraints) { + return Column( children: [ - const SizedBox(width: 10), - Text( - context.l10n.tracks_in_queue(tracks.length), - style: TextStyle( + Container( + height: 5, + width: 100, + margin: const EdgeInsets.only(bottom: 5, top: 2), + decoration: BoxDecoration( color: headlineColor, - fontWeight: FontWeight.bold, - fontSize: 18, + borderRadius: BorderRadius.circular(20), ), ), - const Spacer(), - FilledButton( - style: FilledButton.styleFrom( - backgroundColor: - theme.scaffoldBackgroundColor.withOpacity(0.5), - foregroundColor: theme.textTheme.headlineSmall?.color, - ), - child: Row( - children: [ - const Icon(SpotubeIcons.playlistRemove), - const SizedBox(width: 5), - Text(context.l10n.clear_all), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (constraints.mdAndUp || !isSearching.value) ...[ + const SizedBox(width: 10), + Text( + context.l10n.tracks_in_queue(tracks.length), + style: TextStyle( + color: headlineColor, + fontWeight: FontWeight.bold, + fontSize: 18, + ), + ), + const Spacer(), ], - ), - onPressed: () { - playlistNotifier.stop(); - Navigator.of(context).pop(); - }, + if (constraints.mdAndUp || isSearching.value) + TextField( + onChanged: (value) { + searchText.value = value; + }, + decoration: InputDecoration( + hintText: context.l10n.search, + isDense: true, + prefixIcon: constraints.smAndDown + ? IconButton( + icon: const Icon( + Icons.arrow_back_ios_new_outlined, + ), + onPressed: () { + isSearching.value = false; + searchText.value = ''; + }, + style: IconButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size.square(20), + ), + ) + : const Icon(SpotubeIcons.filter), + constraints: BoxConstraints( + maxHeight: 40, + maxWidth: constraints.smAndDown + ? constraints.maxWidth - 20 + : 300, + ), + ), + ) + else + IconButton.filledTonal( + icon: const Icon(SpotubeIcons.filter), + onPressed: () { + isSearching.value = !isSearching.value; + }, + ), + if (constraints.mdAndUp || !isSearching.value) ...[ + const SizedBox(width: 10), + FilledButton( + style: FilledButton.styleFrom( + backgroundColor: + theme.scaffoldBackgroundColor.withOpacity(0.5), + foregroundColor: theme.textTheme.headlineSmall?.color, + ), + child: Row( + children: [ + const Icon(SpotubeIcons.playlistRemove), + const SizedBox(width: 5), + Text(context.l10n.clear_all), + ], + ), + onPressed: () { + playlistNotifier.stop(); + Navigator.of(context).pop(); + }, + ), + const SizedBox(width: 10), + ], + ], ), - const SizedBox(width: 10), - ], - ), - const SizedBox(height: 10), - Flexible( - child: ReorderableListView.builder( - onReorder: (oldIndex, newIndex) { - playlistNotifier.moveTrack(oldIndex, newIndex); - }, - scrollController: controller, - itemCount: tracks.length, - shrinkWrap: true, - buildDefaultDragHandles: false, - itemBuilder: (context, i) { - final track = tracks.elementAt(i); - return AutoScrollTag( - key: ValueKey(i), - controller: controller, - index: i, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: TrackTile( + const SizedBox(height: 10), + if (!isSearching.value && searchText.value.isEmpty) + Flexible( + child: ReorderableListView.builder( + onReorder: (oldIndex, newIndex) { + playlistNotifier.moveTrack(oldIndex, newIndex); + }, + scrollController: controller, + itemCount: tracks.length, + shrinkWrap: true, + buildDefaultDragHandles: false, + itemBuilder: (context, i) { + final track = tracks.elementAt(i); + return AutoScrollTag( + key: ValueKey(i), + controller: controller, index: i, - track: track, - onTap: () async { - if (playlist.activeTrack?.id == track.id) { - return; - } - await playlistNotifier.jumpToTrack(track); - }, - leadingActions: [ - ReorderableDragStartListener( + child: Padding( + padding: + const EdgeInsets.symmetric(horizontal: 8.0), + child: TrackTile( index: i, - child: const Icon(SpotubeIcons.dragHandle), + track: track, + onTap: () async { + if (playlist.activeTrack?.id == track.id) { + return; + } + await playlistNotifier.jumpToTrack(track); + }, + leadingActions: [ + ReorderableDragStartListener( + index: i, + child: const Icon(SpotubeIcons.dragHandle), + ), + ], ), - ], - ), - ), - ); - }), - ), - ], + ), + ); + }, + ), + ) + else + Flexible( + child: ListView.builder( + itemCount: filteredTracks.length, + itemBuilder: (context, i) { + final track = filteredTracks.elementAt(i); + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: TrackTile( + index: i, + track: track, + onTap: () async { + if (playlist.activeTrack?.id == track.id) { + return; + } + await playlistNotifier.jumpToTrack(track); + }, + ), + ); + }, + ), + ), + ], + ); + }), ), ), );