From 062378b4a83cb11f352ada3c742796acdb3457e0 Mon Sep 17 00:00:00 2001 From: MSOB7YY Date: Mon, 30 Dec 2024 01:36:16 +0200 Subject: [PATCH] feat: sort playlist tracks for local & yt playlists --- lib/controller/playlist_controller.dart | 52 ++++-- lib/controller/search_sort_controller.dart | 57 ++++--- lib/core/enums.dart | 12 ++ lib/core/functions.dart | 156 ++++++++++++++---- lib/core/namida_converter_ext.dart | 146 +++++++++------- lib/core/translations/keys.dart | 2 + lib/packages/miniplayer.dart | 12 +- lib/packages/miniplayer_base.dart | 12 +- .../subpages/playlist_tracks_subpage.dart | 8 +- lib/ui/widgets/custom_widgets.dart | 37 +++++ .../youtube_playlist_controller.dart | 55 +++++- lib/youtube/pages/yt_playlist_subpage.dart | 18 +- lib/youtube/youtube_playlists_view.dart | 3 +- pubspec.yaml | 2 +- 14 files changed, 413 insertions(+), 159 deletions(-) diff --git a/lib/controller/playlist_controller.dart b/lib/controller/playlist_controller.dart index 5ee5e1225..120de708d 100644 --- a/lib/controller/playlist_controller.dart +++ b/lib/controller/playlist_controller.dart @@ -24,9 +24,9 @@ import 'package:namida/core/translations/language.dart'; import 'package:namida/core/utils.dart'; import 'package:namida/ui/widgets/custom_widgets.dart'; -typedef LocalPlaylist = GeneralPlaylist; +typedef LocalPlaylist = GeneralPlaylist; -class PlaylistController extends PlaylistManager { +class PlaylistController extends PlaylistManager { static PlaylistController get inst => _instance; static final PlaylistController _instance = PlaylistController._internal(); PlaylistController._internal(); @@ -34,9 +34,6 @@ class PlaylistController extends PlaylistManager { @override Track identifyBy(TrackWithDate item) => item.track; - final canReorderTracks = false.obs; - void resetCanReorder() => canReorderTracks.value = false; - void addNewPlaylist(String name, {List tracks = const [], int? creationDate, @@ -510,13 +507,16 @@ class PlaylistController extends PlaylistManager { Map itemToJson(TrackWithDate item) => item.toJson(); @override - bool canRemovePlaylist(GeneralPlaylist playlist) { + dynamic sortToJson(List items) => items.map((e) => e.name).toList(); + + @override + bool canRemovePlaylist(LocalPlaylist playlist) { _popPageIfCurrent(() => playlist.name); return true; } @override - void onPlaylistRemovedFromMap(GeneralPlaylist playlist) { + void onPlaylistRemovedFromMap(LocalPlaylist playlist) { final plIndex = SearchSortController.inst.playlistSearchList.value.indexWhere((element) => playlist.name == element); if (plIndex > -1) SearchSortController.inst.playlistSearchList.removeAt(plIndex); } @@ -532,19 +532,42 @@ class PlaylistController extends PlaylistManager { } @override - Future>> prepareAllPlaylistsFunction() async { + void onPlaylistItemsSort(List sorts, bool reverse, List items) { + final comparables = Function(TrackWithDate tr)>[]; + for (final s in sorts) { + if (s == SortType.dateAdded) { + Comparable comparable(TrackWithDate e) => e.dateAddedMS; + comparables.add(comparable); + } else { + final comparable = SearchSortController.inst.getTracksSortingComparables(s); + if (comparable != null) { + Comparable comparabletwd(TrackWithDate twd) => comparable(twd.track); + comparables.add(comparabletwd); + } + } + } + + if (reverse) { + items.sortByReverseAlts(comparables); + } else { + items.sortByAlts(comparables); + } + } + + @override + Future> prepareAllPlaylistsFunction() async { return await _readPlaylistFilesCompute.thready(playlistsDirectory); } @override - Future?> prepareFavouritePlaylistFunction() { + Future prepareFavouritePlaylistFunction() { return _prepareFavouritesFile.thready(favouritePlaylistPath); } static LocalPlaylist? _prepareFavouritesFile(String path) { try { final response = File(path).readAsJsonSync(); - return LocalPlaylist.fromJson(response, TrackWithDate.fromJson); + return LocalPlaylist.fromJson(response, TrackWithDate.fromJson, sortFromJson); } catch (_) {} return null; } @@ -558,11 +581,18 @@ class PlaylistController extends PlaylistManager { if (f is File) { try { final response = f.readAsJsonSync(); - final pl = LocalPlaylist.fromJson(response, TrackWithDate.fromJson); + final pl = LocalPlaylist.fromJson(response, TrackWithDate.fromJson, sortFromJson); map[pl.name] = pl; } catch (_) {} } } return map; } + + static List? sortFromJson(dynamic value) { + try { + return (value as List).map((e) => SortType.values.getEnum(e)!).toList(); + } catch (_) {} + return null; + } } diff --git a/lib/controller/search_sort_controller.dart b/lib/controller/search_sort_controller.dart index 7728f1bd8..bfee80fe5 100644 --- a/lib/controller/search_sort_controller.dart +++ b/lib/controller/search_sort_controller.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'dart:isolate'; import 'package:intl/intl.dart'; -import 'package:playlist_manager/playlist_manager.dart'; import 'package:namida/base/ports_provider.dart'; import 'package:namida/class/folder.dart'; @@ -129,34 +128,38 @@ class SearchSortController { } } - late final _mediaTracksSortingComparables = { - SortType.title: (e) => e.title.toLowerCase(), - SortType.album: (e) => e.album.toLowerCase(), - SortType.albumArtist: (e) => e.albumArtist.toLowerCase(), - SortType.year: (e) => e.yearPreferyyyyMMdd, - SortType.artistsList: (e) => e.artistsList.join().toLowerCase(), - SortType.genresList: (e) => e.genresList.join().toLowerCase(), - SortType.dateAdded: (e) => e.dateAdded, - SortType.dateModified: (e) => e.dateModified, - SortType.bitrate: (e) => e.bitrate, - SortType.composer: (e) => e.composer.toLowerCase(), - SortType.trackNo: (e) => e.trackNo, - SortType.discNo: (e) => e.discNo, - SortType.filename: (e) => e.filename.toLowerCase(), - SortType.duration: (e) => e.durationMS, - SortType.sampleRate: (e) => e.sampleRate, - SortType.size: (e) => e.size, - SortType.rating: (e) => e.effectiveRating, - SortType.mostPlayed: (e) => HistoryController.inst.topTracksMapListens.value[e]?.length ?? 0, - SortType.latestPlayed: (e) => HistoryController.inst.topTracksMapListens.value[e]?.lastOrNull ?? 0, - SortType.firstListen: (e) => HistoryController.inst.topTracksMapListens.value[e]?.firstOrNull ?? 0, - }; + Comparable Function(Track e)? getTracksSortingComparables(SortType type) { + return switch (type) { + SortType.title => (e) => e.title.toLowerCase(), + SortType.album => (e) => e.album.toLowerCase(), + SortType.albumArtist => (e) => e.albumArtist.toLowerCase(), + SortType.year => (e) => e.yearPreferyyyyMMdd, + SortType.artistsList => (e) => e.artistsList.join().toLowerCase(), + SortType.genresList => (e) => e.genresList.join().toLowerCase(), + SortType.dateAdded => (e) => e.dateAdded, + SortType.dateModified => (e) => e.dateModified, + SortType.bitrate => (e) => e.bitrate, + SortType.composer => (e) => e.composer.toLowerCase(), + SortType.trackNo => (e) => e.trackNo, + SortType.discNo => (e) => e.discNo, + SortType.filename => (e) => e.filename.toLowerCase(), + SortType.duration => (e) => e.durationMS, + SortType.sampleRate => (e) => e.sampleRate, + SortType.size => (e) => e.size, + SortType.rating => (e) => e.effectiveRating, + SortType.mostPlayed => (e) => HistoryController.inst.topTracksMapListens.value[e]?.length ?? 0, + SortType.latestPlayed => (e) => HistoryController.inst.topTracksMapListens.value[e]?.lastOrNull ?? 0, + SortType.firstListen => (e) => HistoryController.inst.topTracksMapListens.value[e]?.firstOrNull ?? 0, + SortType.shuffle => null, + }; + } List getMediaTracksSortingComparables(MediaType media) { final sorts = settings.mediaItemsTrackSorting.value[media] ?? [SortType.title]; final l = []; sorts.loop((e) { - if (_mediaTracksSortingComparables[e] != null) l.add(_mediaTracksSortingComparables[e]!); + final sorter = getTracksSortingComparables(e); + if (sorter != null) l.add(sorter); }); return l; } @@ -274,7 +277,7 @@ class SearchSortController { }, isolateFunction: (itemsSendPort) async { final params = { - 'playlists': playlistsMap.value.values.map((e) => e.toJson((item) => item.toJson())).toList(), + 'playlists': playlistsMap.value.values.map((e) => e.toJson((item) => item.toJson(), PlaylistController.inst.sortToJson)).toList(), 'translations': { 'k_PLAYLIST_NAME_AUTO_GENERATED': lang.AUTO_GENERATED, 'k_PLAYLIST_NAME_FAV': lang.FAVOURITES, @@ -533,14 +536,14 @@ class SearchSortController { final formatDate = DateFormat('yyyyMMdd'); final playlists = <({ - GeneralPlaylist pl, + LocalPlaylist pl, String name, String dateCreatedFormatted, String dateModifiedFormatted, })>[]; for (int i = 0; i < playlistsMap.length; i++) { var plMap = playlistsMap[i]; - final pl = GeneralPlaylist.fromJson(plMap, (itemJson) => TrackWithDate.fromJson(itemJson)); + final pl = LocalPlaylist.fromJson(plMap, (itemJson) => TrackWithDate.fromJson(itemJson), PlaylistController.sortFromJson); final trName = translatePlName(pl.name); final dateCreatedFormatted = formatDate.format(DateTime.fromMillisecondsSinceEpoch(pl.creationDate)); final dateModifiedFormatted = formatDate.format(DateTime.fromMillisecondsSinceEpoch(pl.modifiedDate)); diff --git a/lib/core/enums.dart b/lib/core/enums.dart index b11ba9e50..39f8b1204 100644 --- a/lib/core/enums.dart +++ b/lib/core/enums.dart @@ -431,3 +431,15 @@ enum CacheVideoPriority { low, GETOUT, } + +enum YTSortType { + title, + channelTitle, + duration, + date, + dateAdded, + shuffle, + mostPlayed, + latestPlayed, + firstListen, +} diff --git a/lib/core/functions.dart b/lib/core/functions.dart index bd0493f48..d45ec3ad9 100644 --- a/lib/core/functions.dart +++ b/lib/core/functions.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:calendar_date_picker2/calendar_date_picker2.dart'; @@ -211,18 +212,124 @@ class NamidaOnTaps { } void onSubPageTracksSortIconTap(MediaType media) { - final sorters = (settings.mediaItemsTrackSorting.value[media] ?? []).obs; + const defaultSorts = >{ + MediaType.album: [SortType.trackNo, SortType.year, SortType.title], + MediaType.artist: [SortType.year, SortType.title], + MediaType.genre: [SortType.year, SortType.title], + MediaType.folder: [SortType.filename], + MediaType.folderVideo: [SortType.filename], + }; + return _onSubPageSortIconTap( + minimumItems: 1, + allSortsList: List.from(SortType.values), + sortToText: (sort) => sort.toText(), + defaultSorts: defaultSorts[media] ?? [SortType.year], + currentSorts: settings.mediaItemsTrackSorting.value[media] ?? [], + currentReverse: settings.mediaItemsTrackSortingReverse.value[media] ?? false, + allowCustom: false, + onSortChange: (activeSorters) { + settings.updateMediaItemsTrackSorting(media, activeSorters); + }, + onSortReverseChange: (reverse) { + settings.updateMediaItemsTrackSortingReverse(media, reverse); + }, + onDone: () { + Indexer.inst.sortMediaTracksSubLists([media]); + }, + ); + } + + void onPlaylistSubPageTracksSortIconTap( + String playlistName, + PlaylistManager playlistManager, + List allSorts, + String Function(S sort) sortToText, + ) { + final initialpl = playlistManager.getPlaylist(playlistName); + List? newSorts; + bool? newSortReverse; + + void onFinalUpdatePropertySort(List sorts, bool? reverse) { + playlistManager.updatePropertyInPlaylist(playlistName, itemsSortType: sorts, itemsSortReverse: reverse); + playlistManager.resetCanReorder(); + } + + return _onSubPageSortIconTap( + minimumItems: 0, + defaultSorts: [], + allSortsList: List.from(allSorts), + sortToText: sortToText, + currentSorts: initialpl?.sortsType ?? [], + currentReverse: initialpl?.sortReverse ?? false, + allowCustom: false, + onSortChange: (activeSorters) { + newSorts = activeSorters; + }, + onSortReverseChange: (reverse) { + newSortReverse = reverse; + final pl = playlistManager.getPlaylist(playlistName); + if (pl != null && pl.sortReverse != reverse) { + playlistManager.updatePropertyInPlaylist(playlistName, itemsSortReverse: reverse); + } + }, + onDone: () { + final pl = playlistManager.getPlaylist(playlistName); + if (pl != null && newSorts != null) { + if (!listEquals(pl.sortsType, newSorts)) { + if ((pl.sortsType?.isEmpty ?? true) && newSorts!.isNotEmpty) { + NamidaNavigator.inst.navigateDialog( + dialog: CustomBlurryDialog( + isWarning: true, + normalTitleStyle: true, + bodyText: lang.YOUR_CUSTOM_ORDER_WILL_BE_LOST, + actions: [ + const CancelButton(), + NamidaButton( + text: lang.CONFIRM, + onPressed: () { + onFinalUpdatePropertySort(newSorts!, newSortReverse); + NamidaNavigator.inst.closeDialog(); + }, + ) + ], + ), + ); + } else { + onFinalUpdatePropertySort(newSorts!, newSortReverse); + } + } + } + }, + ); + } + + void _onSubPageSortIconTap({ + required List currentSorts, + required List allSortsList, + required bool currentReverse, + required List defaultSorts, + required int minimumItems, + required String Function(S sort) sortToText, + required bool allowCustom, + required void Function(List activeSorters) onSortChange, + required void Function(bool reverse) onSortReverseChange, + required void Function() onDone, + }) { + final sorters = List.from(currentSorts).obs; + final isReverse = currentReverse.obs; + + final allSorts = allSortsList.obs; - final allSorts = List.from(SortType.values).obs; void resortVisualItems() => allSorts.sortByReverse((e) { final active = sorters.contains(e); return active ? sorters.length - sorters.value.indexOf(e) : sorters.value.indexOf(e); }); + resortVisualItems(); void resortMedia() { - settings.updateMediaItemsTrackSorting(media, sorters.value); - Indexer.inst.sortMediaTracksSubLists([media]); + onSortChange(sorters.value); + onDone(); } NamidaNavigator.inst.navigateDialog( @@ -239,16 +346,8 @@ class NamidaOnTaps { icon: const Icon(Broken.refresh), tooltip: lang.RESTORE_DEFAULTS, onPressed: () { - final defaultSorts = >{ - MediaType.album: [SortType.trackNo, SortType.year, SortType.title], - MediaType.artist: [SortType.year, SortType.title], - MediaType.genre: [SortType.year, SortType.title], - MediaType.folder: [SortType.filename], - MediaType.folderVideo: [SortType.filename], - }; - final defaults = defaultSorts[media] ?? [SortType.year]; - sorters.value = defaults; - settings.updateMediaItemsTrackSorting(media, defaults); + sorters.value = defaultSorts; + onSortChange(defaultSorts); }, ), DoneButton(additional: resortMedia), @@ -258,17 +357,16 @@ class NamidaOnTaps { height: namida.height * 0.4, child: Column( children: [ - Obx( - (context) { - final currentlyReverse = settings.mediaItemsTrackSortingReverse.valueR[media] ?? false; - return ListTileWithCheckMark( - title: lang.REVERSE_ORDER, - active: currentlyReverse, - onTap: () { - settings.updateMediaItemsTrackSortingReverse(media, !currentlyReverse); - }, - ); - }, + ObxO( + rx: isReverse, + builder: (context, reverse) => ListTileWithCheckMark( + title: lang.REVERSE_ORDER, + active: reverse, + onTap: () { + onSortReverseChange(!reverse); + isReverse.value = !reverse; + }, + ), ), const SizedBox(height: 12.0), Expanded( @@ -284,7 +382,7 @@ class NamidaOnTaps { final activeSorts = allSorts.where((element) => sorters.contains(element)).toList(); sorters.value = activeSorts; - settings.updateMediaItemsTrackSorting(media, activeSorts); + onSortChange(activeSorts); }, itemBuilder: (context, i) { final sorting = allSorts[i]; @@ -295,11 +393,11 @@ class NamidaOnTaps { (context) { final isActive = sorters.contains(sorting); return ListTileWithCheckMark( - title: "${i + 1}. ${sorting.toText()}", + title: "${i + 1}. ${sortToText(sorting)}", active: isActive, onTap: () { - if (isActive && sorters.length <= 1) { - showMinimumItemsSnack(); + if (isActive && sorters.length <= minimumItems) { + showMinimumItemsSnack(minimumItems); return; } if (sorters.contains(sorting)) { diff --git a/lib/core/namida_converter_ext.dart b/lib/core/namida_converter_ext.dart index 47b752e01..80d461f83 100644 --- a/lib/core/namida_converter_ext.dart +++ b/lib/core/namida_converter_ext.dart @@ -142,6 +142,10 @@ extension SortToText on SortType { String toText() => _NamidaConverters.inst.getTitle(this); } +extension YTSortToText on YTSortType { + String toText() => _NamidaConverters.inst.getTitle(this); +} + extension CacheVideoPriorityToText on CacheVideoPriority { String toText() => _NamidaConverters.inst.getTitle(this); } @@ -825,6 +829,15 @@ extension RouteUtils on NamidaRoute { null; } + final showMainMenu = route == RouteType.SUBPAGE_albumTracks || + route == RouteType.SUBPAGE_artistTracks || + route == RouteType.SUBPAGE_albumArtistTracks || + route == RouteType.SUBPAGE_composerTracks || + route == RouteType.SUBPAGE_genreTracks || + route == RouteType.SUBPAGE_queueTracks; + + final showPlaylistMenu = route == RouteType.SUBPAGE_playlistTracks || route == RouteType.SUBPAGE_historyTracks || route == RouteType.SUBPAGE_mostPlayedTracks; + return [ _getAnimatedCrossFade( child: NamidaAppBarIcon( @@ -865,41 +878,29 @@ extension RouteUtils on NamidaRoute { ), shouldShow: sortingTracksMediaType != null, ), - - if (name != null) - _getAnimatedCrossFade( - child: _getMoreIcon(() { - switch (route) { - case RouteType.SUBPAGE_albumTracks: - NamidaDialogs.inst.showAlbumDialog(name); - break; - case RouteType.SUBPAGE_artistTracks: - NamidaDialogs.inst.showArtistDialog(name, MediaType.artist); - break; - case RouteType.SUBPAGE_albumArtistTracks: - NamidaDialogs.inst.showArtistDialog(name, MediaType.albumArtist); - break; - case RouteType.SUBPAGE_composerTracks: - NamidaDialogs.inst.showArtistDialog(name, MediaType.composer); - break; - case RouteType.SUBPAGE_genreTracks: - NamidaDialogs.inst.showGenreDialog(name); - break; - case RouteType.SUBPAGE_queueTracks: - NamidaDialogs.inst.showQueueDialog(int.parse(name)); - break; - - default: - null; + _getAnimatedCrossFade( + child: NamidaAppBarIcon( + icon: Broken.sort, + onPressed: () { + if (route == RouteType.YOUTUBE_PLAYLIST_SUBPAGE) { + NamidaOnTaps.inst.onPlaylistSubPageTracksSortIconTap( + name ?? '', + ytplc.YoutubePlaylistController.inst, + YTSortType.values, + (sort) => sort.toText(), + ); + } else { + NamidaOnTaps.inst.onPlaylistSubPageTracksSortIconTap( + name ?? '', + PlaylistController.inst, + SortType.values, + (sort) => sort.toText(), + ); } - }), - shouldShow: route == RouteType.SUBPAGE_albumTracks || - route == RouteType.SUBPAGE_artistTracks || - route == RouteType.SUBPAGE_albumArtistTracks || - route == RouteType.SUBPAGE_composerTracks || - route == RouteType.SUBPAGE_genreTracks || - route == RouteType.SUBPAGE_queueTracks, + }, ), + shouldShow: route == RouteType.SUBPAGE_playlistTracks || route == RouteType.YOUTUBE_PLAYLIST_SUBPAGE, + ), _getAnimatedCrossFade( child: HistoryJumpToDayIcon( @@ -925,44 +926,66 @@ extension RouteUtils on NamidaRoute { // ---- Playlist Tracks ---- _getAnimatedCrossFade( - child: ObxO( - key: UniqueKey(), // i have no f idea why this happens.. namida ghosts are here again - rx: PlaylistController.inst.canReorderTracks, - builder: (context, reorderable) => NamidaAppBarIcon( - tooltip: () => PlaylistController.inst.canReorderTracks.value ? lang.DISABLE_REORDERING : lang.ENABLE_REORDERING, - icon: reorderable ? Broken.forward_item : Broken.lock_1, - onPressed: () => PlaylistController.inst.canReorderTracks.value = !PlaylistController.inst.canReorderTracks.value, - ), + child: EnableDisablePlaylistReordering( + playlistName: name ?? '', + playlistManager: PlaylistController.inst, ), shouldShow: route == RouteType.SUBPAGE_playlistTracks, ), - if (name != null) - _getAnimatedCrossFade( - child: _getMoreIcon(() { - NamidaDialogs.inst.showPlaylistDialog(name); - }), - shouldShow: route == RouteType.SUBPAGE_playlistTracks || route == RouteType.SUBPAGE_historyTracks || route == RouteType.SUBPAGE_mostPlayedTracks, - ), _getAnimatedCrossFade( - child: ObxO( - rx: ytplc.YoutubePlaylistController.inst.canReorderVideos, - builder: (context, reorderable) => NamidaAppBarIcon( - tooltip: () => ytplc.YoutubePlaylistController.inst.canReorderVideos.value ? lang.DISABLE_REORDERING : lang.ENABLE_REORDERING, - icon: reorderable ? Broken.forward_item : Broken.lock_1, - onPressed: () => ytplc.YoutubePlaylistController.inst.canReorderVideos.value = !ytplc.YoutubePlaylistController.inst.canReorderVideos.value, - ), + child: EnableDisablePlaylistReordering( + playlistName: name ?? '', + playlistManager: ytplc.YoutubePlaylistController.inst, ), shouldShow: route == RouteType.YOUTUBE_PLAYLIST_SUBPAGE, ), + _getAnimatedCrossFade( + child: _getMoreIcon(() { + if (name == null) return; + switch (route) { + case RouteType.SUBPAGE_albumTracks: + NamidaDialogs.inst.showAlbumDialog(name); + break; + case RouteType.SUBPAGE_artistTracks: + NamidaDialogs.inst.showArtistDialog(name, MediaType.artist); + break; + case RouteType.SUBPAGE_albumArtistTracks: + NamidaDialogs.inst.showArtistDialog(name, MediaType.albumArtist); + break; + case RouteType.SUBPAGE_composerTracks: + NamidaDialogs.inst.showArtistDialog(name, MediaType.composer); + break; + case RouteType.SUBPAGE_genreTracks: + NamidaDialogs.inst.showGenreDialog(name); + break; + case RouteType.SUBPAGE_queueTracks: + NamidaDialogs.inst.showQueueDialog(int.parse(name)); + break; + + default: + null; + } + }), + shouldShow: showMainMenu && name != null, + ), + + _getAnimatedCrossFade( + child: _getMoreIcon(() { + if (name == null) return; + NamidaDialogs.inst.showPlaylistDialog(name); + }), + shouldShow: showPlaylistMenu && name != null, + ), + // -- Settings Icon _getAnimatedCrossFade( child: NamidaAppBarIcon( icon: Broken.setting_2, onPressed: const SettingsPage().navigate, ), - shouldShow: shouldShowInitialActions, + shouldShow: !showMainMenu && !showPlaylistMenu && shouldShowInitialActions, ), const SizedBox(width: 8.0), @@ -1107,6 +1130,17 @@ class _NamidaConverters { SortType.latestPlayed: lang.RECENT_LISTENS, SortType.firstListen: lang.FIRST_LISTEN, }, + YTSortType: { + YTSortType.title: lang.TITLE, + YTSortType.channelTitle: lang.CHANNEL, + YTSortType.duration: lang.DURATION, + YTSortType.date: lang.DATE, + YTSortType.dateAdded: lang.DATE_ADDED, + YTSortType.shuffle: lang.SHUFFLE, + YTSortType.mostPlayed: lang.MOST_PLAYED, + YTSortType.latestPlayed: lang.RECENT_LISTENS, + YTSortType.firstListen: lang.FIRST_LISTEN, + }, CacheVideoPriority: { CacheVideoPriority.VIP: 'VIP', CacheVideoPriority.high: 'High', diff --git a/lib/core/translations/keys.dart b/lib/core/translations/keys.dart index 904d07dae..074be8e86 100644 --- a/lib/core/translations/keys.dart +++ b/lib/core/translations/keys.dart @@ -658,6 +658,7 @@ abstract class LanguageKeys { String get THEME_MODE => _getKey('THEME_MODE'); String get THEME_SETTINGS_SUBTITLE => _getKey('THEME_SETTINGS_SUBTITLE'); String get THEME_SETTINGS => _getKey('THEME_SETTINGS'); + String get THIS_PLAYLIST_HAS_ACTIVE_SORTERS_DISABLE_THEM_BEFORE_REORDERING => _getKey('THIS_PLAYLIST_HAS_ACTIVE_SORTERS_DISABLE_THEM_BEFORE_REORDERING'); String get THIS_VIDEO_IS_LIKELY_DELETED_OR_SET_TO_PRIVATE => _getKey('THIS_VIDEO_IS_LIKELY_DELETED_OR_SET_TO_PRIVATE'); String get THUMBNAILS => _getKey('THUMBNAILS'); String get TITLE => _getKey('TITLE'); @@ -738,6 +739,7 @@ abstract class LanguageKeys { String get YEAR => _getKey('YEAR'); String get YES => _getKey('YES'); String get YOUR_CURRENT_MEMBERSHIP_IS => _getKey('YOUR_CURRENT_MEMBERSHIP_IS'); + String get YOUR_CUSTOM_ORDER_WILL_BE_LOST => _getKey('YOUR_CUSTOM_ORDER_WILL_BE_LOST'); String get YOUTUBE_MUSIC => _getKey('YOUTUBE_MUSIC'); String get YOUTUBE => _getKey('YOUTUBE'); String get YOUTUBE_SETTINGS_SUBTITLE => _getKey('YOUTUBE_SETTINGS_SUBTITLE'); diff --git a/lib/packages/miniplayer.dart b/lib/packages/miniplayer.dart index 083b035a6..0cea2fe81 100644 --- a/lib/packages/miniplayer.dart +++ b/lib/packages/miniplayer.dart @@ -169,7 +169,9 @@ class NamidaMiniPlayerMixed extends StatelessWidget { return item is Selectable ? trackConfig.currentImageBuilder(item, bcp) : ytConfig.currentImageBuilder(item, bcp); }, textBuilder: (item) { - return item is Selectable ? trackConfig.textBuilder(item) as MiniplayerInfoData : ytConfig.textBuilder(item as YoutubeID) as MiniplayerInfoData; + return item is Selectable + ? trackConfig.textBuilder(item) as MiniplayerInfoData + : ytConfig.textBuilder(item as YoutubeID) as MiniplayerInfoData; }, canShowBuffering: (item) => item is Selectable ? trackConfig.canShowBuffering(item) : ytConfig.canShowBuffering(item), ); @@ -181,7 +183,7 @@ class NamidaMiniPlayerTrack extends StatelessWidget { void _openMenu(Track track) => NamidaDialogs.inst.showTrackDialog(track, source: QueueSource.playerQueue); - MiniplayerInfoData _textBuilder(Playable playable) { + MiniplayerInfoData _textBuilder(Playable playable) { String firstLine = ''; String secondLine = ''; @@ -215,7 +217,7 @@ class NamidaMiniPlayerTrack extends StatelessWidget { } NamidaMiniPlayerBase getMiniPlayerBase(BuildContext context) { - return NamidaMiniPlayerBase( + return NamidaMiniPlayerBase( queueItemExtent: Dimensions.inst.trackTileItemExtent, trackTileConfigs: const TrackTilePropertiesConfigs( displayRightDragHandler: true, @@ -421,7 +423,7 @@ class _NamidaMiniPlayerYoutubeIDState extends State { ); } - MiniplayerInfoData _textBuilder(BuildContext context, Playable playbale) { + MiniplayerInfoData _textBuilder(BuildContext context, Playable playbale) { final video = playbale as YoutubeID; String firstLine = ''; String secondLine = ''; @@ -448,7 +450,7 @@ class _NamidaMiniPlayerYoutubeIDState extends State { } NamidaMiniPlayerBase getMiniPlayerBase(BuildContext context) { - return NamidaMiniPlayerBase( + return NamidaMiniPlayerBase( queueItemExtent: Dimensions.youtubeCardItemExtent, itemBuilder: (context, i, currentIndex, queue, _) { final video = queue[i] as YoutubeID; diff --git a/lib/packages/miniplayer_base.dart b/lib/packages/miniplayer_base.dart index ea2bc227c..bb53851ad 100644 --- a/lib/packages/miniplayer_base.dart +++ b/lib/packages/miniplayer_base.dart @@ -70,10 +70,10 @@ class FocusedMenuOptions { }); } -class MiniplayerInfoData { +class MiniplayerInfoData { final String firstLine; final String secondLine; - final FavouritePlaylist favouritePlaylist; + final FavouritePlaylist favouritePlaylist; final E itemToLike; final Future Function(bool isLiked) onLikeTap; final void Function() onShowAddToPlaylistDialog; @@ -100,7 +100,7 @@ class MiniplayerInfoData { secondLineGood = secondLine.isNotEmpty; } -class NamidaMiniPlayerBase extends StatefulWidget { +class NamidaMiniPlayerBase extends StatefulWidget { final double? queueItemExtent; final double? Function(Playable item)? queueItemExtentBuilder; final (Widget, Key) Function(BuildContext context, int index, int currentIndex, List queue, TrackTileProperties? properties) itemBuilder; @@ -113,7 +113,7 @@ class NamidaMiniPlayerBase extends StatefulWidget { final FocusedMenuOptions Function(Playable item) focusedMenuOptions; final Widget Function(Playable item, double cp) imageBuilder; final Widget Function(Playable item, double bcp) currentImageBuilder; - final MiniplayerInfoData Function(Playable item) textBuilder; + final MiniplayerInfoData Function(Playable item) textBuilder; final bool Function(Playable item) canShowBuffering; final TrackTilePropertiesConfigs? trackTileConfigs; @@ -1328,8 +1328,8 @@ class _RawImageContainer extends StatelessWidget { } } -class _TrackInfo extends StatelessWidget { - final MiniplayerInfoData textData; +class _TrackInfo extends StatelessWidget { + final MiniplayerInfoData textData; final double cp; final double qp; final double p; diff --git a/lib/ui/pages/subpages/playlist_tracks_subpage.dart b/lib/ui/pages/subpages/playlist_tracks_subpage.dart index 6613841d5..381c2c842 100644 --- a/lib/ui/pages/subpages/playlist_tracks_subpage.dart +++ b/lib/ui/pages/subpages/playlist_tracks_subpage.dart @@ -408,7 +408,7 @@ class _NormalPlaylistTracksPageState extends State wit @override Widget build(BuildContext context) { final threeC = ObxO( - rx: PlaylistController.inst.canReorderTracks, + rx: PlaylistController.inst.canReorderItems, builder: (context, reorderable) => ThreeLineSmallContainers(enabled: reorderable), ); @@ -427,14 +427,14 @@ class _NormalPlaylistTracksPageState extends State wit final tracks = tracksWithDate.toTracks(); return ObxO( - rx: PlaylistController.inst.canReorderTracks, + rx: PlaylistController.inst.canReorderItems, builder: (context, reorderable) => TrackTilePropertiesProvider( configs: TrackTilePropertiesConfigs( queueSource: playlist.toQueueSource(), playlistName: playlist.name, draggableThumbnail: reorderable, horizontalGestures: !reorderable, - selectable: () => !PlaylistController.inst.canReorderTracks.value, + selectable: () => !PlaylistController.inst.canReorderItems.value, ), builder: (properties) => NamidaListViewRaw( scrollController: _scrollController, @@ -463,7 +463,7 @@ class _NormalPlaylistTracksPageState extends State wit return FadeDismissible( key: Key("Diss_$i$trackWithDate"), - draggableRx: PlaylistController.inst.canReorderTracks, + draggableRx: PlaylistController.inst.canReorderItems, onDismissed: (direction) => NamidaOnTaps.inst.onRemoveTracksFromPlaylist(playlist.name, [trackWithDate]), onTopWidget: Positioned( left: 0, diff --git a/lib/ui/widgets/custom_widgets.dart b/lib/ui/widgets/custom_widgets.dart index 4d79cc1a5..1f1d712cb 100644 --- a/lib/ui/widgets/custom_widgets.dart +++ b/lib/ui/widgets/custom_widgets.dart @@ -13,6 +13,7 @@ import 'package:flutter_scrollbar_modified/flutter_scrollbar_modified.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:history_manager/history_manager.dart'; import 'package:like_button/like_button.dart'; +import 'package:playlist_manager/playlist_manager.dart'; import 'package:selectable_autolink_text/selectable_autolink_text.dart'; import 'package:sleek_circular_slider/sleek_circular_slider.dart'; import 'package:wheel_slider/wheel_slider.dart'; @@ -4583,6 +4584,42 @@ class AnimatedThemeOrTheme extends StatelessWidget { } } +class EnableDisablePlaylistReordering extends StatelessWidget { + final String playlistName; + final PlaylistManager playlistManager; + + const EnableDisablePlaylistReordering({ + super.key, + required this.playlistName, + required this.playlistManager, + }); + + @override + Widget build(BuildContext context) { + return ObxO( + key: UniqueKey(), // i have no f idea why this happens.. namida ghosts are here again + rx: playlistManager.canReorderItems, + builder: (context, reorderable) => NamidaAppBarIcon( + tooltip: () => playlistManager.canReorderItems.value ? lang.DISABLE_REORDERING : lang.ENABLE_REORDERING, + icon: reorderable ? Broken.forward_item : Broken.lock_1, + onPressed: () { + final playlist = playlistManager.getPlaylist(playlistName); + if (playlist == null) return; + if (playlist.sortsType?.isNotEmpty ?? false) { + snackyy( + isError: true, + title: lang.WARNING, + message: lang.THIS_PLAYLIST_HAS_ACTIVE_SORTERS_DISABLE_THEM_BEFORE_REORDERING, + ); + return; + } + playlistManager.canReorderItems.value = !playlistManager.canReorderItems.value; + }, + ), + ); + } +} + class SetVideosPriorityChipController { // -- worst of my creations so far SetVideosPriorityChipController(); diff --git a/lib/youtube/controller/youtube_playlist_controller.dart b/lib/youtube/controller/youtube_playlist_controller.dart index 8838d8650..fb8c32e69 100644 --- a/lib/youtube/controller/youtube_playlist_controller.dart +++ b/lib/youtube/controller/youtube_playlist_controller.dart @@ -17,10 +17,12 @@ import 'package:namida/core/functions.dart'; import 'package:namida/core/translations/language.dart'; import 'package:namida/core/utils.dart'; import 'package:namida/youtube/class/youtube_id.dart'; +import 'package:namida/youtube/controller/youtube_history_controller.dart'; +import 'package:namida/youtube/controller/youtube_info_controller.dart'; -typedef YoutubePlaylist = GeneralPlaylist; +typedef YoutubePlaylist = GeneralPlaylist; -class YoutubePlaylistController extends PlaylistManager { +class YoutubePlaylistController extends PlaylistManager { static YoutubePlaylistController get inst => _instance; static final YoutubePlaylistController _instance = YoutubePlaylistController._internal(); YoutubePlaylistController._internal(); @@ -28,9 +30,6 @@ class YoutubePlaylistController extends PlaylistManager { @override String identifyBy(YoutubeID item) => item.id; - final canReorderVideos = false.obs; - void resetCanReorder() => canReorderVideos.value = false; - void addNewPlaylist( String name, { Iterable? videoIds, @@ -111,8 +110,7 @@ class YoutubePlaylistController extends PlaylistManager { reverse ??= settings.ytPlaylistSortReversed.value; final playlistList = playlistsMap.entries.toList(); - void sortThis(Comparable Function(MapEntry> p) comparable) => - reverse! ? playlistList.sortByReverse(comparable) : playlistList.sortBy(comparable); + void sortThis(Comparable Function(MapEntry p) comparable) => reverse! ? playlistList.sortByReverse(comparable) : playlistList.sortBy(comparable); switch (sortBy) { case GroupSortType.title: @@ -177,6 +175,9 @@ class YoutubePlaylistController extends PlaylistManager { @override Map itemToJson(YoutubeID item) => item.toJson(); + @override + dynamic sortToJson(List items) => items.map((e) => e.name).toList(); + @override String get favouritePlaylistPath => AppPaths.YT_LIKES_PLAYLIST; @@ -199,7 +200,7 @@ class YoutubePlaylistController extends PlaylistManager { static YoutubePlaylist? _prepareFavouritesFile(String path) { try { final response = File(path).readAsJsonSync(); - return YoutubePlaylist.fromJson(response, (itemJson) => YoutubeID.fromJson(itemJson)); + return YoutubePlaylist.fromJson(response, (itemJson) => YoutubeID.fromJson(itemJson), _sortFromJson); } catch (_) { return null; } @@ -214,7 +215,7 @@ class YoutubePlaylistController extends PlaylistManager { if (f is File) { try { final response = f.readAsJsonSync(); - final pl = YoutubePlaylist.fromJson(response, (itemJson) => YoutubeID.fromJson(itemJson)); + final pl = YoutubePlaylist.fromJson(response, (itemJson) => YoutubeID.fromJson(itemJson), _sortFromJson); map[pl.name] = pl; } catch (_) {} } @@ -224,4 +225,40 @@ class YoutubePlaylistController extends PlaylistManager { @override void sortPlaylists() => sortYTPlaylists(); + + static List? _sortFromJson(dynamic value) { + try { + return (value as List).map((e) => YTSortType.values.getEnum(e)!).toList(); + } catch (_) {} + return null; + } + + @override + void onPlaylistItemsSort(List sorts, bool reverse, List items) { + final comparables = Function(YoutubeID vid)>[]; + for (final s in sorts) { + final comparable = _mediaTracksSortingComparables(s); + if (comparable != null) comparables.add(comparable); + } + + if (reverse) { + items.sortByReverseAlts(comparables); + } else { + items.sortByAlts(comparables); + } + } + + Comparable Function(YoutubeID e)? _mediaTracksSortingComparables(YTSortType type) { + return switch (type) { + YTSortType.title => (e) => YoutubeInfoController.utils.getVideoName(e.id) ?? '', + YTSortType.channelTitle => (e) => YoutubeInfoController.utils.getVideoChannelName(e.id) ?? '', + YTSortType.duration => (e) => YoutubeInfoController.utils.getVideoDurationSeconds(e.id) ?? 0, + YTSortType.date => (e) => YoutubeInfoController.utils.getVideoReleaseDate(e.id) ?? DateTime(0), + YTSortType.dateAdded => (e) => e.dateAddedMS, + YTSortType.shuffle => null, + YTSortType.mostPlayed => (e) => YoutubeHistoryController.inst.topTracksMapListens.value[e.id]?.length ?? 0, + YTSortType.latestPlayed => (e) => YoutubeHistoryController.inst.topTracksMapListens.value[e.id]?.lastOrNull ?? 0, + YTSortType.firstListen => (e) => YoutubeHistoryController.inst.topTracksMapListens.value[e.id]?.firstOrNull ?? 0, + }; + } } diff --git a/lib/youtube/pages/yt_playlist_subpage.dart b/lib/youtube/pages/yt_playlist_subpage.dart index 3a1b1f2d4..1e39f7219 100644 --- a/lib/youtube/pages/yt_playlist_subpage.dart +++ b/lib/youtube/pages/yt_playlist_subpage.dart @@ -179,8 +179,8 @@ class _YTNormalPlaylistSubpageState extends State { final bigThumbWidth = maxWidth - horizontalBigThumbPadding * 2; Color? threeCColor; late final threeC = ObxO( - rx: YoutubePlaylistController.inst.canReorderVideos, - builder: (context, canReorderVideos) => ThreeLineSmallContainers(enabled: canReorderVideos, color: threeCColor), + rx: YoutubePlaylistController.inst.canReorderItems, + builder: (context, canReorderItems) => ThreeLineSmallContainers(enabled: canReorderItems, color: threeCColor), ); final theme = AppThemes.inst.getAppTheme(bgColor, !context.isDarkMode); return AnimatedThemeOrTheme( @@ -339,8 +339,8 @@ class _YTNormalPlaylistSubpageState extends State { ), const SliverPadding(padding: EdgeInsets.only(bottom: 24.0)), ObxO( - rx: YoutubePlaylistController.inst.canReorderVideos, - builder: (context, canReorderVideos) => NamidaSliverReorderableList( + rx: YoutubePlaylistController.inst.canReorderItems, + builder: (context, canReorderItems) => NamidaSliverReorderableList( onReorder: (oldIndex, newIndex) => YoutubePlaylistController.inst.reorderTrack(playlist, oldIndex, newIndex), itemExtent: Dimensions.youtubeCardItemExtent, itemCount: playlist.tracks.length, @@ -348,7 +348,7 @@ class _YTNormalPlaylistSubpageState extends State { final video = playlist.tracks[index]; return FadeDismissible( key: Key("Diss_$index$video"), - draggableRx: YoutubePlaylistController.inst.canReorderVideos, + draggableRx: YoutubePlaylistController.inst.canReorderItems, onDismissed: (direction) => YTUtils.onRemoveVideosFromPlaylist(playlist.name, [video]), child: YTHistoryVideoCard( key: ValueKey(index), @@ -360,8 +360,8 @@ class _YTNormalPlaylistSubpageState extends State { day: null, playlistID: playlist.playlistID, playlistName: playlistCurrentName, - draggingEnabled: canReorderVideos, - openMenuOnLongPress: !canReorderVideos, + draggingEnabled: canReorderItems, + openMenuOnLongPress: !canReorderItems, draggableThumbnail: true, showMoreIcon: true, draggingBarsBuilder: (color) { @@ -370,8 +370,8 @@ class _YTNormalPlaylistSubpageState extends State { }, draggingThumbnailBuilder: (draggingTrigger) { return ObxO( - rx: YoutubePlaylistController.inst.canReorderVideos, - builder: (context, canReorderVideos) => canReorderVideos ? draggingTrigger : const SizedBox(), + rx: YoutubePlaylistController.inst.canReorderItems, + builder: (context, canReorderItems) => canReorderItems ? draggingTrigger : const SizedBox(), ); }, canHaveDuplicates: true, diff --git a/lib/youtube/youtube_playlists_view.dart b/lib/youtube/youtube_playlists_view.dart index 0cdf39230..b1841fe60 100644 --- a/lib/youtube/youtube_playlists_view.dart +++ b/lib/youtube/youtube_playlists_view.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:jiffy/jiffy.dart'; import 'package:playlist_manager/module/playlist_id.dart'; -import 'package:playlist_manager/playlist_manager.dart'; import 'package:namida/class/route.dart'; import 'package:namida/controller/file_browser.dart'; @@ -54,7 +53,7 @@ class YoutubePlaylistsView extends StatelessWidget with NamidaRouteWidget { return videos.values; } - List getFavouriteVideos(GeneralPlaylist playlist) { + List getFavouriteVideos(YoutubePlaylist playlist) { final videos = []; final all = playlist.tracks; for (int i = all.length - 1; i >= 0; i--) { diff --git a/pubspec.yaml b/pubspec.yaml index 5e1f115bc..50b08d734 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: namida description: A Beautiful and Feature-rich Music Player, With YouTube & Video Support Built in Flutter publish_to: "none" -version: 4.8.48-beta+241229111 +version: 4.8.5-beta+241229235 environment: sdk: ">=3.4.0 <4.0.0"