mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feat-216] support for sorting maps/songs
This commit is contained in:
@@ -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
|
||||
|
||||
+14
-3
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user