From a06cd0da84cc03a2a7cadbc80d70556cb0cf9310 Mon Sep 17 00:00:00 2001 From: Kingkor Roy Tirtho Date: Fri, 6 Jan 2023 00:48:04 +0600 Subject: [PATCH] feat: search/filter tracks inside playlist or album --- lib/components/library/user_artists.dart | 2 +- .../track_table/track_collection_view.dart | 93 ++++++++++++++++--- .../shared/track_table/track_tile.dart | 4 +- lib/extensions/list.dart | 49 ++++++++++ 4 files changed, 131 insertions(+), 17 deletions(-) diff --git a/lib/components/library/user_artists.dart b/lib/components/library/user_artists.dart index 294470da4..2411c90ff 100644 --- a/lib/components/library/user_artists.dart +++ b/lib/components/library/user_artists.dart @@ -64,7 +64,7 @@ class UserArtists extends HookConsumerWidget { ), ), backgroundColor: PlatformTheme.of(context).scaffoldBackgroundColor, - body: (artistQuery.isLoading || !artistQuery.hasData) + body: artistQuery.pages.isEmpty ? Padding( padding: const EdgeInsets.all(20), child: Row( diff --git a/lib/components/shared/track_table/track_collection_view.dart b/lib/components/shared/track_table/track_collection_view.dart index d3319b715..cbdaf0ed8 100644 --- a/lib/components/shared/track_table/track_collection_view.dart +++ b/lib/components/shared/track_table/track_collection_view.dart @@ -1,5 +1,7 @@ import 'package:fl_query/fl_query.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:collection/collection.dart'; +import 'package:fuzzywuzzy/fuzzywuzzy.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:platform_ui/platform_ui.dart'; @@ -8,13 +10,14 @@ import 'package:spotube/components/shared/page_window_title_bar.dart'; import 'package:spotube/components/shared/image/universal_image.dart'; import 'package:spotube/components/shared/track_table/tracks_table_view.dart'; import 'package:spotube/provider/auth_provider.dart'; -import 'package:spotube/utils/type_conversion_utils.dart'; import 'package:spotube/hooks/use_custom_status_bar_color.dart'; import 'package:spotube/hooks/use_palette_color.dart'; import 'package:spotube/models/logger.dart'; import 'package:flutter/material.dart'; import 'package:spotify/spotify.dart'; import 'package:spotube/utils/platform.dart'; +import 'package:spotube/utils/type_conversion_utils.dart'; +import 'package:tuple/tuple.dart'; class TrackCollectionView extends HookConsumerWidget { final logger = getLogger(TrackCollectionView); @@ -97,6 +100,28 @@ class TrackCollectionView extends HookConsumerWidget { final collapsed = useState(false); + final searchText = useState(""); + + final filteredTracks = useMemoized(() { + return tracksSnapshot.data + ?.mapIndexed((i, e) => Tuple2( + searchText.value.isEmpty + ? 100 + : weightedRatio( + "${e.name} - ${TypeConversionUtils.artists_X_String(e.artists ?? [])}", + searchText.value, + ), + e..discNumber = i, + )) + .toList() + .sorted( + (a, b) => b.item1.compareTo(a.item1), + ) + .where((e) => e.item1 > 50) + .map((e) => e.item2) + .toList(); + }, [tracksSnapshot.data, searchText.value]); + useCustomStatusBarColor( color?.color ?? PlatformTheme.of(context).scaffoldBackgroundColor!, GoRouter.of(context).location == routePath, @@ -116,13 +141,51 @@ class TrackCollectionView extends HookConsumerWidget { return () => controller.removeListener(listener); }, [collapsed.value]); + final leading = Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (platform != TargetPlatform.windows) + PlatformBackButton(color: color?.titleTextColor), + const SizedBox(width: 10), + ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 400, + maxHeight: 40, + ), + child: PlatformTextField( + onChanged: (value) => searchText.value = value, + placeholder: "Search tracks...", + backgroundColor: Colors.transparent, + prefixIcon: Icons.search_rounded, + ), + ), + ], + ); + + useEffect(() { + OverlayEntry? entry; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (platform == TargetPlatform.windows) { + entry = OverlayEntry(builder: (context) { + return Positioned( + left: 40, + top: 7, + child: leading, + ); + }); + Overlay.of(context)!.insert(entry!); + } + }); + return () => entry?.remove(); + }, [color?.titleTextColor, leading]); + return SafeArea( child: PlatformScaffold( appBar: kIsDesktop ? PageWindowTitleBar( backgroundColor: color?.color, foregroundColor: color?.titleTextColor, - leading: PlatformBackButton(color: color?.titleTextColor), + leading: leading, ) : null, body: CustomScrollView( @@ -134,9 +197,7 @@ class TrackCollectionView extends HookConsumerWidget { pinned: true, expandedHeight: 400, automaticallyImplyLeading: kIsMobile, - leading: kIsMobile - ? PlatformBackButton(color: color?.titleTextColor) - : null, + leading: kIsMobile ? leading : null, iconTheme: IconThemeData(color: color?.titleTextColor), primary: true, backgroundColor: color?.color, @@ -239,17 +300,19 @@ class TrackCollectionView extends HookConsumerWidget { child: PlatformText("Error ${tracksSnapshot.error}")); } - final tracks = tracksSnapshot.data!; return TracksTableView( - tracks is! List - ? tracks - .map( - (track) => - TypeConversionUtils.simpleTrack_X_Track( - track, album!), - ) - .toList() - : tracks, + List.from( + (filteredTracks ?? []).map( + (e) { + if (e is Track) { + return e; + } else { + return TypeConversionUtils.simpleTrack_X_Track( + e, album!); + } + }, + ), + ), onTrackPlayButtonPressed: onPlay, playlistId: id, userPlaylist: isOwned, diff --git a/lib/components/shared/track_table/track_tile.dart b/lib/components/shared/track_table/track_tile.dart index 49dee2101..51577dfdb 100644 --- a/lib/components/shared/track_table/track_tile.dart +++ b/lib/components/shared/track_table/track_tile.dart @@ -199,7 +199,9 @@ class TrackTile extends HookConsumerWidget { height: 20, width: 35, child: Center( - child: AutoSizeText((track.key + 1).toString()), + child: AutoSizeText( + (track.key + 1).toString(), + ), ), ), Padding( diff --git a/lib/extensions/list.dart b/lib/extensions/list.dart index 4b364e711..b8ca784ec 100644 --- a/lib/extensions/list.dart +++ b/lib/extensions/list.dart @@ -1,4 +1,5 @@ import 'package:collection/collection.dart'; +import 'package:tuple/tuple.dart'; extension MultiSortListMap on List { /// [preference] - List of properties in which you want to sort the list @@ -46,3 +47,51 @@ extension MultiSortListMap on List { return sorted((a, b) => sortAll(a, b)); } } + +extension MultiSortListTupleMap on List> { + /// [preference] - List of properties in which you want to sort the list + /// i.e. + /// ``` + /// List preference = ['property1','property2']; + /// ``` + /// This will first sort the list by property1 then by property2 + /// + /// [criteria] - List of booleans that specifies the criteria of sort + /// i.e., For ascending order `true` and for descending order `false`. + /// ``` + /// List criteria = [true. false]; + /// ``` + List> sortByProperties( + List criteria, List preference) { + if (preference.isEmpty || criteria.isEmpty || isEmpty) { + return this; + } + if (preference.length != criteria.length) { + print('Criteria length is not equal to preference'); + return this; + } + + int compare(int i, Tuple2 a, Tuple2 b) { + if (a.item1[preference[i]] == b.item1[preference[i]]) { + return 0; + } else if (a.item1[preference[i]] > b.item1[preference[i]]) { + return criteria[i] ? 1 : -1; + } else { + return criteria[i] ? -1 : 1; + } + } + + int sortAll(Tuple2 a, Tuple2 b) { + int i = 0; + int result = 0; + while (i < preference.length) { + result = compare(i, a, b); + if (result != 0) break; + i++; + } + return result; + } + + return sorted((a, b) => sortAll(a, b)); + } +}