diff --git a/src/renderer/components/maps-mangement-components/filter-panel.component.tsx b/src/renderer/components/maps-mangement-components/filter-panel.component.tsx index e64b4b75..c9e901c8 100644 --- a/src/renderer/components/maps-mangement-components/filter-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/filter-panel.component.tsx @@ -1,15 +1,13 @@ import { MapFilter, MapTag } from "shared/models/maps/beat-saver.model" import {motion} from "framer-motion" -import { MutableRefObject, useRef, useState } from "react" +import { MutableRefObject} from "react" import { MAP_TYPES } from "renderer/partials/map-tags/map-types" import { MAP_STYLES } from "renderer/partials/map-tags/map-styles" import { BsmCheckbox } from "../shared/bsm-checkbox.component" import { diffColors } from "./map-item.component" -import { getTrackBackground, Range } from 'react-range'; -import { useThemeColor } from "renderer/hooks/use-theme-color.hook" -import { getCorrectTextColor } from "renderer/helpers/correct-text-color" -import { hour_to_s, min_to_s } from "renderer/helpers/time-utils" +import { min_to_s } from "renderer/helpers/time-utils" import dateFormat from "dateformat" +import { BsmRange } from "../shared/bsm-range.component" export type Props = { className?: string, @@ -27,177 +25,158 @@ export function FilterPanel({className, ref, playlist = false, filter, onChange} const MIN_DURATION = 0; const MAX_DURATION = min_to_s(30); - const [npss, setNpss] = useState([filter?.minNps || MIN_NPS, filter?.maxNps || MAX_NPS]); - const [durations, setDurations] = useState([filter?.minDuration || MIN_DURATION, filter?.maxDuration || MAX_DURATION]); - const firstColor = useThemeColor("first-color"); - const thumbTextColor = getCorrectTextColor(firstColor); + const npss = [filter?.minNps || MIN_NPS, filter?.maxNps || MAX_NPS]; + const durations = [filter?.minDuration || MIN_DURATION, filter?.maxDuration || MAX_DURATION]; - const isTagEnabled = (tag: MapTag): boolean => { - return filter?.enabledTags?.some(t => t === tag) || filter?.excludedTags?.some(t => t === tag); + const isTagActivated = (tag: MapTag): boolean => filter?.enabledTags?.has(tag) || filter?.excludedTags?.has(tag); + const isTagExcluded = (tag: MapTag): boolean => filter?.excludedTags?.has(tag); + + const renderDurationLabel = (sec: number): JSX.Element => { + + const textValue = (() => { + if(sec === MIN_DURATION){ return "Durée"; } //TODO TRADUIRE + if(sec === MAX_DURATION){ return "∞"; } + const date = new Date(0); + date.setSeconds(sec); + return sec > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss"); + })(); + + return renderLabel(textValue, sec === MAX_DURATION); } - const isTagExcluded = (tag: MapTag): boolean => { - return filter?.excludedTags?.some(t => t === tag); + const renderNpsLabel = (nps: number): JSX.Element => { + + const textValue = (() => { + if(nps === MIN_NPS){ return "NPS"; } + if(nps === MAX_NPS){ return "∞"; } + return nps; + })(); + + return renderLabel(textValue, nps === MAX_NPS); + + } + + const renderLabel = (text: unknown, isMax: boolean): JSX.Element => { + return ( + + {text} + + ) } - const secToTimer = (sec: number): string => { - const date = new Date(0); - date.setSeconds(sec); - return sec > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss"); + const onNpssChange = ([min, max]: number[]) => { + const newFilter: MapFilter = {...(filter ?? {}), minNps: min, maxNps: max}; + if(max === MAX_NPS){ + delete newFilter["maxNps"]; + } + onChange(newFilter); } - const handleRangeChange = ([min, max]: number[]) => { - setNpss(() => [min, max]); - onChange?.({...filter, minNps: min, maxNps: max}); + const onDurationsChange = ([min, max]: number[]) => { + const newFilter: MapFilter = {...(filter ?? {}), minDuration: min, maxDuration: max}; + if(max === MAX_DURATION){ + delete newFilter["maxDuration"]; + } + onChange(newFilter); } - const handleRangeDurationChange = ([min, max]: number[]) => { - setDurations([min, max]); - onChange?.({...filter, minDuration: min, maxDuration: max}); + const handleTagClick = (tag: MapTag) => { + if(filter.enabledTags.has(tag)){ + filter.enabledTags.delete(tag); + filter.excludedTags.add(tag); + return onChange({ + ...filter, + enabledTags: filter.enabledTags, + excludedTags: filter.excludedTags + }) + } + if(filter.excludedTags.has(tag)){ + filter.excludedTags.delete(tag); + return onChange({ + ...filter, + excludedTags: filter.excludedTags + }) + } + + filter.enabledTags.add(tag); + onChange({ + ...filter, + enabledTags: filter.enabledTags + }) + } - console.log(hour_to_s(2)); + type BooleanKeys = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T]; + + const handleCheckbox = (key: BooleanKeys) => { + const newFilter = {...(filter ?? {})}; + if(newFilter[key] === true){ + delete newFilter[key]; + } + else{ + newFilter[key] = true; + } + onChange(newFilter); + } return !playlist ? ( -
-
- ( -
- {children} -
- )} - renderThumb={({ index, props }) => ( -
- { - npss[index] === MAX_NPS ? "∞" : npss[index] === MIN_NPS ? "NPS" : npss[index].toFixed(1) - } -
- )} - /> -
-
- ( -
- {children} -
- )} - renderThumb={({ index, props }) => ( -
- { - durations[index] === MAX_DURATION ? "∞" : durations[index] === MIN_DURATION ? "Durée" : secToTimer(durations[index]) - } -
- )} - /> -
+
+ +
-
+
{/* TODO TRADUIRE */}

general

-
- - AI +
handleCheckbox("automapper")}> + handleCheckbox("automapper")}/> + AI
-
- - Rancked +
handleCheckbox("ranked")}> + handleCheckbox("ranked")}/> + Rancked
-
- - Curated +
handleCheckbox("curated")}> + handleCheckbox("curated")}/> + Curated
-
- - Verified Mapper +
handleCheckbox("verified")}> + handleCheckbox("verified")}/> + Verified Mapper
-
- - Full Spread +
handleCheckbox("fullSpread")}> + handleCheckbox("fullSpread")}/> + Full Spread

