mirror of
https://github.com/KRTirtho/spotube.git
synced 2025-09-12 23:45:18 +00:00
feat: sort tracks in playlist, album and local tracks
This commit is contained in:
parent
91d5d1003b
commit
cb4bd25df1
@ -6,7 +6,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||
import 'package:spotube/components/Shared/TrackCollectionView.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||
import 'package:spotube/provider/Auth.dart';
|
||||
@ -18,14 +20,20 @@ class AlbumView extends HookConsumerWidget {
|
||||
final AlbumSimple album;
|
||||
const AlbumView(this.album, {Key? key}) : super(key: key);
|
||||
|
||||
Future<void> playPlaylist(Playback playback, List<Track> tracks,
|
||||
{Track? currentTrack}) async {
|
||||
currentTrack ??= tracks.first;
|
||||
Future<void> playPlaylist(
|
||||
Playback playback,
|
||||
List<Track> tracks,
|
||||
WidgetRef ref, {
|
||||
Track? currentTrack,
|
||||
}) async {
|
||||
final sortBy = ref.read(trackCollectionSortState(album.id!));
|
||||
final sortedTracks = ServiceUtils.sortTracks(tracks, sortBy);
|
||||
currentTrack ??= sortedTracks.first;
|
||||
final isPlaylistPlaying = playback.playlist?.id == album.id;
|
||||
if (!isPlaylistPlaying) {
|
||||
await playback.playPlaylist(
|
||||
CurrentPlaylist(
|
||||
tracks: tracks,
|
||||
tracks: sortedTracks,
|
||||
id: album.id!,
|
||||
name: album.name!,
|
||||
thumbnail: TypeConversionUtils.image_X_UrlString(
|
||||
@ -33,7 +41,7 @@ class AlbumView extends HookConsumerWidget {
|
||||
placeholder: ImagePlaceholder.collection,
|
||||
),
|
||||
),
|
||||
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||
sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||
);
|
||||
} else if (isPlaylistPlaying &&
|
||||
currentTrack.id != null &&
|
||||
@ -82,6 +90,7 @@ class AlbumView extends HookConsumerWidget {
|
||||
.map((track) =>
|
||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||
.toList(),
|
||||
ref,
|
||||
);
|
||||
} else if (isAlbumPlaying && track != null) {
|
||||
playPlaylist(
|
||||
@ -91,6 +100,7 @@ class AlbumView extends HookConsumerWidget {
|
||||
TypeConversionUtils.simpleTrack_X_Track(track, album))
|
||||
.toList(),
|
||||
currentTrack: track,
|
||||
ref,
|
||||
);
|
||||
} else {
|
||||
playback.stop();
|
||||
|
@ -1,7 +1,9 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:metadata_god/metadata_god.dart';
|
||||
import 'package:mime/mime.dart';
|
||||
@ -10,6 +12,7 @@ import 'package:path_provider/path_provider.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/LoaderShimmers/ShimmerTrackTile.dart';
|
||||
import 'package:spotube/components/Shared/SortTracksDropdown.dart';
|
||||
import 'package:spotube/components/Shared/TrackTile.dart';
|
||||
import 'package:spotube/hooks/useAsyncEffect.dart';
|
||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||
@ -18,6 +21,7 @@ import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/provider/UserPreferences.dart';
|
||||
import 'package:spotube/utils/platform.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
const supportedAudioTypes = [
|
||||
@ -37,6 +41,15 @@ const imgMimeToExt = {
|
||||
"image/gif": ".gif",
|
||||
};
|
||||
|
||||
enum SortBy {
|
||||
none,
|
||||
ascending,
|
||||
descending,
|
||||
artist,
|
||||
album,
|
||||
dateAdded,
|
||||
}
|
||||
|
||||
final localTracksProvider = FutureProvider<List<Track>>((ref) async {
|
||||
try {
|
||||
if (kIsWeb) return [];
|
||||
@ -132,6 +145,7 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, ref) {
|
||||
final sortBy = useState<SortBy>(SortBy.none);
|
||||
final playback = ref.watch(playbackProvider);
|
||||
final isPlaylistPlaying = playback.playlist?.id == "local";
|
||||
final trackSnapshot = ref.watch(localTracksProvider);
|
||||
@ -176,6 +190,13 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
: null,
|
||||
),
|
||||
const Spacer(),
|
||||
SortTracksDropdown(
|
||||
value: sortBy.value,
|
||||
onChanged: (value) {
|
||||
if (value != null) sortBy.value = value;
|
||||
},
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
ElevatedButton(
|
||||
child: const Icon(Icons.refresh_rounded),
|
||||
onPressed: () {
|
||||
@ -187,11 +208,15 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
),
|
||||
trackSnapshot.when(
|
||||
data: (tracks) {
|
||||
final sortedTracks = useMemoized(() {
|
||||
return ServiceUtils.sortTracks(tracks, sortBy.value);
|
||||
}, [sortBy.value, tracks]);
|
||||
|
||||
return Expanded(
|
||||
child: ListView.builder(
|
||||
itemCount: tracks.length,
|
||||
itemCount: sortedTracks.length,
|
||||
itemBuilder: (context, index) {
|
||||
final track = tracks[index];
|
||||
final track = sortedTracks[index];
|
||||
return TrackTile(
|
||||
playback,
|
||||
duration:
|
||||
@ -204,7 +229,7 @@ class UserLocalTracks extends HookConsumerWidget {
|
||||
onTrackPlayButtonPressed: (currentTrack) {
|
||||
return playLocalTracks(
|
||||
playback,
|
||||
tracks,
|
||||
sortedTracks,
|
||||
currentTrack: track,
|
||||
);
|
||||
},
|
||||
|
@ -6,6 +6,7 @@ import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotube/components/Shared/HeartButton.dart';
|
||||
import 'package:spotube/components/Shared/TrackCollectionView.dart';
|
||||
import 'package:spotube/components/Shared/TracksTableView.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/hooks/usePaletteColor.dart';
|
||||
import 'package:spotube/models/CurrentPlaylist.dart';
|
||||
@ -16,6 +17,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/provider/SpotifyDI.dart';
|
||||
import 'package:spotube/provider/SpotifyRequests.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
import 'package:spotube/utils/type_conversion_utils.dart';
|
||||
|
||||
class PlaylistView extends HookConsumerWidget {
|
||||
@ -23,15 +25,21 @@ class PlaylistView extends HookConsumerWidget {
|
||||
final PlaylistSimple playlist;
|
||||
PlaylistView(this.playlist, {Key? key}) : super(key: key);
|
||||
|
||||
playPlaylist(Playback playback, List<Track> tracks,
|
||||
{Track? currentTrack}) async {
|
||||
currentTrack ??= tracks.first;
|
||||
Future<void> playPlaylist(
|
||||
Playback playback,
|
||||
List<Track> tracks,
|
||||
WidgetRef ref, {
|
||||
Track? currentTrack,
|
||||
}) async {
|
||||
final sortBy = ref.read(trackCollectionSortState(playlist.id!));
|
||||
final sortedTracks = ServiceUtils.sortTracks(tracks, sortBy);
|
||||
currentTrack ??= sortedTracks.first;
|
||||
final isPlaylistPlaying =
|
||||
playback.playlist?.id != null && playback.playlist?.id == playlist.id;
|
||||
if (!isPlaylistPlaying) {
|
||||
await playback.playPlaylist(
|
||||
CurrentPlaylist(
|
||||
tracks: tracks,
|
||||
tracks: sortedTracks,
|
||||
id: playlist.id!,
|
||||
name: playlist.name!,
|
||||
thumbnail: TypeConversionUtils.image_X_UrlString(
|
||||
@ -39,7 +47,7 @@ class PlaylistView extends HookConsumerWidget {
|
||||
placeholder: ImagePlaceholder.collection,
|
||||
),
|
||||
),
|
||||
tracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||
sortedTracks.indexWhere((s) => s.id == currentTrack?.id),
|
||||
);
|
||||
} else if (isPlaylistPlaying &&
|
||||
currentTrack.id != null &&
|
||||
@ -85,14 +93,12 @@ class PlaylistView extends HookConsumerWidget {
|
||||
onPlay: ([track]) {
|
||||
if (tracksSnapshot.asData?.value != null) {
|
||||
if (!isPlaylistPlaying) {
|
||||
playPlaylist(
|
||||
playback,
|
||||
tracksSnapshot.asData!.value,
|
||||
);
|
||||
playPlaylist(playback, tracksSnapshot.asData!.value, ref);
|
||||
} else if (isPlaylistPlaying && track != null) {
|
||||
playPlaylist(
|
||||
playback,
|
||||
tracksSnapshot.asData!.value,
|
||||
ref,
|
||||
currentTrack: track,
|
||||
);
|
||||
} else {
|
||||
|
54
lib/components/Shared/SortTracksDropdown.dart
Normal file
54
lib/components/Shared/SortTracksDropdown.dart
Normal file
@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
||||
|
||||
class SortTracksDropdown extends StatelessWidget {
|
||||
final SortBy? value;
|
||||
final void Function(SortBy)? onChanged;
|
||||
const SortTracksDropdown({
|
||||
this.onChanged,
|
||||
this.value,
|
||||
Key? key,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return PopupMenuButton<SortBy>(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
value: SortBy.none,
|
||||
enabled: value != SortBy.none,
|
||||
child: const Text("None"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortBy.ascending,
|
||||
enabled: value != SortBy.ascending,
|
||||
child: const Text("Sort by A-Z"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortBy.descending,
|
||||
enabled: value != SortBy.descending,
|
||||
child: const Text("Sort by Z-A"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortBy.dateAdded,
|
||||
enabled: value != SortBy.dateAdded,
|
||||
child: const Text("Sort by Date"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortBy.artist,
|
||||
enabled: value != SortBy.artist,
|
||||
child: const Text("Sort by Artist"),
|
||||
),
|
||||
PopupMenuItem(
|
||||
value: SortBy.album,
|
||||
enabled: value != SortBy.album,
|
||||
child: const Text("Sort by Album"),
|
||||
),
|
||||
];
|
||||
},
|
||||
onSelected: onChanged,
|
||||
icon: const Icon(Icons.sort_rounded),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,13 +2,19 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
||||
import 'package:spotube/components/Shared/DownloadConfirmationDialog.dart';
|
||||
import 'package:spotube/components/Shared/NotFound.dart';
|
||||
import 'package:spotube/components/Shared/SortTracksDropdown.dart';
|
||||
import 'package:spotube/components/Shared/TrackTile.dart';
|
||||
import 'package:spotube/hooks/useBreakpoints.dart';
|
||||
import 'package:spotube/provider/Downloader.dart';
|
||||
import 'package:spotube/provider/Playback.dart';
|
||||
import 'package:spotube/utils/primitive_utils.dart';
|
||||
import 'package:spotube/utils/service_utils.dart';
|
||||
|
||||
final trackCollectionSortState =
|
||||
StateProvider.family<SortBy, String>((ref, _) => SortBy.none);
|
||||
|
||||
class TracksTableView extends HookConsumerWidget {
|
||||
final void Function(Track currentTrack)? onTrackPlayButtonPressed;
|
||||
@ -39,26 +45,34 @@ class TracksTableView extends HookConsumerWidget {
|
||||
|
||||
final selected = useState<List<String>>([]);
|
||||
final showCheck = useState<bool>(false);
|
||||
final sortBy = ref.watch(trackCollectionSortState(playlistId ?? ''));
|
||||
|
||||
final selectedTracks = useMemoized(
|
||||
() => tracks.where(
|
||||
(track) => selected.value.contains(track.id),
|
||||
),
|
||||
[tracks],
|
||||
final sortedTracks = useMemoized(
|
||||
() {
|
||||
return ServiceUtils.sortTracks(tracks, sortBy);
|
||||
},
|
||||
[tracks, sortBy],
|
||||
);
|
||||
|
||||
final children = tracks.isEmpty
|
||||
final selectedTracks = useMemoized(
|
||||
() => sortedTracks.where(
|
||||
(track) => selected.value.contains(track.id),
|
||||
),
|
||||
[sortedTracks],
|
||||
);
|
||||
|
||||
final children = sortedTracks.isEmpty
|
||||
? [const NotFound(vertical: true)]
|
||||
: [
|
||||
if (heading != null) heading!,
|
||||
Row(
|
||||
children: [
|
||||
Checkbox(
|
||||
value: selected.value.length == tracks.length,
|
||||
value: selected.value.length == sortedTracks.length,
|
||||
onChanged: (checked) {
|
||||
if (!showCheck.value) showCheck.value = true;
|
||||
if (checked == true) {
|
||||
selected.value = tracks.map((s) => s.id!).toList();
|
||||
selected.value = sortedTracks.map((s) => s.id!).toList();
|
||||
} else {
|
||||
selected.value = [];
|
||||
showCheck.value = false;
|
||||
@ -104,11 +118,20 @@ class TracksTableView extends HookConsumerWidget {
|
||||
Text("Time", style: tableHeadStyle),
|
||||
const SizedBox(width: 10),
|
||||
],
|
||||
SortTracksDropdown(
|
||||
value: sortBy,
|
||||
onChanged: (value) {
|
||||
ref
|
||||
.read(trackCollectionSortState(playlistId ?? '').state)
|
||||
.state = value;
|
||||
},
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
enabled: selected.value.isNotEmpty,
|
||||
value: "download",
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.file_download_outlined),
|
||||
@ -117,7 +140,6 @@ class TracksTableView extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
value: "download",
|
||||
),
|
||||
];
|
||||
},
|
||||
@ -144,7 +166,7 @@ class TracksTableView extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
...tracks.asMap().entries.map((track) {
|
||||
...sortedTracks.asMap().entries.map((track) {
|
||||
String duration =
|
||||
"${track.value.duration?.inMinutes.remainder(60)}:${PrimitiveUtils.zeroPadNumStr(track.value.duration?.inSeconds.remainder(60) ?? 0)}";
|
||||
return InkWell(
|
||||
|
@ -173,8 +173,8 @@ class _SpotubeState extends ConsumerState<Spotube> with WidgetsBindingObserver {
|
||||
localStorage!.setString(
|
||||
LocalStorageKeys.windowSizeInfo,
|
||||
jsonEncode({
|
||||
'width': appWindow.isMaximized ? 0 : appWindow.size.width,
|
||||
'height': appWindow.isMaximized ? 0 : appWindow.size.height,
|
||||
'width': appWindow.isMaximized ? 0.0 : appWindow.size.width,
|
||||
'height': appWindow.isMaximized ? 0.0 : appWindow.size.height,
|
||||
}),
|
||||
);
|
||||
prevSize = appWindow.size;
|
||||
|
@ -6,6 +6,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:html/dom.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotify/spotify.dart';
|
||||
import 'package:spotube/components/Library/UserLocalTracks.dart';
|
||||
import 'package:spotube/models/LocalStorageKeys.dart';
|
||||
import 'package:spotube/models/Logger.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
@ -393,4 +394,31 @@ abstract class ServiceUtils {
|
||||
static void navigate(BuildContext context, String location, {Object? extra}) {
|
||||
GoRouter.of(context).push(location, extra: extra);
|
||||
}
|
||||
|
||||
static List<T> sortTracks<T extends Track>(List<T> tracks, SortBy sortBy) {
|
||||
if (sortBy == SortBy.none) return tracks;
|
||||
return List<T>.from(tracks)
|
||||
..sort((a, b) {
|
||||
switch (sortBy) {
|
||||
case SortBy.album:
|
||||
return a.album?.name?.compareTo(b.album?.name ?? "") ?? 0;
|
||||
case SortBy.artist:
|
||||
return a.artists?.first.name
|
||||
?.compareTo(b.artists?.first.name ?? "") ??
|
||||
0;
|
||||
case SortBy.ascending:
|
||||
return a.name?.compareTo(b.name ?? "") ?? 0;
|
||||
case SortBy.dateAdded:
|
||||
final aDate =
|
||||
double.parse(a.album?.releaseDate?.split("-").first ?? "2069");
|
||||
final bDate =
|
||||
double.parse(b.album?.releaseDate?.split("-").first ?? "2069");
|
||||
return aDate.compareTo(bDate);
|
||||
case SortBy.descending:
|
||||
return b.name?.compareTo(a.name ?? "") ?? 0;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user