feat: sort tracks in playlist, album and local tracks

This commit is contained in:
Kingkor Roy Tirtho 2022-10-13 18:59:30 +06:00
parent 91d5d1003b
commit cb4bd25df1
7 changed files with 174 additions and 29 deletions

View File

@ -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();

View File

@ -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,
);
},

View File

@ -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 {

View 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),
);
}
}

View File

@ -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(

View File

@ -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;

View File

@ -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;
}
});
}
}