Merge pull request #601 from silentrald/feat/216

[feat-216] support for sorting maps/songs and playlists
This commit is contained in:
MathieuG-P
2025-01-11 12:59:15 +01:00
committed by GitHub
23 changed files with 510 additions and 36 deletions
+16
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "Deine Karten importieren",
"subtext": "Lasse deine ZIP-Dateien hier fallen, um deine Karten zu importieren"
},
"sort": {
"name": "Name",
"song-author": "Liedautor",
"map-author": "Kartenautor",
"bpm": "BPM",
"duration": "Dauer",
"likes": "Likes",
"date-uploaded": "Hochladedatum"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "Importiere deine Wiedergabelisten",
"subtext": "Ziehe deine \".bplist\" oder \".json\" Dateien hierhin, um sie zu importieren"
},
"sort": {
"title": "Titel",
"author": "Autor",
"number-of-maps": "Anzahl der Karten",
"duration": "Dauer",
"notes-per-second": "NPS"
}
}
}
+16
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "Import your maps",
"subtext": "Drop your zip files here to import your maps"
},
"sort": {
"name": "Name",
"song-author": "Song Author",
"map-author": "Map Author",
"bpm": "BPM",
"duration": "Duration",
"likes": "Likes",
"date-uploaded": "Date Uploaded"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "Import your playlists",
"subtext": "Drop your \".bplist\" or \".json\" files here to import them"
},
"sort": {
"title": "Title",
"author": "Author",
"number-of-maps": "Number of Maps",
"duration": "Duration",
"notes-per-second": "NPS"
}
}
}
+16
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "Importar tus mapas",
"subtext": "Suelta tus archivos zip aquí para importar tus mapas"
},
"sort": {
"name": "Nombre",
"song-author": "Autor de la canción",
"map-author": "Autor del mapa",
"bpm": "BPM",
"duration": "Duración",
"likes": "Me gusta",
"date-uploaded": "Fecha de subida"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "Importa tus listas de reproducción",
"subtext": "Suelta tus archivos \".bplist\" o \".json\" aquí para importarlos"
},
"sort": {
"title": "Título",
"author": "Autor",
"number-of-maps": "Número de mapas",
"duration": "Duración",
"notes-per-second": "NPS"
}
}
}
+16
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "Importer vos maps",
"subtext": "Déposez vos fichiers zip ici pour importer vos maps"
},
"sort": {
"name": "Nom",
"song-author": "Auteur de la chanson",
"map-author": "Auteur de la carte",
"bpm": "BPM",
"duration": "Durée",
"likes": "J'aime",
"date-uploaded": "Date de téléchargement"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "Importer vos playlists",
"subtext": "Déposez vos fichiers \".bplist\" ou \".json\" ici pour les importer"
},
"sort": {
"title": "Titre",
"author": "Auteur",
"number-of-maps": "Nombre de cartes",
"duration": "Durée",
"notes-per-second": "NPS"
}
}
}
+16
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "Importa le tue mappe",
"subtext": "Rilascia qui i tuoi file zip per importare le tue mappe"
},
"sort": {
"name": "Nome",
"song-author": "Autore della canzone",
"map-author": "Autore della mappa",
"bpm": "BPM",
"duration": "Durata",
"likes": "Mi piace",
"date-uploaded": "Data di caricamento"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "Importa le tue playlist",
"subtext": "Rilascia qui i file \".bplist\" o \".json\" per importarli"
},
"sort": {
"title": "Titolo",
"author": "Autore",
"number-of-maps": "Numero di mappe",
"duration": "Durata",
"notes-per-second": "NPS"
}
}
}
+16
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "マップをインポート",
"subtext": "ZIPファイルをここにドロップしてマップをインポート"
},
"sort": {
"name": "名前",
"song-author": "曲の作者",
"map-author": "地図の作者",
"bpm": "BPM",
"duration": "持続時間",
"likes": "いいね",
"date-uploaded": "アップロード日"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "プレイリストをインポート",
"subtext": "\".bplist\" または \".json\" ファイルをここにドロップしてインポート"
},
"sort": {
"title": "タイトル",
"author": "著者",
"number-of-maps": "地図の数",
"duration": "持続時間",
"notes-per-second": "NPS"
}
}
}
+16
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "맵 가져오기",
"subtext": "ZIP 파일을 여기에 드롭하여 맵 가져오기"
},
"sort": {
"name": "이름",
"song-author": "작곡가",
"map-author": "지도 제작자",
"bpm": "BPM",
"duration": "지속 시간",
"likes": "좋아요",
"date-uploaded": "업로드 날짜"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "재생 목록 가져오기",
"subtext": "\".bplist\" 또는 \".json\" 파일을 여기에 드롭하여 가져오기"
},
"sort": {
"title": "제목",
"author": "저자",
"number-of-maps": "지도 수",
"duration": "지속 시간",
"notes-per-second": "NPS"
}
}
}
+16
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "Импортируйте свои карты",
"subtext": "Перетащите сюда файлы ZIP, чтобы импортировать свои карты"
},
"sort": {
"name": "Имя",
"song-author": "Автор песни",
"map-author": "Автор карты",
"bpm": "BPM",
"duration": "Длительность",
"likes": "Лайки",
"date-uploaded": "Дата загрузки"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "Импортируйте ваши плейлисты",
"subtext": "Перетащите сюда файлы \".bplist\" или \".json\" для их импорта"
},
"sort": {
"title": "Заголовок",
"author": "Автор",
"number-of-maps": "Количество карт",
"duration": "Длительность",
"notes-per-second": "NPS"
}
}
}
+16
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "導入您的地圖",
"subtext": "將ZIP文件拖放到此處以導入您的地圖"
},
"sort": {
"name": "名字",
"song-author": "歌曲作者",
"map-author": "地圖作者",
"bpm": "BPM",
"duration": "持續時間",
"likes": "喜歡",
"date-uploaded": "上傳日期"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "匯入您的播放列表",
"subtext": "將您的 \".bplist\" 或 \".json\" 檔案拖曳到這裡進行匯入"
},
"sort": {
"title": "標題",
"author": "作者",
"number-of-maps": "地圖數量",
"duration": "持續時間",
"notes-per-second": "NPS"
}
}
}
+16
View File
@@ -91,6 +91,15 @@
"drop-zone": {
"text": "导入您的地图",
"subtext": "将ZIP文件拖放到此处以导入您的地图"
},
"sort": {
"name": "名字",
"song-author": "歌曲作者",
"map-author": "地图作者",
"bpm": "BPM",
"duration": "持续时间",
"likes": "喜欢",
"date-uploaded": "上传日期"
}
},
"playlists": {
@@ -102,6 +111,13 @@
"drop-zone": {
"text": "导入您的播放列表",
"subtext": "将您的 \".bplist\" 或 \".json\" 文件拖放到这里进行导入"
},
"sort": {
"title": "标题",
"author": "作者",
"number-of-maps": "地图数量",
"duration": "持续时间",
"notes-per-second": "NPS"
}
}
}
+1 -1
View File
@@ -23,7 +23,7 @@ jest.mock("electron-log", () => ({
info: jest.fn(),
error: jest.fn(),
}));
jest.mock("ps-list", () => () => []);
jest.mock("ps-list", () => (): unknown[] => []);
const IS_WINDOWS = process.platform === "win32";
const IS_LINUX = process.platform === "linux";
@@ -3,10 +3,10 @@ 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";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
import { FolderLinkState } from "renderer/services/version-folder-linker.service";
import { useService } from "renderer/hooks/use-service.hook";
import { BsContentTabPanel } from "../shared/bs-content-tab-panel/bs-content-tab-panel.component";
@@ -20,10 +20,14 @@ import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { useConstant } from "renderer/hooks/use-constant.hook";
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
import { LocalPlaylistFilter, LocalPlaylistFilterPanel } from "./playlists/local-playlist-filter-panel.component";
import { LocalPlaylistFilter, LocalPlaylistFilterPanel, LocalPlaylistSort } from "./playlists/local-playlist-filter-panel.component";
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 { BsmButton } from "../shared/bsm-button.component";
import { mapSorter } from "shared/models/maps/sorter";
import { playlistSorter } from "shared/models/playlists/sorter";
type Props = {
readonly version?: BSVersion;
@@ -39,12 +43,15 @@ export const InstalledMapsContext = createContext<{
export function MapsPlaylistsPanel({ version, isActive }: Props) {
const MAP_TAB = 0;
const PLAYLIST_TAB = 1;
const mapsManager = useService(MapsManagerService);
const mapsDownloader = useService(MapsDownloaderService);
const playlistsManager = useService(PlaylistsManagerService);
const playlistsDownloader = useService(PlaylistDownloaderService);
const t = useTranslation();
const { text: t } = useTranslationV2();
const [tabIndex, setTabIndex] = useState(0);
const [mapsDropZoneOpen, setMapsDropZoneOpen] = useState(false);
@@ -64,7 +71,23 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
const playlistsRef = useRef<LocalPlaylistsListRef>();
const [mapFilter, setMapFilter] = useState<MapFilter>({});
const [mapSort, setMapSort] = useState<MapSort>({
compare: mapSorter.getDefaultComparator(),
ascending: true,
});
const [playlistFilter, setPlaylistFilter] = useState<LocalPlaylistFilter>({});
const [playlistSort, setPlaylistSort] = useState<LocalPlaylistSort>({
compare: playlistSorter.getDefaultComparator(),
ascending: true,
});
const [selectedSort, setSelectedSort] = useState<string>("name");
const [sortOptions, setSortOptions] = useState<BsmSelectOption<string>[]>(
() => mapSorter.getComparatorKeys().map(key => ({
text: `pages.version-viewer.maps.tabs.maps.sort.${key}`,
value: key,
}))
);
const [search, setSearch] = useState("");
const mapsLinkedState = useObservable(() => {
@@ -100,6 +123,42 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
return mapsManager.unlinkVersion(version);
};
const handleTabChange = (index: number) => {
if (tabIndex === index) {
return;
}
setTabIndex(index);
switch (index) {
case MAP_TAB:
setSortOptions(() => mapSorter.getComparatorKeys().map(key => ({
text: `pages.version-viewer.maps.tabs.maps.sort.${key}`,
value: key,
})));
setSelectedSort(mapSorter.defaultKey);
setMapSort({
compare: mapSorter.getDefaultComparator(),
ascending: playlistSort.ascending,
});
break;
case PLAYLIST_TAB:
setSortOptions(() => playlistSorter.getComparatorKeys().map(key => ({
text: `pages.version-viewer.maps.tabs.playlists.sort.${key}`,
value: key,
})));
setSelectedSort(playlistSorter.defaultKey);
setPlaylistSort({
compare: playlistSorter.getDefaultComparator(),
ascending: mapSort.ascending,
});
break;
default:
break;
}
};
const handlePlaylistLinkClick = () => {
if(playlistLinkedState === FolderLinkState.Pending || playlistLinkedState === FolderLinkState.Processing){ return Promise.resolve(false); }
@@ -135,8 +194,50 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
];
})();
const handleSortChange = (key: string) => {
switch (tabIndex) {
case MAP_TAB:
setMapSort({
compare: mapSorter.getComparator(key),
ascending: mapSort.ascending,
});
break;
case PLAYLIST_TAB:
setPlaylistSort({
compare: playlistSorter.getComparator(key),
ascending: playlistSort.ascending,
});
break;
default:
break;
}
}
const handleAscendingClick = () => {
switch (tabIndex) {
case MAP_TAB:
setMapSort({
compare: mapSort.compare,
ascending: !mapSort.ascending,
});
break;
case PLAYLIST_TAB:
setPlaylistSort({
compare: playlistSort.compare,
ascending: !playlistSort.ascending,
});
break;
default:
break;
}
}
const dropDownItems = ((): DropDownItem[] => {
if (tabIndex === 0) {
if (tabIndex === MAP_TAB) {
return [
{ icon: "export", text: "pages.version-viewer.maps.search-bar.dropdown.export-maps", onClick: () => mapsRef.current.exportMaps?.() },
{ icon: "trash", text: "pages.version-viewer.maps.search-bar.dropdown.delete-maps", onClick: () => mapsRef.current.deleteMaps?.() },
@@ -153,7 +254,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
return (
<>
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white mb-4">
<nav className="w-full shrink-0 flex h-9 justify-center gap-2 text-main-color-1 dark:text-white mb-4">
<BsmDropdownButton
classNames={{
mainContainer: "h-full relative z-[1] flex justify-center w-fit",
@@ -164,7 +265,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
icon="add"
buttonColor="primary"
text="misc.add"
align="center"
align="left"
menuTranslationY="6px"
withBar={false}
items={addDropDownItems}
@@ -173,7 +274,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
<input
type="text"
className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2"
placeholder={tabIndex === 0 ? t("pages.version-viewer.maps.search-bar.search-placeholder") : t("playlist.search-playlist")}
placeholder={tabIndex === MAP_TAB ? t("pages.version-viewer.maps.search-bar.search-placeholder") : t("playlist.search-playlist")}
value={search}
onChange={e => setSearch(() => e.target.value)}
tabIndex={-1}
@@ -188,16 +289,37 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
)
)}
</BsmDropdownButton>
<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" />
<div className="h-full flex bg-theme-2 rounded-full">
<BsmButton
className="h-full aspect-square rounded-l-full z-[1]"
iconClassName="transition-transform size-full ease-in-out duration-200 bg-theme-2"
iconStyle={{ transform: (tabIndex === MAP_TAB && mapSort.ascending)
|| (tabIndex === PLAYLIST_TAB && playlistSort.ascending)
? "rotate(0deg)" : "rotate(180deg)"}}
icon="arrow-upward"
withBar={false}
onClick={handleAscendingClick}
/>
<BsmSelect
className="h-full bg-theme-2 rounded-r-full cursor-pointer outline-none pb-1 text-center"
options={sortOptions}
selected={selectedSort}
onChange={handleSortChange}
/>
</div>
<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="right" />
</nav>
<BsContentTabPanel
tabIndex={tabIndex}
onTabChange={index => setTabIndex(index)}
onTabChange={handleTabChange}
tabs={[
{
text: "misc.maps",
icon: MapIcon,
onClick: () => setTabIndex(0),
onClick: () => handleTabChange(MAP_TAB),
linkProps: version ? {
state: mapsLinkedState,
onClick: handleMapsLinkClick,
@@ -206,7 +328,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
{
text: "misc.playlists",
icon: PlaylistIcon,
onClick: () => setTabIndex(1),
onClick: () => handleTabChange(PLAYLIST_TAB),
linkProps: version ? {
state: playlistLinkedState,
onClick: handlePlaylistLinkClick,
@@ -228,7 +350,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
@@ -243,7 +374,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
properties: ["openFile", "multiSelections"],
}}}
>
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} ref={playlistsRef} version={version} linkedState={playlistLinkedState} filter={playlistFilter} search={search}/>
<LocalPlaylistsListPanel
className="w-full h-full shrink-0"
isActive={isActive && tabIndex === 1}
ref={playlistsRef}
version={version}
linkedState={playlistLinkedState}
filter={playlistFilter}
search={search}
sort={playlistSort}
/>
</Dropzone>
</InstalledMapsContext.Provider>
@@ -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) {
@@ -6,6 +6,8 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
import { hourToS, sToMs } from "shared/helpers/time.helpers";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import formatDuration from "format-duration";
import { LocalBPListsDetails } from "shared/models/playlists/local-playlist.models";
import { Comparator } from "shared/models/comparator.type";
type Props = {
className?: string;
@@ -120,3 +122,8 @@ export type LocalPlaylistFilter = Partial<{
minDuration: number;
maxDuration: number;
}>
export interface LocalPlaylistSort {
compare: Comparator<LocalBPListsDetails>;
ascending: boolean;
}
@@ -24,7 +24,7 @@ import { DeletePlaylistModal } from "renderer/components/modal/modal-types/playl
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
import { PlaylistItemComponentPropsMapper } from "shared/mappers/playlist/playlist-item-component-props.mapper";
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
import { LocalPlaylistFilter } from "./local-playlist-filter-panel.component";
import { LocalPlaylistFilter, LocalPlaylistSort } from "./local-playlist-filter-panel.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
@@ -48,6 +48,7 @@ type Props = {
search?: string;
linkedState?: FolderLinkState;
isActive?: boolean;
sort: LocalPlaylistSort;
};
export type LocalPlaylistsListRef = {
@@ -57,7 +58,15 @@ export type LocalPlaylistsListRef = {
exportPlaylists: () => Promise<void>;
}
export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(({ version, className, filter: playlistFiler, search, isActive, linkedState }, forwardedRef) => {
export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(({
version,
className,
filter: playlistFilter,
search,
isActive,
linkedState,
sort: playlistSort,
}, forwardedRef) => {
const t = useTranslation();
@@ -397,27 +406,31 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
if(!p.playlistTitle?.toLowerCase().includes(search.toLowerCase())){ return false; }
if(!p.playlistAuthor?.toLowerCase().includes(search.toLowerCase())){ return false; }
if(typeof p.nbMaps === "number" && (typeof playlistFiler?.minNbMaps === "number" || typeof playlistFiler?.maxNbMaps === "number")){
if(playlistFiler?.minNbMaps && p.nbMaps < playlistFiler.minNbMaps){ return false; }
if(playlistFiler?.maxNbMaps && p.nbMaps > playlistFiler.maxNbMaps){ return false; }
if(typeof p.nbMaps === "number" && (typeof playlistFilter?.minNbMaps === "number" || typeof playlistFilter?.maxNbMaps === "number")){
if(playlistFilter?.minNbMaps && p.nbMaps < playlistFilter.minNbMaps){ return false; }
if(playlistFilter?.maxNbMaps && p.nbMaps > playlistFilter.maxNbMaps){ return false; }
}
if(typeof p.nbMappers === "number" && (typeof playlistFiler?.minNbMappers === "number" || typeof playlistFiler?.maxNbMappers === "number")){
if(playlistFiler?.minNbMappers && p.nbMappers < playlistFiler.minNbMappers){ return false; }
if(playlistFiler?.maxNbMappers && p.nbMappers > playlistFiler.maxNbMappers){ return false; }
if(typeof p.nbMappers === "number" && (typeof playlistFilter?.minNbMappers === "number" || typeof playlistFilter?.maxNbMappers === "number")){
if(playlistFilter?.minNbMappers && p.nbMappers < playlistFilter.minNbMappers){ return false; }
if(playlistFilter?.maxNbMappers && p.nbMappers > playlistFilter.maxNbMappers){ return false; }
}
if(typeof p.duration === "number" && (typeof playlistFiler?.minDuration === "number" || typeof playlistFiler?.maxDuration === "number")){
if(playlistFiler?.minDuration && p.duration < playlistFiler.minDuration){ return false; }
if(playlistFiler?.maxDuration && p.duration > playlistFiler.maxDuration){ return false; }
if(typeof p.duration === "number" && (typeof playlistFilter?.minDuration === "number" || typeof playlistFilter?.maxDuration === "number")){
if(playlistFilter?.minDuration && p.duration < playlistFilter.minDuration){ return false; }
if(playlistFilter?.maxDuration && p.duration > playlistFilter.maxDuration){ return false; }
}
if(typeof p.minNps === "number" && typeof playlistFiler.minNps === "number" && p.minNps < playlistFiler.minNps){ return false; }
if(typeof p.maxNps === "number" && typeof playlistFiler.maxNps === "number" && p.maxNps > playlistFiler.maxNps){ return false; }
if(typeof p.minNps === "number" && typeof playlistFilter.minNps === "number" && p.minNps < playlistFilter.minNps){ return false; }
if(typeof p.maxNps === "number" && typeof playlistFilter.maxNps === "number" && p.maxNps > playlistFilter.maxNps){ return false; }
return true;
});
}, [playlists, search, playlistFiler]);
})
.sort((playlist1, playlist2) =>
playlistSort.compare(playlist1, playlist2)
* (playlistSort.ascending ? 1 : -1)
);
}, [playlists, search, playlistFilter, playlistSort]);
return (
<div className={className}>
@@ -133,7 +133,7 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
return (
<div ref={ref} className="w-full h-full flex flex-col items-center justify-center gap-4">
<div className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white">
<div className="w-full shrink-0 flex h-9 justify-center gap-2 text-main-color-1 dark:text-white">
<BsmButton
className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold"
icon="add"
@@ -149,7 +149,7 @@ export function ModelsPanel({ version, isActive, goToMods }: { version?: BSVersi
<div className="h-full rounded-full bg-light-main-color-2 dark:bg-main-color-2 grow p-[6px]">
<input type="text" className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2" placeholder={t("models.panel.actions.search")} onChange={e => setSearch(e.target.value)} tabIndex={-1} />
</div>
<BsmDropdownButton items={threeDotsItems} 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} menuTranslationY="6px" align="center" />
<BsmDropdownButton items={threeDotsItems} 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} menuTranslationY="6px" align="right" />
</div>
<BsContentTabPanel
tabIndex={currentTabIndex}
@@ -68,10 +68,11 @@ import { BrowseIcon } from "./icons/browse-icon.component";
import { AddFileIcon } from "./icons/add-file-icon.component";
import { CancelIcon } from "./icons/cancel-icon.component";
import { WarningIcon } from "./icons/warning-icon.component";
import { ArrowUpwardIcon } from "./icons/arrow-upward-icon.component";
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" );
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "arrow-upward" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" );
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
// TODO : Very ugly very messy, need to find a better way to do this
@@ -282,6 +283,10 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
if(icon === "add-file") {
return <AddFileIcon className={className} style={style} />;
}
if(icon === "arrow-upward") {
return <ArrowUpwardIcon className={className} style={style} />;
}
if(icon === "null") {
return null;
}
@@ -0,0 +1,15 @@
import { createSvgIcon } from "../svg-icon.type";
export const ArrowUpwardIcon = createSvgIcon((props, ref) => {
return (
<svg
ref={ref}
{...props}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 -960 960 960"
fill="currentColor"
>
<path d="M440-608 324-492q-11 11-28 11t-28-11q-11-11-11-28t11-28l184-184q12-12 28-12t28 12l184 184q11 11 11 28t-11 28q-11 11-28 11t-28-11L520-608v328q0 17-11.5 28.5T480-240q-17 0-28.5-11.5T440-280v-328Z" />
</svg>
);
});
+8
View File
@@ -0,0 +1,8 @@
export enum Comparison {
EQUAL = 0,
GREATER = 1,
LESSER = -1,
};
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 "../comparator.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;
+42
View File
@@ -0,0 +1,42 @@
import { Comparison } from "../comparator.type";
import { Sorter } from "../sorter.model";
import { BsmLocalMap } from "./bsm-local-map.interface";
const sortName = (map1: BsmLocalMap, map2: BsmLocalMap) => map1.mapInfo.songName.localeCompare(map2.mapInfo.songName);
export const mapSorter = new Sorter<BsmLocalMap>({
comparators: {
name: sortName,
"song-author": (map1, map2) => map1.mapInfo.songAuthorName.localeCompare(map2.mapInfo.songAuthorName),
"map-author": (map1, map2) => {
// Compare the number of mappers, else compare the first mapper
const difference = map1.mapInfo.levelMappers.length - map2.mapInfo.levelMappers.length;
return difference || map1.mapInfo.levelMappers[0].localeCompare(map2.mapInfo.levelMappers[0]);
},
bpm: (map1, map2) => map1.mapInfo.beatsPerMinute - map2.mapInfo.beatsPerMinute,
duration: (map1, map2) => {
if (!map1.songDetails) {
return map2.songDetails ? Comparison.LESSER : Comparison.EQUAL;
}
return !map2.songDetails ? Comparison.GREATER : map1.songDetails.duration - map2.songDetails.duration;
},
likes: (map1, map2) => {
if (!map1.songDetails) {
return map2.songDetails ? Comparison.LESSER : Comparison.EQUAL;
}
return !map2.songDetails ? Comparison.GREATER : map1.songDetails.upVotes - map2.songDetails.upVotes;
},
"date-uploaded": (map1, map2) => {
if (!map1.songDetails) {
return map2.songDetails ? Comparison.LESSER : Comparison.EQUAL;
}
return !map2.songDetails ? Comparison.GREATER : map1.songDetails.uploadedAt - map2.songDetails.uploadedAt;
},
},
tiebreak: sortName,
defaultKey: "name"
});
+30
View File
@@ -0,0 +1,30 @@
import { Comparison } from "../comparator.type";
import { Sorter } from "../sorter.model";
import { LocalBPListsDetails } from "./local-playlist.models";
const sortTitle = (playlist1: LocalBPListsDetails, playlist2: LocalBPListsDetails) =>
playlist1.playlistTitle.localeCompare(playlist2.playlistTitle);
export const playlistSorter = new Sorter({
comparators: {
title: sortTitle,
author: (playlist1, playlist2) => playlist1.playlistAuthor.localeCompare(playlist2.playlistAuthor),
"number-of-maps": (playlist1, playlist2) => playlist1.nbMaps - playlist2.nbMaps,
duration: (playlist1, playlist2) => {
if (!playlist1.duration) {
return playlist2.duration ? Comparison.LESSER : Comparison.EQUAL;
}
return !playlist2.duration ? Comparison.GREATER : playlist1.duration - playlist2.duration;
},
"notes-per-second": (playlist1, playlist2) => {
if (!playlist1.maxNps) {
return playlist2.maxNps ? Comparison.LESSER : Comparison.EQUAL;
}
return !playlist2.maxNps ? Comparison.GREATER : playlist1.maxNps - playlist2.maxNps;
},
},
tiebreak: sortTitle,
defaultKey: "title",
});
+36
View File
@@ -0,0 +1,36 @@
import { Comparator } from "./comparator.type";
export type SorterOptions<T> = {
defaultKey: string;
comparators: Record<string, Comparator<T>>;
tiebreak: Comparator<T>;
}
export class Sorter<T> {
public readonly defaultKey: string;
private readonly comparators: Record<string, Comparator<T>>;
private readonly tiebreak: Comparator<T>;
constructor(options: SorterOptions<T>) {
this.defaultKey = options.defaultKey;
this.comparators = options.comparators;
this.tiebreak = options.tiebreak;
}
public getComparatorKeys(): string[] {
return Object.keys(this.comparators);
}
public getDefaultComparator(): Comparator<T> {
return this.comparators[this.defaultKey];
}
public getComparator(key: string): Comparator<T> {
const comparator = this.comparators[key] || this.comparators[this.defaultKey];
return this.tiebreak === comparator
? comparator
: (object1, object2) => comparator(object1, object2) || this.tiebreak(object1, object2);
}
}