From 5e1a021c3a9dade659fdc1dc10f1de2018b8b857 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Sun, 16 Jun 2024 21:13:54 +0200 Subject: [PATCH] [feature-107] progress on the modal for creating and editing playlists --- .../maps/filter-panel.component.tsx | 45 ++- .../maps/map-item.component.tsx | 19 +- .../edit-playlist-modal.component.tsx | 314 ++++++++++++++---- .../virtual-scroll.component.tsx | 4 +- src/shared/helpers/type.helpers.ts | 1 + .../map/map-item-component-props.mapper.ts | 3 + src/shared/models/maps/beat-saver.model.ts | 12 + 7 files changed, 297 insertions(+), 101 deletions(-) diff --git a/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx index bcd669ac..d2fbf93b 100644 --- a/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/filter-panel.component.tsx @@ -226,18 +226,14 @@ export function FilterPanel({ className, ref, playlist = false, filter, localDat // Filter functions function isFitEnabledTags(filter: MapFilter, tags: MapTag[]): boolean { + if (!filter?.enabledTags || filter.enabledTags.size === 0) { return true; } if(!Array.isArray(tags)) { return false; } - if (!filter?.enabledTags || filter.enabledTags.size === 0) { - return true; - } return Array.from(filter.enabledTags.values()).every(tag => tags.some(mapTag => mapTag === tag)); } function isFitExcludedTags(filter: MapFilter, tags: MapTag[]): boolean { + if (!filter?.excludedTags || filter.excludedTags.size === 0) { return true; } if(!Array.isArray(tags)) { return false; } - if (!filter?.excludedTags || filter.excludedTags.size === 0) { - return true; - } return !tags.some(tag => filter.excludedTags.has(tag as MapTag)); } @@ -325,7 +321,7 @@ export const isLocalMapFitMapFilter = ({filter, map, search}: { filter: MapFilte if (!isFitChroma(filter, map.songDetails?.difficulties.some(diff => !!diff.chroma))) { return false; } if (!isFitFullSpread(filter, map.songDetails?.difficulties.length)) { return false; } if (!isFitAutomapper(filter, map.songDetails?.automapper)){ return false; } - if (!isFitRanked(filter, map.songDetails?.ranked)) { return false; } + if (!isFitRanked(filter, map.songDetails?.ranked || map.songDetails?.rankedBL)) { return false; } if (!isFitCurated(filter, map.songDetails?.curated)) { return false; } if (!isFitVerified(filter, map.songDetails?.uploader.verified)) { return false; } if (!isFitSearch(search, {songName: map.rawInfo?._songName, songAuthorName: map.rawInfo?._songAuthorName, levelAuthorName: map.rawInfo?._levelAuthorName})) { return false; } @@ -333,22 +329,25 @@ export const isLocalMapFitMapFilter = ({filter, map, search}: { filter: MapFilte }; export const isBsvMapFitMapFilter = ({filter, map, search}: { filter: MapFilter, map: BsvMapDetail, search: string }): boolean => { - if (!isFitEnabledTags(filter, map.tags)) { return false; } + + console.log("LAAAAAA", map); + + if (!isFitEnabledTags(filter, map.tags)) { console.log(1); return false; } if (!isFitExcludedTags(filter, map.tags)) { return false; } - if (map.versions?.at(0)?.diffs && !map.versions.at(0).diffs.some(diff => isFitMinNps(filter, diff.nps))) { return false; } - if (map.versions?.at(0)?.diffs && !map.versions.at(0).diffs.some(diff => isFitMaxNps(filter, diff.nps))) { return false; } - if (!isFitMinDuration(filter, map.metadata.duration)) { return false; } - if (!isFitMaxDuration(filter, map.metadata.duration)) { return false; } - if (!isFitNoodle(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.ne))) { return false; } - if (!isFitMe(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.me))) { return false; } - if (!isFitCinema(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.cinema))) { return false; } - if (!isFitChroma(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.chroma))) { return false; } - if (!isFitFullSpread(filter, map.versions?.at(0)?.diffs.length)) { return false; } - if (!isFitAutomapper(filter, map.automapper)){ return false; } - if (!isFitRanked(filter, map.ranked)) { return false; } - if (!isFitCurated(filter, !!map.curator)) { return false; } - if (!isFitVerified(filter, !!map.curatedAt)) { return false; } - if (!isFitSearch(search, {songName: map.name, songAuthorName: map.metadata.songAuthorName, levelAuthorName: map.metadata.levelAuthorName})) { return false; } + if (map.versions?.at(0)?.diffs && !map.versions.at(0).diffs.some(diff => isFitMinNps(filter, diff.nps))) { console.log(2); return false; } + if (map.versions?.at(0)?.diffs && !map.versions.at(0).diffs.some(diff => isFitMaxNps(filter, diff.nps))) { console.log(3); return false; } + if (!isFitMinDuration(filter, map.metadata.duration)) { console.log(4); return false; } + if (!isFitMaxDuration(filter, map.metadata.duration)) { console.log(5); return false; } + if (!isFitNoodle(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.ne))) { console.log(6); return false; } + if (!isFitMe(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.me))) { console.log(7); return false; } + if (!isFitCinema(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.cinema))) { console.log(8); return false; } + if (!isFitChroma(filter, map.versions?.at(0)?.diffs.some(diff => !!diff.chroma))) { console.log(9); return false; } + if (!isFitFullSpread(filter, map.versions?.at(0)?.diffs.length)) { console.log(10); return false; } + if (!isFitAutomapper(filter, map.automapper)){ console.log(11); return false; } + if (!isFitRanked(filter, map.ranked || map.blRanked)) { console.log(12); return false; } + if (!isFitCurated(filter, !!map.curator)) { console.log(13); return false; } + if (!isFitVerified(filter, !!map.curatedAt)) { console.log(14); return false; } + if (!isFitSearch(search, {songName: map.name, songAuthorName: map.metadata.songAuthorName, levelAuthorName: map.metadata.levelAuthorName})) { console.log(15); return false; } return true; }; @@ -365,7 +364,7 @@ export const isSongDetailsFitMapFilter = ({filter, map, search}: { filter: MapFi if (!isFitChroma(filter, map.difficulties.some(diff => !!diff.chroma))) { return false; } if (!isFitFullSpread(filter, map.difficulties.length)) { return false; } if (!isFitAutomapper(filter, map.automapper)){ return false; } - if (!isFitRanked(filter, map.ranked)) { return false; } + if (!isFitRanked(filter, map.ranked || map.rankedBL)) { return false; } if (!isFitCurated(filter, map.curated)) { return false; } if (!isFitVerified(filter, map.uploader.verified)) { return false; } if (!isFitSearch(search, {songName: map.name, songAuthorName: map.uploader.name, levelAuthorName: map.uploader.name})) { return false; } diff --git a/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx b/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx index 9cf0c587..135660bd 100644 --- a/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/map-item.component.tsx @@ -24,6 +24,7 @@ import { BsvMapDetail, RawMapInfoData, SongDetailDiffCharactertistic, SongDetail import { useConstant } from "renderer/hooks/use-constant.hook"; import { CalendarDateTime, getLocalTimeZone } from "@internationalized/date"; import { typedMemo } from "renderer/helpers/typed-memo"; +import { Observable, of } from "rxjs"; export type ParsedMapDiff = { type: SongDiffName; name: string; stars: number }; @@ -37,14 +38,17 @@ export type MapItemComponentProps = { autorId: number; mapId: string; diffs: Map; - ranked: boolean; + ranked?: boolean; + blRanked?: boolean; bpm?: number; duration: number; likes: number; createdAt: number | CalendarDateTime; selected?: boolean; + selected$?: Observable; downloading?: boolean; showOwned?: boolean; + isOwned$?: Observable; callBackParam: T; onDelete?: (param: T) => void; onDownload?: (param: T) => void; @@ -53,7 +57,7 @@ export type MapItemComponentProps = { onDoubleClick?: (param: T) => void; }; -export function MapItemComponent ({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, ranked, bpm, duration, likes, createdAt, selected, downloading, showOwned, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick }: MapItemComponentProps) { +export function MapItemComponent ({ hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, ranked, blRanked, bpm, duration, likes, createdAt, selected, selected$, downloading, showOwned, isOwned$, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick }: MapItemComponentProps) { const linkOpener = useService(LinkOpenerService); const audioPlayer = useService(AudioPlayerService); @@ -61,6 +65,8 @@ export function MapItemComponent ({ hash, title, autor, songAutor, const t = useTranslation(); const ref = useRef(null); + const isSelected = useObservable(() => selected$ ?? of(selected), false, [selected$, selected]); + const isOwned = useObservable(() => isOwned$ ?? of(showOwned ?? false), false, [isOwned$, showOwned]); const [hovered, setHovered] = useState(false); const [bottomBarHovered, setBottomBarHovered, cancelBottomBarHovered] = useDelayedState(false); const [diffsPanelHovered, setDiffsPanelHovered] = useState(false); @@ -162,7 +168,7 @@ export function MapItemComponent ({ hash, title, autor, songAutor, return ( setHovered(true)} onHoverEnd={() => setHovered(false)} style={{ zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden" }}> - + {(diffsPanelHovered || bottomBarHovered) && ( @@ -183,7 +189,7 @@ export function MapItemComponent ({ hash, title, autor, songAutor, )}
-
+
({ hash, title, autor, songAutor, {t("maps.map-specificities.ranked")}
)} + {blRanked && ( +
+ {t("maps.map-specificities.ranked")} +
+ )}
{renderDiffPreview()}
diff --git a/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal.component.tsx b/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal.component.tsx index 0b348794..7ff19595 100644 --- a/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/edit-playlist-modal.component.tsx @@ -1,15 +1,15 @@ -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { FilterPanel, isLocalMapFitMapFilter, isMapFitFilter } from "renderer/components/maps-playlists-panel/maps/filter-panel.component"; import { MapItem, extractMapDiffs } from "renderer/components/maps-playlists-panel/maps/map-item.component"; import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component"; -import { BsmSelect } from "renderer/components/shared/bsm-select.component"; -import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component"; +import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component"; +import { VirtualScroll, VirtualScrollEndHandler } from "renderer/components/shared/virtual-scroll/virtual-scroll.component"; import { ChevronTopIcon } from "renderer/components/svgs/icons/chevron-top-icon.component"; import { useObservable } from "renderer/hooks/use-observable.hook"; import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; import { useThemeColor } from "renderer/hooks/use-theme-color.hook"; import { ModalComponent } from "renderer/services/modale.service" -import { Observable, filter, lastValueFrom, take } from "rxjs"; +import { BehaviorSubject, Observable, Subject, filter, lastValueFrom, map, of, take } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { LocalBPList, LocalBPListsDetails } from "shared/models/playlists/local-playlist.models" @@ -19,7 +19,11 @@ import { useService } from "renderer/hooks/use-service.hook"; import { MapsManagerService } from "renderer/services/maps-manager.service"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { MapItemComponentPropsMapper } from "shared/mappers/map/map-item-component-props.mapper"; -import { MapFilter } from "shared/models/maps/beat-saver.model"; +import { BsvSearchOrder, MapFilter, SearchParams } from "shared/models/maps/beat-saver.model"; +import { useConstant } from "renderer/hooks/use-constant.hook"; +import Tippy from "@tippyjs/react"; +import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service"; +import { BsmButton } from "renderer/components/shared/bsm-button.component"; type Props = { version?: BSVersion; @@ -33,41 +37,74 @@ export const EditPlaylistModal: ModalComponent = ({ const color = useThemeColor("first-color"); const mapsService = useService(MapsManagerService); + const beatSaver = useService(BeatSaverService); - const maps = useObservable(() => maps$.pipe(filter(Array.isArray), take(1)), undefined); + const keyPressed$ = useConstant(() => new BehaviorSubject(undefined)); + const filterContainerRef = useRef(null); + const localMaps$ = useConstant(() => new BehaviorSubject<(BsmLocalMap|SongDetails)[]>(undefined)); + const localMaps = useObservable(() => localMaps$, undefined); + + const availabledHashsSelected$ = useConstant(() => new BehaviorSubject([])); const [availableMapsSearch, setAvailableMapsSearch] = useState(""); const [availableMapsFilter, setAvailableMapsFilter] = useState({}); + + const [bsvLoading, setBsvLoading] = useState(false); + const bsvMaps$ = useConstant(() => new BehaviorSubject(undefined)); + const bsvMaps = useObservable(() => bsvMaps$, undefined); + const [bsvSearchOrder, setBsvSortOrder] = useState(BsvSearchOrder.Latest); + const [bsvSearchParams, setBsvSearchParams] = useState({ + sortOrder: bsvSearchOrder, + filter: availableMapsFilter, + page: 0, + q: availableMapsSearch, + }); + const sortOptions: BsmSelectOption[] = useConstant(() => { + return Object.values(BsvSearchOrder).map(sort => ({ text: `beat-saver.maps-sorts.${sort}`, value: sort })); + }); + + const playlistMaps$ = useConstant(() => new BehaviorSubject>(undefined)); + const playlistMaps = useObservable(() => playlistMaps$, undefined); + + const playlistHashsSelected$ = useConstant(() => new BehaviorSubject([])); const [playlistMapsSearch, setPlaylistMapsSearch] = useState(""); const [playlistMapsFilter, setPlaylistMapsFilter] = useState({}); const [availableMapsSource, setAvailableMapsSource] = useState(0); - const [playlistMaps, setPlaylistMaps] = useState>(); + const displayablePlaylistMaps = useMemo(() => playlistMaps ? Object.values(playlistMaps).filter(Boolean) : [], [playlistMaps]); - const availableMaps = (() => { + useOnUpdate(() => { + const keyDown = (e: KeyboardEvent) => keyPressed$.next(e.key); + document.addEventListener("keydown", keyDown); - })(); + const keyUp = () => keyPressed$.next(undefined); + document.addEventListener("keyup", keyUp); + + return () => { + document.removeEventListener("keydown", keyDown); + document.removeEventListener("keyup", keyUp); + } + }, []); useOnUpdate(() => { - if(!maps){ return; } - - const playlistMapsRes = (playlist?.songs ?? []).reduce((acc, song) => { - const map = maps.find(map => map.hash === song.hash); - - if(map){ - acc[map.hash] = map; - } - else { - acc[song.hash] = undefined; - } - - return acc; - }, {} as Record); - - (async () => { + const maps = await lastValueFrom(maps$.pipe(take(1))); + + const playlistMapsRes = (playlist?.songs ?? []).reduce((acc, song) => { + const map = maps.find(map => map.hash === song.hash); + + if(map){ + acc[map.hash] = map; + } + else { + acc[song.hash] = undefined; + } + + return acc; + }, {} as Record); + const notInstalledHashs = Object.keys(playlistMapsRes).filter(hash => !playlistMapsRes[hash]); const songsDetails = await lastValueFrom(mapsService.getMapsInfoFromHashs(notInstalledHashs)); @@ -75,64 +112,179 @@ export const EditPlaylistModal: ModalComponent = ({ playlistMapsRes[song.hash] = song; }); + localMaps$.next([...(maps ?? []), ...(songsDetails ?? [])]); + playlistMaps$.next(playlistMapsRes ?? {}); })() - .catch(logRenderError) - .finally(() => setPlaylistMaps(playlistMapsRes)); - }, [maps]); + .catch(logRenderError); + }, []); useOnUpdate(() => { + availabledHashsSelected$.next([]); + setAvailableMapsFilter({}); + setAvailableMapsSearch(""); + bsvMaps$.next([]); + setBsvSearchParams({ sortOrder: bsvSearchOrder, filter: availableMapsFilter, page: 0, q: availableMapsSearch }); + }, [availableMapsSource]) - }, [maps, playlistMaps, playlist]); + useOnUpdate(() => { + if(availableMapsSource === 0){ return; } - const renderMapItem = useCallback((map: (BsmLocalMap|BsvMapDetail|SongDetails), onClick: (map: (BsmLocalMap|BsvMapDetail|SongDetails)) => void) => { + setBsvLoading(true); + + (async () => { + const maps = await beatSaver.searchMaps(bsvSearchParams); + bsvMaps$.next([...(bsvMaps ?? []), ...maps]); + setBsvLoading(false); + })() + .catch(logRenderError) + }, [bsvSearchParams]) + + const getHashOfMap = useCallback((map: (BsmLocalMap|BsvMapDetail|SongDetails)) => { + return (map as BsmLocalMap | SongDetails).hash ?? (map as BsvMapDetail).versions?.[0]?.hash; + }, []); + + const handleOnClickMap = useCallback(async ({ map, selectedHashs$, mapsSource$, noKeyPressedFallBack } : { + map: BsmLocalMap | BsvMapDetail | SongDetails; + mapsSource$: Observable<(BsmLocalMap|BsvMapDetail|SongDetails)[]>; + selectedHashs$: BehaviorSubject; + noKeyPressedFallBack?: () => void; + }) => { + + const keyPressed = keyPressed$.value; + const mapHash = getHashOfMap(map); + + if(keyPressed === "Control"){ + const selectedHashs = selectedHashs$.value; + const newSelectedHashs = selectedHashs.includes(mapHash) ? selectedHashs.filter(hash => hash !== mapHash) : [...selectedHashs, mapHash]; + return selectedHashs$.next(newSelectedHashs); + } + + const mapsSource = await lastValueFrom(mapsSource$.pipe(take(1))); + + if(keyPressed === "Shift" && Array.isArray(mapsSource)){ + const selectedHashs = selectedHashs$.value; + let lastSelectedMapIndex = mapsSource.findIndex(map => getHashOfMap(map) === selectedHashs.at(0)); + lastSelectedMapIndex = lastSelectedMapIndex === -1 ? 0 : lastSelectedMapIndex; + const currentMapIndex = mapsSource.findIndex(map => getHashOfMap(map) === mapHash); + const newSelectedHashs = mapsSource.slice(Math.min(lastSelectedMapIndex, currentMapIndex), Math.max(lastSelectedMapIndex, currentMapIndex) + 1).map(map => getHashOfMap(map)); + return selectedHashs$.next(newSelectedHashs); + } + + noKeyPressedFallBack?.(); + selectedHashs$.next([]); + }, []); + + const renderMapItem = useCallback((map: (BsmLocalMap|BsvMapDetail|SongDetails), onClick: (map: (BsmLocalMap|BsvMapDetail|SongDetails)) => void, isSelected$: Observable, isOwned$?: Observable) => { return ( ); }, []); - const renderAvailableMapItem = useCallback((map: BsmLocalMap) => { - return renderMapItem(map, () => setPlaylistMaps(prev => ({ ...prev, [map.hash]: map }))); + const renderAvailableMapItem = useCallback((localMap: BsmLocalMap | SongDetails | BsvMapDetail) => { + const mapHash = getHashOfMap(localMap); + const isSelected$ = availabledHashsSelected$.pipe(map(hashs => hashs.includes(mapHash))); + return renderMapItem(localMap, () => handleOnClickMap({ + map: localMap, + mapsSource$: localMaps$ as BehaviorSubject<(BsmLocalMap|BsvMapDetail|SongDetails)[]>, + selectedHashs$: availabledHashsSelected$, + noKeyPressedFallBack: () => playlistMaps$.next(Object.assign({[mapHash]: localMap}, playlistMaps$.value)) + }), isSelected$); }, []); - const renderPlaylistMapItem = useCallback((map: (BsmLocalMap|BsvMapDetail|SongDetails)) => { - return renderMapItem(map, () => setPlaylistMaps(prev => { - - if(!map){ return prev; } - - const hash = (map as BsmLocalMap | SongDetails).hash ? (map as BsmLocalMap|SongDetails).hash : (map as BsvMapDetail).versions?.[0]?.hash; - - if(!hash){ return prev; } - - const newPlaylistMaps = { ...prev }; - delete newPlaylistMaps[(map as BsmLocalMap).hash]; - return newPlaylistMaps; - })); + const renderBsvMapItem = useCallback((bsvMap: BsvMapDetail) => { + const mapHash = getHashOfMap(bsvMap); + const isSelected$ = availabledHashsSelected$.pipe(map(hashs => hashs.includes(mapHash))); + const isOwned$ = playlistMaps$.pipe(map(playlistMaps => !!playlistMaps[mapHash])); + return renderMapItem(bsvMap, () => handleOnClickMap({ + map: bsvMap, + mapsSource$: bsvMaps$, + selectedHashs$: availabledHashsSelected$, + noKeyPressedFallBack: () => playlistMaps$.next(Object.assign({[mapHash]: bsvMap}, playlistMaps$.value)) + }), isSelected$, isOwned$); }, []); - const renderList = (maps: T[], render: (item: T) => JSX.Element) => { + const renderPlaylistMapItem = useCallback((playlistMap: (BsmLocalMap|BsvMapDetail|SongDetails)) => { + const mapHash = getHashOfMap(playlistMap); + const isSelected$ = playlistHashsSelected$.pipe(map(hashs => hashs.includes(mapHash))); + return renderMapItem(playlistMap, () => handleOnClickMap({ + map: playlistMap, + mapsSource$: playlistMaps$.pipe(map(Object.values)), + selectedHashs$: playlistHashsSelected$, + noKeyPressedFallBack: () => playlistMaps$.next(Object.fromEntries(Object.entries(playlistMaps$.value).filter(([hash]) => hash !== mapHash))) + }), isSelected$); + }, []); + + const renderList = (maps: T[], render: (item: T) => JSX.Element, scrollEndHandler?: VirtualScrollEndHandler) => { return ( - +
+ +
) } + const addMapsToPlaylist = useCallback(() => { + const tmpLocalMaps = availableMapsSource === 0 ? (localMaps$.value ?? []) : (bsvMaps$.value ?? []) + const mapsToAdd = availabledHashsSelected$.value?.length ? ( + availabledHashsSelected$.value.map(hash => tmpLocalMaps.find(map => getHashOfMap(map) === hash)).filter(Boolean) + ) : tmpLocalMaps; + + const newPlaylistMaps = {...playlistMaps$.value} ?? {}; + mapsToAdd.forEach(map => newPlaylistMaps[getHashOfMap(map)] = map); + + playlistMaps$.next(newPlaylistMaps); + availabledHashsSelected$.next([]); + }, [availableMapsSource]); + + const removeMapsFromPlaylist = useCallback(() => { + const hashsToRemove = playlistHashsSelected$.value?.length ? ( + playlistHashsSelected$.value + ) : Object.keys(playlistMaps$.value ?? {}).filter(hash => playlistMaps$.value[hash]); + + const newPlaylistMaps = {...playlistMaps$.value}; + hashsToRemove.forEach(hash => { delete newPlaylistMaps[hash]; }); + + playlistMaps$.next(newPlaylistMaps); + playlistHashsSelected$.next([]); + }, []); + + const handleSortChange = (newSort: BsvSearchOrder) => { + setBsvSortOrder(() => newSort); + bsvMaps$.next([]); + setBsvSearchParams(() => ({ ...bsvSearchParams, sortOrder: newSort })); + }; + + const loadMoreBsvMaps = () => { + if(bsvLoading){ return; } + setBsvSearchParams(prev => ({ ...prev, page: prev.page + 1 })); + }; + + const handleNewSearch = () => { + if(availableMapsSource === 0){ return; } + bsvMaps$.next([]); + setBsvSearchParams(() => ({ page: 0, filter: availableMapsFilter, q: availableMapsSearch, sortOrder: bsvSearchOrder})); + } + return (
{(() => { - if(!playlistMaps || !maps){ + if(!playlistMaps || !localMaps){ return
Loading...
} else{ @@ -140,29 +292,47 @@ export const EditPlaylistModal: ModalComponent = ({
header
-
-
- - setAvailableMapsSearch(() => e.target.value)} /> - - +
+
+ + setAvailableMapsSearch(() => e.target.value)} /> + {availableMapsSource === 1 && ( + + )} + + filterContainerRef.current.close()}/> + {availableMapsSource === 1 && ( + + )}
- {renderList(maps.filter(map => { - if(playlistMaps[map.hash]){ return false; } - return isLocalMapFitMapFilter({ map, filter: availableMapsFilter, search: availableMapsSearch }); - }), renderAvailableMapItem)} -
-
+ + {availableMapsSource === 0 ? ( + renderList(localMaps.filter(map => { + if(playlistMaps?.[map.hash]){ return false; } + return isMapFitFilter({ map, filter: availableMapsFilter, search: availableMapsSearch }); + }), renderAvailableMapItem) + ) : ( + renderList(bsvMaps, renderBsvMapItem, { onScrollEnd: loadMoreBsvMaps }) + )} +
- - + + + + + +
setPlaylistMapsSearch(() => e.target.value)} /> - +
diff --git a/src/renderer/components/shared/virtual-scroll/virtual-scroll.component.tsx b/src/renderer/components/shared/virtual-scroll/virtual-scroll.component.tsx index adcfb62b..6cc90590 100644 --- a/src/renderer/components/shared/virtual-scroll/virtual-scroll.component.tsx +++ b/src/renderer/components/shared/virtual-scroll/virtual-scroll.component.tsx @@ -14,7 +14,7 @@ export type VirtualScrollClassNames = { rows?: string; } -type ScrollEndHandler = { +export type VirtualScrollEndHandler = { onScrollEnd: () => void; margin?: number; } @@ -29,7 +29,7 @@ type Props = { items: T[]; renderItem: (item: T) => JSX.Element; rowKey?: (rowItems: T[]) => Key; - scrollEnd?: ScrollEndHandler; + scrollEnd?: VirtualScrollEndHandler; } export function VirtualScroll({ className, classNames, minItemWidth, maxColumns, minColumns, itemHeight, items, scrollEnd, renderItem, rowKey}: Props) { diff --git a/src/shared/helpers/type.helpers.ts b/src/shared/helpers/type.helpers.ts index 3df721b6..7ee39b71 100644 --- a/src/shared/helpers/type.helpers.ts +++ b/src/shared/helpers/type.helpers.ts @@ -1 +1,2 @@ export type FieldRequired = Partial & Required>; // All fields of T are optional except for K +export type ObjectValues = T[keyof T]; // All values of T diff --git a/src/shared/mappers/map/map-item-component-props.mapper.ts b/src/shared/mappers/map/map-item-component-props.mapper.ts index ba5098d0..bc64da46 100644 --- a/src/shared/mappers/map/map-item-component-props.mapper.ts +++ b/src/shared/mappers/map/map-item-component-props.mapper.ts @@ -50,6 +50,7 @@ export abstract class MapItemComponentPropsMapper { diffs: MapItemComponentPropsMapper.extractMapDiffs({ rawMapInfo: map.rawInfo, songDetails: map.songDetails }), mapId: map.songDetails?.id, ranked: map.songDetails?.ranked, + blRanked: map.songDetails?.rankedBL, autorId: map.songDetails?.uploader.id, likes: map.songDetails?.upVotes, createdAt: map.songDetails?.uploadedAt, @@ -69,6 +70,7 @@ export abstract class MapItemComponentPropsMapper { likes: map.stats.upvotes, mapId: map.id, ranked: map.ranked, + blRanked: map.blRanked, title: map.name, songAutor: map.metadata.songAuthorName, diffs: MapItemComponentPropsMapper.extractMapDiffs({ bsvMap: map }), @@ -87,6 +89,7 @@ export abstract class MapItemComponentPropsMapper { likes: song.upVotes, mapId: song.id, ranked: song.ranked, + blRanked: song.rankedBL, title: song.name, diffs: MapItemComponentPropsMapper.extractMapDiffs({ songDetails: song }), callBackParam: song diff --git a/src/shared/models/maps/beat-saver.model.ts b/src/shared/models/maps/beat-saver.model.ts index d26adb30..4ab0b5e9 100644 --- a/src/shared/models/maps/beat-saver.model.ts +++ b/src/shared/models/maps/beat-saver.model.ts @@ -1,3 +1,4 @@ +import { ObjectValues } from "shared/helpers/type.helpers"; import { SongDetailDiffCharactertistic, SongDiffName } from "./song-details-cache/song-details-cache.model"; export interface BsvMapDetail { @@ -13,6 +14,9 @@ export interface BsvMapDetail { name: string; qualified: boolean; ranked: boolean; + blQualified: boolean + blRanked: boolean + declaredAi: ObjectValues; stats: BsvMapStats; tags: MapTag[]; updatedAt: BsvInstant; @@ -212,6 +216,14 @@ export enum MapSpecificity { } +//[ Admin, Uploader, SageScore, None ] +export const BsvDeclaredAi = { + Admin: "Admin", + Uploader: "Uploader", + SageScore: "SageScore", + None: "None" +} as const; + export interface MapFilter { automapper?: boolean; chroma?: boolean;