requirements

-
- - Chroma +
handleCheckbox("chroma")}> + handleCheckbox("chroma")}/> + Chroma
-
- - Noodle +
handleCheckbox("noodle")}> + handleCheckbox("noodle")}/> + Noodle
-
- - Mapping Extensions +
handleCheckbox("me")}> + handleCheckbox("me")}/> + Me
-
- - Cinema +
handleCheckbox("cinema")}> + handleCheckbox("cinema")}/> + Cinema
-
+

TAGS

{MAP_TYPES.map(tag => ( - {tag} + handleTagClick(tag)} className={`text-sm text-black rounded-md px-1 font-bold cursor-pointer ${(!isTagActivated(tag)) && "opacity-40 hover:opacity-100"}`} style={{backgroundColor: isTagExcluded(tag) ? diffColors.Expert : diffColors.Normal}}>{tag} ))}
{MAP_STYLES.map(tag => ( - {tag} + handleTagClick(tag)} className={`text-sm text-black rounded-md px-1 font-bold cursor-pointer ${(!isTagActivated(tag)) && "opacity-40 hover:opacity-100"}`} style={{backgroundColor: isTagExcluded(tag) ? diffColors.Expert : diffColors.Easy}}>{tag} ))}
diff --git a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx index d611b835..234c381b 100644 --- a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx @@ -1,20 +1,20 @@ import { MapsManagerService } from "renderer/services/maps-manager.service" import { BSVersion } from "shared/bs-version.interface" -import VisibilitySensor from "react-visibility-sensor" import { useEffect, useRef, useState } from "react" import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface" import { Subscription } from "rxjs" -import { MapItem, MapItemProps, ParsedMapDiff } from "./map-item.component" -import { BsvMapCharacteristic, BsvMapDifficultyType } from "shared/models/maps/beat-saver.model" +import { MapItem, ParsedMapDiff } from "./map-item.component" +import { BsvMapCharacteristic, MapFilter } from "shared/models/maps/beat-saver.model" import { useInView } from "framer-motion" -import { MapsToolbar } from "./maps-toolbar.component" type Props = { version: BSVersion, - className?: string + className?: string, + filter?: MapFilter + search?: string, } -export function LocalMapsListPanel({version, className} : Props) { +export function LocalMapsListPanel({version, className, filter, search} : Props) { const mapsManager = MapsManagerService.getInstance(); @@ -33,7 +33,6 @@ export function LocalMapsListPanel({version, className} : Props) { return () => { setMaps(() => []); - console.log("DESTORYED"); subs.forEach(s => s.unsubscribe()); } }, [isVisible, version]) @@ -73,6 +72,106 @@ export function LocalMapsListPanel({version, className} : Props) { setSelectedMaps(() => hashs); } + const isMapFitFilter = (map: BsmLocalMap): boolean => { + + const fitEnabledTags = (() => { + if(!filter?.enabledTags || filter.enabledTags.size === 0){ return true; } + if(!map?.bsaverInfo?.tags){ return false; } + return Array.from(filter.enabledTags.values()).every(tag => map.bsaverInfo.tags.some(mapTag => mapTag === tag)); + })(); + + const fitExcluedTags = (() => { + if(!filter?.excludedTags || filter.excludedTags.size === 0){ return true; } + if(!map?.bsaverInfo?.tags){ return true; } + return !map.bsaverInfo.tags.some(tag => filter.excludedTags.has(tag)); + })(); + + const fitMinNps = (() => { + if(!filter?.minNps){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return !map.bsaverInfo.versions.some(version => { + return version.diffs.some(diff => diff.nps < filter.minNps); + }); + })(); + + const fitMaxNps = (() => { + if(!filter?.maxNps){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return !map.bsaverInfo.versions.some(version => { + return version.diffs.some(diff => diff.nps > filter.maxNps); + }); + })(); + + const fitMinDuration = (() => { + if(!filter?.minDuration){ return true; } + + if(!map?.bsaverInfo?.metadata?.duration){ return false; } + return map.bsaverInfo.metadata.duration >= filter.minDuration; + })(); + + const fitMaxDuration = (() => { + if(!filter?.maxDuration){ return true; } + if(!map?.bsaverInfo?.metadata?.duration){ return false; } + return map.bsaverInfo.metadata.duration <= filter.maxDuration; + })(); + + const fitNoodle = (() => { + if(!filter?.noodle){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.ne)); + })(); + + const fitMe = (() => { + if(!filter?.me){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.me)); + })(); + + const fitCinema = (() => { + if(!filter?.cinema){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.cinema)); + })(); + + const fitChroma =(() => { + if(!filter?.chroma){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.chroma)); + })(); + + const fitFullSpread = (() => { + if(!filter?.fullSpread){ return true; } + if(!map?.bsaverInfo?.versions?.at(0)){ return false; } + return map.bsaverInfo.versions.some(version => version?.diffs?.length >= 5); + })(); + + const fitAutoMapper = filter?.automapper ? map.bsaverInfo?.automapper === filter.automapper : true; + const fitRanked = filter?.ranked ? map.bsaverInfo?.ranked === filter.ranked : true; + const fitCurated = filter?.curated ? !!map.bsaverInfo?.curatedAt : true; + const fitVerified = filter?.verified ? !!map.bsaverInfo?.uploader?.verifiedMapper : true; + + const filterCheck = fitEnabledTags && fitExcluedTags && fitMinNps && fitMaxNps && fitMinDuration && fitMaxDuration && fitNoodle && fitMe && fitCinema && fitChroma && fitFullSpread && fitAutoMapper && fitRanked && fitCurated && fitVerified; + + const searchCheck = (() => { + return ( + ((map.rawInfo?._songName ?? map.bsaverInfo?.name) || "")?.toLowerCase().includes(search.toLowerCase()) || + ((map.rawInfo?._songAuthorName ?? map.bsaverInfo?.metadata?.songAuthorName) || "")?.toLowerCase().includes(search.toLowerCase()) || + ((map.rawInfo?._levelAuthorName ?? map.bsaverInfo?.metadata?.levelAuthorName) || "")?.toLowerCase().includes(search.toLowerCase())); + })(); + + return filterCheck && searchCheck; + + } + + const renderMaps = (): JSX.Element[] => { + return maps.reduce((acc, current) => { + if(isMapFitFilter(current)){ + acc.push(renderMapItem(current)); + } + return acc + }, []); + } + const renderMapItem = (map: BsmLocalMap) => { return
    - {maps.map(map => renderMapItem(map))} + {renderMaps()}
) diff --git a/src/renderer/components/maps-mangement-components/map-item.component.tsx b/src/renderer/components/maps-mangement-components/map-item.component.tsx index cfaa1f31..196d059c 100644 --- a/src/renderer/components/maps-mangement-components/map-item.component.tsx +++ b/src/renderer/components/maps-mangement-components/map-item.component.tsx @@ -5,7 +5,7 @@ import { BsmLink } from "../shared/bsm-link.component"; import { BsmIcon } from "../svgs/bsm-icon.component"; import { BsmButton } from "../shared/bsm-button.component"; import { AnimatePresence, motion } from "framer-motion"; -import { useState, Fragment } from "react"; +import { useState, Fragment, memo } from "react"; import { LinkOpenerService } from "renderer/services/link-opener.service"; import dateFormat from "dateformat"; import { AudioPlayerService } from "renderer/services/audio-player.service"; @@ -127,7 +127,7 @@ export function MapItem({hash, title, autor, songAutor, coverUrl, songUrl, autor } return ( - setHovered(true)} onHoverEnd={() => setHovered(false)} style={{zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden"}} onClick={e => {e.stopPropagation(); onSelected(hash)}}> + setHovered(true)} onHoverEnd={() => setHovered(false)} style={{zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden"}} onClick={e => {onSelected(hash)}}> {(hovered || selected) && } {(diffsPanelHovered || bottomBarHovered) && ( diff --git a/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx index e8654302..dfc10a3e 100644 --- a/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx @@ -2,10 +2,9 @@ import { useState } from "react" import { BSVersion } from "shared/bs-version.interface" import { TabNavBar } from "../shared/tab-nav-bar.component" import { LocalMapsListPanel } from "./local-maps-list-panel.component" -import {AnimatePresence, motion} from "framer-motion" import { BsmDropdownButton } from "../shared/bsm-dropdown-button.component" import { FilterPanel } from "./filter-panel.component" -import { MapFilter } from "shared/models/maps/beat-saver.model" +import { MapFilter, MapTag } from "shared/models/maps/beat-saver.model" type Props = { oneBlock?: boolean, @@ -15,20 +14,19 @@ type Props = { export function MapsPlaylistsPanel({version, oneBlock = false}: Props) { const [tabIndex, setTabIndex] = useState(0); - const [filter, setFilter] = useState({ - automapper: false, - chroma: false, - cinema: false, - curated: false, - enabledTags: [], - excludedTags: [], - fullSpread: false, - me: false, - noodle: false, - ranked: false, - verified: false, - - }) + const [mapFilter, setMapFilter] = useState({ + enabledTags: new Set(), + excludedTags: new Set() + }); + const [mapSearch, setMapSearch] = useState(""); + const [playlistSearch, setPlaylistSearch] = useState(""); + + const handleSearch = (value: string) => { + if(tabIndex === 0){ + return setMapSearch(() => value); + } + return setPlaylistSearch(() => value); + } return ( <> @@ -36,10 +34,10 @@ export function MapsPlaylistsPanel({version, oneBlock = false}: Props) {