[feat-216] support for sorting maps/songs

This commit is contained in:
silentrald
2024-10-02 21:27:30 +08:00
parent 30c6058edb
commit 1310d16597
13 changed files with 212 additions and 5 deletions
@@ -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<LocalPlaylistsListRef>();
const [mapFilter, setMapFilter] = useState<MapFilter>({});
const [mapSort, setMapSort] = useState<MapSort>({
compare: mapsSorter.getDefaultComparator(),
ascending: true,
});
const [playlistFilter, setPlaylistFilter] = useState<LocalPlaylistFilter>({});
const [search, setSearch] = useState("");
@@ -135,6 +143,20 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
];
})();
const sortOptions: BsmSelectOption<string>[] = 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) {
)
)}
</BsmDropdownButton>
<BsmSelect
className="bg-theme-1 rounded-full px-1 pb-0.5 text-center cursor-pointer"
options={sortOptions}
selected="name"
onChange={handleSortChange}
/>
<BsmButton
className="h-full flex aspect-square relative rounded-full z-[1] bg-light-main-color-1 dark:bg-main-color-3"
iconClassName="transition-transform size-full ease-in-out duration-200 bg-light-main-color-1 dark:bg-main-color-3"
iconStyle={{ transform: mapSort.ascending ? "rotate(360deg)" : "rotate(180deg)" }}
icon="chevron-top"
typeColor="primary"
withBar={false}
onClick={() => setMapSort({
compare: mapSort.compare,
ascending: !mapSort.ascending,
})}
/>
<BsmDropdownButton className="h-full flex aspect-square relative rounded-full z-[1] bg-light-main-color-1 dark:bg-main-color-3" buttonClassName="rounded-full h-full w-full p-[6px]" icon="three-dots" withBar={false} items={dropDownItems} menuTranslationY="6px" align="center" />
</nav>
<BsContentTabPanel
@@ -228,7 +268,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
properties: ["openFile", "multiSelections"],
}}}
>
<LocalMapsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 0} ref={mapsRef} version={version} filter={mapFilter} search={search} linkedState={mapsLinkedState} />
<LocalMapsListPanel
className="w-full h-full shrink-0"
isActive={isActive && tabIndex === 0}
ref={mapsRef}
version={version}
filter={mapFilter}
search={search}
linkedState={mapsLinkedState}
sort={mapSort}
/>
</Dropzone>
<Dropzone
@@ -3,7 +3,7 @@ import { BSVersion } from "shared/bs-version.interface";
import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useState } from "react";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { Subscription, BehaviorSubject, lastValueFrom } from "rxjs";
import { MapFilter } from "shared/models/maps/beat-saver.model";
import { MapFilter, MapSort } from "shared/models/maps/beat-saver.model";
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
import { bufferTime, last, tap } from "rxjs/operators";
import { useTranslation } from "renderer/hooks/use-translation.hook";
@@ -32,6 +32,7 @@ type Props = {
search?: string;
linkedState?: FolderLinkState;
isActive?: boolean;
sort: MapSort;
};
export type LocalMapsListPanelRef = {
@@ -40,7 +41,15 @@ export type LocalMapsListPanelRef = {
removeDuplicates: () => void;
}
export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ version, className, filter, search, linkedState, isActive }, forwardRef) => {
export const LocalMapsListPanel = forwardRef<unknown, Props>(({
version,
className,
filter,
search,
linkedState,
isActive,
sort,
}, forwardRef) => {
const mapsManager = useService(MapsManagerService);
const mapsDownloader = useService(MapsDownloaderService);
@@ -204,7 +213,9 @@ export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ 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) {
@@ -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<BsmLocalMap>;
} = {
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<BsmLocalMap> {
return this.comparators.name;
}
public getComparator(key: string): Comparator<BsmLocalMap> {
const comparator = this.comparators[key];
return comparator || this.comparators.name;
}
}
+3
View File
@@ -0,0 +1,3 @@
export type Comparator<T> = (object1: T, object2: T) => number;
@@ -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<MapTag>;
}
export interface MapSort {
compare: Comparator<BsmLocalMap>;
ascending: boolean;
}
export interface SearchResponse {
docs: BsvMapDetail[];
redirect: string;