mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-13 16:05:18 +00:00
feat: search/filter tracks inside playlist or album
This commit is contained in:
parent
8a6ba3b35f
commit
a06cd0da84
@ -64,7 +64,7 @@ class UserArtists extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
backgroundColor: PlatformTheme.of(context).scaffoldBackgroundColor,
|
backgroundColor: PlatformTheme.of(context).scaffoldBackgroundColor,
|
||||||
body: (artistQuery.isLoading || !artistQuery.hasData)
|
body: artistQuery.pages.isEmpty
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.all(20),
|
padding: const EdgeInsets.all(20),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import 'package:fl_query/fl_query.dart';
|
import 'package:fl_query/fl_query.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.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:go_router/go_router.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:platform_ui/platform_ui.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/image/universal_image.dart';
|
||||||
import 'package:spotube/components/shared/track_table/tracks_table_view.dart';
|
import 'package:spotube/components/shared/track_table/tracks_table_view.dart';
|
||||||
import 'package:spotube/provider/auth_provider.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_custom_status_bar_color.dart';
|
||||||
import 'package:spotube/hooks/use_palette_color.dart';
|
import 'package:spotube/hooks/use_palette_color.dart';
|
||||||
import 'package:spotube/models/logger.dart';
|
import 'package:spotube/models/logger.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:spotify/spotify.dart';
|
import 'package:spotify/spotify.dart';
|
||||||
import 'package:spotube/utils/platform.dart';
|
import 'package:spotube/utils/platform.dart';
|
||||||
|
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
class TrackCollectionView<T> extends HookConsumerWidget {
|
class TrackCollectionView<T> extends HookConsumerWidget {
|
||||||
final logger = getLogger(TrackCollectionView);
|
final logger = getLogger(TrackCollectionView);
|
||||||
@ -97,6 +100,28 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
|||||||
|
|
||||||
final collapsed = useState(false);
|
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<Artist>(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(
|
useCustomStatusBarColor(
|
||||||
color?.color ?? PlatformTheme.of(context).scaffoldBackgroundColor!,
|
color?.color ?? PlatformTheme.of(context).scaffoldBackgroundColor!,
|
||||||
GoRouter.of(context).location == routePath,
|
GoRouter.of(context).location == routePath,
|
||||||
@ -116,13 +141,51 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
|||||||
return () => controller.removeListener(listener);
|
return () => controller.removeListener(listener);
|
||||||
}, [collapsed.value]);
|
}, [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(
|
return SafeArea(
|
||||||
child: PlatformScaffold(
|
child: PlatformScaffold(
|
||||||
appBar: kIsDesktop
|
appBar: kIsDesktop
|
||||||
? PageWindowTitleBar(
|
? PageWindowTitleBar(
|
||||||
backgroundColor: color?.color,
|
backgroundColor: color?.color,
|
||||||
foregroundColor: color?.titleTextColor,
|
foregroundColor: color?.titleTextColor,
|
||||||
leading: PlatformBackButton(color: color?.titleTextColor),
|
leading: leading,
|
||||||
)
|
)
|
||||||
: null,
|
: null,
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
@ -134,9 +197,7 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
|||||||
pinned: true,
|
pinned: true,
|
||||||
expandedHeight: 400,
|
expandedHeight: 400,
|
||||||
automaticallyImplyLeading: kIsMobile,
|
automaticallyImplyLeading: kIsMobile,
|
||||||
leading: kIsMobile
|
leading: kIsMobile ? leading : null,
|
||||||
? PlatformBackButton(color: color?.titleTextColor)
|
|
||||||
: null,
|
|
||||||
iconTheme: IconThemeData(color: color?.titleTextColor),
|
iconTheme: IconThemeData(color: color?.titleTextColor),
|
||||||
primary: true,
|
primary: true,
|
||||||
backgroundColor: color?.color,
|
backgroundColor: color?.color,
|
||||||
@ -239,17 +300,19 @@ class TrackCollectionView<T> extends HookConsumerWidget {
|
|||||||
child: PlatformText("Error ${tracksSnapshot.error}"));
|
child: PlatformText("Error ${tracksSnapshot.error}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
final tracks = tracksSnapshot.data!;
|
|
||||||
return TracksTableView(
|
return TracksTableView(
|
||||||
tracks is! List<Track>
|
List.from(
|
||||||
? tracks
|
(filteredTracks ?? []).map(
|
||||||
.map(
|
(e) {
|
||||||
(track) =>
|
if (e is Track) {
|
||||||
TypeConversionUtils.simpleTrack_X_Track(
|
return e;
|
||||||
track, album!),
|
} else {
|
||||||
)
|
return TypeConversionUtils.simpleTrack_X_Track(
|
||||||
.toList()
|
e, album!);
|
||||||
: tracks,
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
onTrackPlayButtonPressed: onPlay,
|
onTrackPlayButtonPressed: onPlay,
|
||||||
playlistId: id,
|
playlistId: id,
|
||||||
userPlaylist: isOwned,
|
userPlaylist: isOwned,
|
||||||
|
@ -199,7 +199,9 @@ class TrackTile extends HookConsumerWidget {
|
|||||||
height: 20,
|
height: 20,
|
||||||
width: 35,
|
width: 35,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: AutoSizeText((track.key + 1).toString()),
|
child: AutoSizeText(
|
||||||
|
(track.key + 1).toString(),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Padding(
|
Padding(
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:tuple/tuple.dart';
|
||||||
|
|
||||||
extension MultiSortListMap on List<Map> {
|
extension MultiSortListMap on List<Map> {
|
||||||
/// [preference] - List of properties in which you want to sort the list
|
/// [preference] - List of properties in which you want to sort the list
|
||||||
@ -46,3 +47,51 @@ extension MultiSortListMap on List<Map> {
|
|||||||
return sorted((a, b) => sortAll(a, b));
|
return sorted((a, b) => sortAll(a, b));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MultiSortListTupleMap<V> on List<Tuple2<Map, V>> {
|
||||||
|
/// [preference] - List of properties in which you want to sort the list
|
||||||
|
/// i.e.
|
||||||
|
/// ```
|
||||||
|
/// List<String> 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<bool> criteria = [true. false];
|
||||||
|
/// ```
|
||||||
|
List<Tuple2<Map, V>> sortByProperties(
|
||||||
|
List<bool> criteria, List<String> 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<Map, V> a, Tuple2<Map, V> 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<Map, V> a, Tuple2<Map, V> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user