feat(player_queue): filtering track support

This commit is contained in:
Kingkor Roy Tirtho 2023-09-10 22:28:38 +06:00
parent 1c50612559
commit d4f99ec899
2 changed files with 207 additions and 87 deletions

View File

@ -100,9 +100,13 @@ class PlayerOverlay extends HookConsumerWidget {
child: GestureDetector( child: GestureDetector(
onTap: () => onTap: () =>
GoRouter.of(context).push("/player"), GoRouter.of(context).push("/player"),
child: PlayerTrackDetails( child: Container(
albumArt: albumArt, width: double.infinity,
color: textColor, color: Colors.transparent,
child: PlayerTrackDetails(
albumArt: albumArt,
color: textColor,
),
), ),
), ),
), ),
@ -114,7 +118,9 @@ class PlayerOverlay extends HookConsumerWidget {
SpotubeIcons.skipBack, SpotubeIcons.skipBack,
color: textColor, color: textColor,
), ),
onPressed: playlistNotifier.previous, onPressed: playlist.isFetching
? null
: playlistNotifier.previous,
), ),
Consumer( Consumer(
builder: (context, ref, _) { builder: (context, ref, _) {
@ -143,7 +149,9 @@ class PlayerOverlay extends HookConsumerWidget {
SpotubeIcons.skipForward, SpotubeIcons.skipForward,
color: textColor, color: textColor,
), ),
onPressed: playlistNotifier.next, onPressed: playlist.isFetching
? null
: playlistNotifier.next,
), ),
], ],
), ),

View File

@ -1,16 +1,21 @@
import 'dart:ui'; import 'dart:ui';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:fuzzywuzzy/fuzzywuzzy.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:scroll_to_index/scroll_to_index.dart'; import 'package:scroll_to_index/scroll_to_index.dart';
import 'package:spotube/collections/spotube_icons.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/components/shared/fallbacks/not_found.dart'; import 'package:spotube/components/shared/fallbacks/not_found.dart';
import 'package:spotube/components/shared/track_table/track_tile.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/extensions/context.dart';
import 'package:spotube/hooks/use_auto_scroll_controller.dart'; import 'package:spotube/hooks/use_auto_scroll_controller.dart';
import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart'; import 'package:spotube/provider/proxy_playlist/proxy_playlist_provider.dart';
import 'package:spotube/utils/type_conversion_utils.dart';
class PlayerQueue extends HookConsumerWidget { class PlayerQueue extends HookConsumerWidget {
final bool floating; final bool floating;
@ -24,12 +29,11 @@ class PlayerQueue extends HookConsumerWidget {
final playlist = ref.watch(ProxyPlaylistNotifier.provider); final playlist = ref.watch(ProxyPlaylistNotifier.provider);
final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier); final playlistNotifier = ref.watch(ProxyPlaylistNotifier.notifier);
final controller = useAutoScrollController(); final controller = useAutoScrollController();
final searchText = useState('');
final isSearching = useState(false);
final tracks = playlist.tracks; final tracks = playlist.tracks;
if (tracks.isEmpty) {
return const NotFound(vertical: true);
}
final borderRadius = floating final borderRadius = floating
? BorderRadius.circular(10) ? BorderRadius.circular(10)
: const BorderRadius.only( : const BorderRadius.only(
@ -39,6 +43,27 @@ class PlayerQueue extends HookConsumerWidget {
final theme = Theme.of(context); final theme = Theme.of(context);
final headlineColor = theme.textTheme.headlineSmall?.color; 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(() { useEffect(() {
if (playlist.active == null) return null; if (playlist.active == null) return null;
@ -50,6 +75,10 @@ class PlayerQueue extends HookConsumerWidget {
return null; return null;
}, []); }, []);
if (tracks.isEmpty) {
return const NotFound(vertical: true);
}
return BackdropFilter( return BackdropFilter(
filter: ImageFilter.blur( filter: ImageFilter.blur(
sigmaX: 12.0, sigmaX: 12.0,
@ -64,89 +93,172 @@ class PlayerQueue extends HookConsumerWidget {
color: theme.scaffoldBackgroundColor.withOpacity(0.5), color: theme.scaffoldBackgroundColor.withOpacity(0.5),
borderRadius: borderRadius, borderRadius: borderRadius,
), ),
child: Column( child: CallbackShortcuts(
children: [ bindings: {
Container( LogicalKeySet(LogicalKeyboardKey.escape): () {
height: 5, if (!isSearching.value) {
width: 100, Navigator.of(context).pop();
margin: const EdgeInsets.only(bottom: 5, top: 2), }
decoration: BoxDecoration( isSearching.value = false;
color: headlineColor, searchText.value = '';
borderRadius: BorderRadius.circular(20), }
), },
), child: LayoutBuilder(builder: (context, constraints) {
Row( return Column(
children: [ children: [
const SizedBox(width: 10), Container(
Text( height: 5,
context.l10n.tracks_in_queue(tracks.length), width: 100,
style: TextStyle( margin: const EdgeInsets.only(bottom: 5, top: 2),
decoration: BoxDecoration(
color: headlineColor, color: headlineColor,
fontWeight: FontWeight.bold, borderRadius: BorderRadius.circular(20),
fontSize: 18,
), ),
), ),
const Spacer(), Row(
FilledButton( crossAxisAlignment: CrossAxisAlignment.center,
style: FilledButton.styleFrom( mainAxisAlignment: MainAxisAlignment.center,
backgroundColor: children: [
theme.scaffoldBackgroundColor.withOpacity(0.5), if (constraints.mdAndUp || !isSearching.value) ...[
foregroundColor: theme.textTheme.headlineSmall?.color, const SizedBox(width: 10),
), Text(
child: Row( context.l10n.tracks_in_queue(tracks.length),
children: [ style: TextStyle(
const Icon(SpotubeIcons.playlistRemove), color: headlineColor,
const SizedBox(width: 5), fontWeight: FontWeight.bold,
Text(context.l10n.clear_all), fontSize: 18,
],
),
onPressed: () {
playlistNotifier.stop();
Navigator.of(context).pop();
},
),
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(
index: i,
track: track,
onTap: () async {
if (playlist.activeTrack?.id == track.id) {
return;
}
await playlistNotifier.jumpToTrack(track);
},
leadingActions: [
ReorderableDragStartListener(
index: i,
child: const Icon(SpotubeIcons.dragHandle),
),
],
), ),
), ),
); const Spacer(),
}), ],
), 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(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,
child: 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);
},
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);
},
),
);
},
),
),
],
);
}),
), ),
), ),
); );