From 1310d16597699980c57949e60e43bcedb9dab04a Mon Sep 17 00:00:00 2001 From: silentrald Date: Wed, 2 Oct 2024 21:27:30 +0800 Subject: [PATCH] [feat-216] support for sorting maps/songs --- assets/jsons/translations/de.json | 9 +++ assets/jsons/translations/en.json | 9 +++ assets/jsons/translations/es.json | 9 +++ assets/jsons/translations/fr.json | 9 +++ assets/jsons/translations/ja.json | 9 +++ assets/jsons/translations/ru.json | 9 +++ assets/jsons/translations/zh-tw.json | 9 +++ assets/jsons/translations/zh.json | 9 +++ .../maps-playlists-panel.component.tsx | 53 ++++++++++++++- .../maps/local-maps-list-panel.component.tsx | 17 ++++- src/renderer/services/maps/sorter.service.ts | 65 +++++++++++++++++++ src/shared/models/generics.type.ts | 3 + src/shared/models/maps/beat-saver.model.ts | 7 ++ 13 files changed, 212 insertions(+), 5 deletions(-) create mode 100644 src/renderer/services/maps/sorter.service.ts create mode 100644 src/shared/models/generics.type.ts diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index 5f662c4e..203fc4b7 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -104,6 +104,15 @@ "subtext": "Ziehe deine \".bplist\" oder \".json\" Dateien hierhin, um sie zu importieren" } } + }, + "sort": { + "name": "Name", + "song-author": "Song Author", + "map-author": "Map Author", + "bpm": "BPM", + "duration": "Duration", + "likes": "Likes", + "date-uploaded": "Date Uploaded" } }, "mods": { diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 9deaba07..f332853c 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -104,6 +104,15 @@ "subtext": "Drop your \".bplist\" or \".json\" files here to import them" } } + }, + "sort": { + "name": "Name", + "song-author": "Song Author", + "map-author": "Map Author", + "bpm": "BPM", + "duration": "Duration", + "likes": "Likes", + "date-uploaded": "Date Uploaded" } }, "mods": { diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index 528e4939..9c801685 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -104,6 +104,15 @@ "subtext": "Suelta tus archivos \".bplist\" o \".json\" aquí para importarlos" } } + }, + "sort": { + "name": "Name", + "song-author": "Song Author", + "map-author": "Map Author", + "bpm": "BPM", + "duration": "Duration", + "likes": "Likes", + "date-uploaded": "Date Uploaded" } }, "mods": { diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index e0e3c769..44f3c3eb 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -104,6 +104,15 @@ "subtext": "Déposez vos fichiers \".bplist\" ou \".json\" ici pour les importer" } } + }, + "sort": { + "name": "Name", + "song-author": "Song Author", + "map-author": "Map Author", + "bpm": "BPM", + "duration": "Duration", + "likes": "Likes", + "date-uploaded": "Date Uploaded" } }, "mods": { diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index 61ffe3a7..29f2a3f7 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -104,6 +104,15 @@ "subtext": "\".bplist\" または \".json\" ファイルをここにドロップしてインポート" } } + }, + "sort": { + "name": "Name", + "song-author": "Song Author", + "map-author": "Map Author", + "bpm": "BPM", + "duration": "Duration", + "likes": "Likes", + "date-uploaded": "Date Uploaded" } }, "mods": { diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index 651e11a9..019e8e97 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -104,6 +104,15 @@ "subtext": "Перетащите сюда файлы \".bplist\" или \".json\" для их импорта" } } + }, + "sort": { + "name": "Name", + "song-author": "Song Author", + "map-author": "Map Author", + "bpm": "BPM", + "duration": "Duration", + "likes": "Likes", + "date-uploaded": "Date Uploaded" } }, "mods": { diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index 668517a5..08867a1e 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -104,6 +104,15 @@ "subtext": "將您的 \".bplist\" 或 \".json\" 檔案拖曳到這裡進行匯入" } } + }, + "sort": { + "name": "Name", + "song-author": "Song Author", + "map-author": "Map Author", + "bpm": "BPM", + "duration": "Duration", + "likes": "Likes", + "date-uploaded": "Date Uploaded" } }, "mods": { diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index ace849b7..f7f01108 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -104,6 +104,15 @@ "subtext": "将您的 \".bplist\" 或 \".json\" 文件拖放到这里进行导入" } } + }, + "sort": { + "name": "Name", + "song-author": "Song Author", + "map-author": "Map Author", + "bpm": "BPM", + "duration": "Duration", + "likes": "Likes", + "date-uploaded": "Date Uploaded" } }, "mods": { diff --git a/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx index 31308119..26c9764b 100644 --- a/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx @@ -3,7 +3,7 @@ import { BSVersion } from "shared/bs-version.interface"; import { LocalMapsListPanel, LocalMapsListPanelRef } from "./maps/local-maps-list-panel.component"; import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component"; import { FilterPanel } from "./maps/filter-panel.component"; -import { MapFilter } from "shared/models/maps/beat-saver.model"; +import { MapFilter, MapSort } from "shared/models/maps/beat-saver.model"; import { MapsManagerService } from "renderer/services/maps-manager.service"; import { MapsDownloaderService } from "renderer/services/maps-downloader.service"; import { useTranslation } from "renderer/hooks/use-translation.hook"; @@ -24,6 +24,9 @@ import { LocalPlaylistFilter, LocalPlaylistFilterPanel } from "./playlists/local import { noop } from "shared/helpers/function.helpers"; import { Dropzone } from "../shared/dropzone.component"; import { logRenderError } from "renderer"; +import { BsmSelect, BsmSelectOption } from "../shared/bsm-select.component"; +import { MapsSorterService } from "renderer/services/maps/sorter.service"; +import { BsmButton } from "../shared/bsm-button.component"; type Props = { readonly version?: BSVersion; @@ -41,6 +44,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { const mapsManager = useService(MapsManagerService); const mapsDownloader = useService(MapsDownloaderService); + const mapsSorter = useService(MapsSorterService); const playlistsManager = useService(PlaylistsManagerService); const playlistsDownloader = useService(PlaylistDownloaderService); @@ -64,6 +68,10 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { const playlistsRef = useRef(); const [mapFilter, setMapFilter] = useState({}); + const [mapSort, setMapSort] = useState({ + compare: mapsSorter.getDefaultComparator(), + ascending: true, + }); const [playlistFilter, setPlaylistFilter] = useState({}); const [search, setSearch] = useState(""); @@ -135,6 +143,20 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { ]; })(); + const sortOptions: BsmSelectOption[] = useConstant(() => { + return mapsSorter.getComparatorKeys().map(key => ({ + text: `pages.version-viewer.maps.sort.${key}`, + value: key, + })); + }); + + const handleSortChange = (key: string) => { + setMapSort({ + compare: mapsSorter.getComparator(key), + ascending: mapSort.ascending, + }); + } + const dropDownItems = ((): DropDownItem[] => { if (tabIndex === 0) { return [ @@ -188,6 +210,24 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { ) )} + + setMapSort({ + compare: mapSort.compare, + ascending: !mapSort.ascending, + })} + /> - + void; } -export const LocalMapsListPanel = forwardRef(({ version, className, filter, search, linkedState, isActive }, forwardRef) => { +export const LocalMapsListPanel = forwardRef(({ + version, + className, + filter, + search, + linkedState, + isActive, + sort, +}, forwardRef) => { const mapsManager = useService(MapsManagerService); const mapsDownloader = useService(MapsDownloaderService); @@ -204,7 +213,9 @@ export const LocalMapsListPanel = forwardRef(({ ve }, [version]); const preppedMaps: RenderableMap[] = (() => { - return renderableMaps?.filter(renderableMap => isLocalMapFitMapFilter({ map: renderableMap.map, filter, search })) ?? []; + return (renderableMaps?.filter(renderableMap => isLocalMapFitMapFilter({ map: renderableMap.map, filter, search })) ?? []) + // NOTE: might be good to cache sorted maps, to reduce loading + .sort((map1, map2) => sort.compare(map1.map, map2.map) * (sort.ascending === false ? -1 : 1)); })(); if (!maps) { diff --git a/src/renderer/services/maps/sorter.service.ts b/src/renderer/services/maps/sorter.service.ts new file mode 100644 index 00000000..14330b40 --- /dev/null +++ b/src/renderer/services/maps/sorter.service.ts @@ -0,0 +1,65 @@ +import { Comparator } from "shared/models/generics.type"; +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; + + +export class MapsSorterService { + private static instance: MapsSorterService; + + public static getInstance() { + if (!MapsSorterService.instance) { + MapsSorterService.instance = new MapsSorterService(); + } + return MapsSorterService.instance; + } + + private readonly comparators: { + [key: string]: Comparator; + } = { + name: (map1, map2) => + map1.rawInfo._songName.localeCompare(map2.rawInfo._songName), + "song-author": (map1, map2) => + map1.rawInfo._songAuthorName.localeCompare(map2.rawInfo._songAuthorName), + "map-author": (map1, map2) => + map1.rawInfo._levelAuthorName.localeCompare(map2.rawInfo._levelAuthorName), + bpm: (map1, map2) => + map1.rawInfo._beatsPerMinute - map2.rawInfo._beatsPerMinute, + duration: (map1, map2) => { + if (!map1.songDetails) { + return map2.songDetails ? -1 : 0; + } + + return !map2.songDetails ? -1 + : map1.songDetails.duration - map2.songDetails.duration; + }, + likes: (map1, map2) => { + if (!map1.songDetails) { + return map2.songDetails ? -1 : 0; + } + + return !map2.songDetails ? -1 + : map1.songDetails.upVotes - map2.songDetails.upVotes; + }, + "date-uploaded": (map1, map2) => { + if (!map1.songDetails) { + return map2.songDetails ? -1 : 0; + } + + return !map2.songDetails ? -1 + : map1.songDetails.uploadedAt - map2.songDetails.uploadedAt; + } + }; + + public getComparatorKeys(): string[] { + return Object.keys(this.comparators); + } + + public getDefaultComparator(): Comparator { + return this.comparators.name; + } + + public getComparator(key: string): Comparator { + const comparator = this.comparators[key]; + return comparator || this.comparators.name; + } + +} diff --git a/src/shared/models/generics.type.ts b/src/shared/models/generics.type.ts new file mode 100644 index 00000000..33ad62f4 --- /dev/null +++ b/src/shared/models/generics.type.ts @@ -0,0 +1,3 @@ + +export type Comparator = (object1: T, object2: T) => number; + diff --git a/src/shared/models/maps/beat-saver.model.ts b/src/shared/models/maps/beat-saver.model.ts index dc0ed9e7..d08bc525 100644 --- a/src/shared/models/maps/beat-saver.model.ts +++ b/src/shared/models/maps/beat-saver.model.ts @@ -1,5 +1,7 @@ import { ObjectValues } from "shared/helpers/type.helpers"; import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model"; +import { BsmLocalMap } from "./bsm-local-map.interface"; +import { Comparator } from "../generics.type"; export interface BsvMapDetail { automapper: boolean; @@ -250,6 +252,11 @@ export interface MapFilter { excludedTags?: Set; } +export interface MapSort { + compare: Comparator; + ascending: boolean; +} + export interface SearchResponse { docs: BsvMapDetail[]; redirect: string;