map filter and search works

This commit is contained in:
MathieuG-P
2022-11-22 00:21:22 +01:00
parent f9edf33097
commit 46f5f9486c
9 changed files with 361 additions and 219 deletions
@@ -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<number[]>([filter?.minNps || MIN_NPS, filter?.maxNps || MAX_NPS]);
const [durations, setDurations] = useState<number[]>([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 (
<span className={`bg-inherit absolute top-[calc(100%+4px)] h-5 font-bold rounded-md shadow-center shadow-black px-1 flex items-center ${isMax ? "text-lg" : "text-sm"}`}>
{text}
</span>
)
}
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<T> = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T];
const handleCheckbox = (key: BooleanKeys<MapFilter>) => {
const newFilter = {...(filter ?? {})};
if(newFilter[key] === true){
delete newFilter[key];
}
else{
newFilter[key] = true;
}
onChange(newFilter);
}
return !playlist ? (
<motion.div ref={ref} className={className} initial={{opacity: 0}} animate={{opacity: 1}} exit={{opacity: 0}}>
<div className="w-full h-6 grid grid-cols-2 gap-x-10 px-4 mb-5 pt-2">
<div>
<Range
values={npss}
min={MIN_NPS}
max={MAX_NPS}
step={.1}
onChange={handleRangeChange}
renderTrack={({props, children}) => (
<div
onMouseDown={props.onMouseDown}
onTouchStart={props.onTouchStart}
className="w-full rounded-full h-1"
{...props}
style={{
...props.style,
background: getTrackBackground({
values: npss,
colors: ["#ccc", firstColor, "#ccc"],
min: MIN_NPS,
max: MAX_NPS
}),
}}
>
{children}
</div>
)}
renderThumb={({ index, props }) => (
<div
className="relative w-4 h-4 rounded-full shadow-center shadow-black brightness-150 flex justify-center outline-none"
{...props}
style={{
...props.style,
backgroundColor: firstColor
}}
>
<span className={`absolute top-[calc(100%+4px)] font-bold w-8 h-5 text-center rounded-md align-middle flex justify-center items-center shadow-sm shadow-black ${npss[index] === MAX_NPS ? "text-xl" : "text-sm"}`} style={{backgroundColor: firstColor, color: thumbTextColor}}>{
npss[index] === MAX_NPS ? "∞" : npss[index] === MIN_NPS ? "NPS" : npss[index].toFixed(1)
}</span>
</div>
)}
/>
</div>
<div>
<Range
values={durations}
min={MIN_DURATION}
max={MAX_DURATION}
step={5}
onChange={handleRangeDurationChange}
renderTrack={({props, children}) => (
<div
onMouseDown={props.onMouseDown}
onTouchStart={props.onTouchStart}
className="w-full rounded-full h-1"
{...props}
style={{
...props.style,
background: getTrackBackground({
values: durations,
colors: ["#ccc", firstColor, "#ccc"],
min: MIN_DURATION,
max: MAX_DURATION
}),
}}
>
{children}
</div>
)}
renderThumb={({ index, props }) => (
<div
className="relative w-4 h-4 rounded-full shadow-center shadow-black brightness-150 flex justify-center outline-none"
{...props}
style={{
...props.style,
backgroundColor: firstColor
}}
>
<span className={`absolute top-[calc(100%+4px)] font-bold min-w-[40px] h-5 text-center rounded-md align-middle flex justify-center items-center shadow-sm shadow-black ${durations[index] === MAX_DURATION ? "text-xl" : "text-sm"}`} style={{backgroundColor: firstColor, color: thumbTextColor}}>{
durations[index] === MAX_DURATION ? "∞" : durations[index] === MIN_DURATION ? "Durée" : secToTimer(durations[index])
}</span>
</div>
)}
/>
</div>
<div className="w-full h-6 grid grid-cols-2 gap-x-10 px-4 mb-6 pt-2">
<BsmRange min={MIN_NPS} max={MAX_NPS} values={npss} onChange={onNpssChange} renderLabel={renderNpsLabel} step={.1}/>
<BsmRange min={MIN_DURATION} max={MAX_DURATION} values={durations} onChange={onDurationsChange} renderLabel={renderDurationLabel} step={5}/>
</div>
<div className="w-full h-full flex gap-x-2">
<div className="w-full h-full flex gap-x-2"> {/* TODO TRADUIRE */}
<section className="shrink-0">
<h2 className="mb-1 uppercase text-sm">general</h2>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" disabled={filter?.automapper}/>
<span className="grow">AI</span>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5 cursor-pointer" onClick={e => handleCheckbox("automapper")}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.automapper} onChange={() => handleCheckbox("automapper")}/>
<span className="grow capitalize">AI</span>
</div>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" disabled={filter?.ranked}/>
<span className="grow">Rancked</span>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5 cursor-pointer" onClick={e => handleCheckbox("ranked")}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.ranked} onChange={() => handleCheckbox("ranked")}/>
<span className="grow capitalize">Rancked</span>
</div>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" disabled={filter?.curated}/>
<span className="grow">Curated</span>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5 cursor-pointer" onClick={e => handleCheckbox("curated")}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.curated} onChange={() => handleCheckbox("curated")}/>
<span className="grow capitalize">Curated</span>
</div>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" disabled={filter?.verified}/>
<span className="grow">Verified Mapper</span>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5 cursor-pointer" onClick={e => handleCheckbox("verified")}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.verified} onChange={() => handleCheckbox("verified")}/>
<span className="grow capitalize">Verified Mapper</span>
</div>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" disabled={filter?.fullSpread}/>
<span className="grow">Full Spread</span>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5 cursor-pointer" onClick={e => handleCheckbox("fullSpread")}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.fullSpread} onChange={() => handleCheckbox("fullSpread")}/>
<span className="grow capitalize">Full Spread</span>
</div>
<h2 className="my-1 uppercase text-sm">requirements</h2>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" disabled={filter?.chroma}/>
<span className="grow">Chroma</span>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5 cursor-pointer" onClick={e => handleCheckbox("chroma")}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.chroma} onChange={() => handleCheckbox("chroma")}/>
<span className="grow capitalize">Chroma</span>
</div>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" disabled={filter?.noodle}/>
<span className="grow">Noodle</span>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5 cursor-pointer" onClick={e => handleCheckbox("noodle")}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.noodle} onChange={() => handleCheckbox("noodle")}/>
<span className="grow capitalize">Noodle</span>
</div>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" disabled={filter?.me}/>
<span className="grow">Mapping Extensions</span>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5 cursor-pointer" onClick={e => handleCheckbox("me")}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.me} onChange={() => handleCheckbox("me")}/>
<span className="grow capitalize" title="Mapping Extensions">Me</span>
</div>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" disabled={filter?.cinema}/>
<span className="grow">Cinema</span>
<div className="flex justify-start items-center h-6 z-20 relative py-0.5 cursor-pointer" onClick={e => handleCheckbox("cinema")}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.cinema} onChange={() => handleCheckbox("cinema")}/>
<span className="grow capitalize">Cinema</span>
</div>
</section>
<section className="grow">
<section className="grow capitalize">
<h2 className="uppercase text-sm mb-1">TAGS</h2>
<div className="w-full flex flex-row flex-wrap items-start justify-start content-start gap-1 mb-2">
{MAP_TYPES.map(tag => (
<span key={tag} className={`text-sm text-black rounded-md px-1 font-bold cursor-pointer ${(!isTagEnabled(tag)) && "opacity-40 hover:opacity-100"}`} style={{backgroundColor: isTagExcluded(tag) ? diffColors.Expert : diffColors.Normal}}>{tag}</span>
<span key={tag} onClick={e => 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}</span>
))}
</div>
<div className="w-full flex flex-row flex-wrap items-start justify-start content-start gap-1">
{MAP_STYLES.map(tag => (
<span key={tag} className={`text-sm text-black rounded-md px-1 font-bold cursor-pointer ${(!isTagEnabled(tag)) && "opacity-40 hover:opacity-100"}`} style={{backgroundColor: isTagExcluded(tag) ? diffColors.Expert : diffColors.Easy}}>{tag}</span>
<span key={tag} onClick={e => 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}</span>
))}
</div>
@@ -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 <MapItem
@@ -95,7 +194,7 @@ export function LocalMapsListPanel({version, className} : Props) {
return (
<div ref={ref} className={className}>
<ul className="p-3 w-full grow flex flex-wrap justify-center content-start gap-2 overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
{maps.map(map => renderMapItem(map))}
{renderMaps()}
</ul>
</div>
)
@@ -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 (
<motion.li className="relative h-[100px] min-w-[400px] shrink-0 grow basis-0 text-white group cursor-pointer" onHoverStart={() => 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)}}>
<motion.li className="relative h-[100px] min-w-[400px] shrink-0 grow basis-0 text-white group cursor-pointer" onHoverStart={() => 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) && <motion.span className="glow-on-hover" animate={{opacity: 1}} transition={{duration: .1, ease: "easeIn"}}/>}
<AnimatePresence>
{(diffsPanelHovered || bottomBarHovered) && (
@@ -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<MapFilter>({
automapper: false,
chroma: false,
cinema: false,
curated: false,
enabledTags: [],
excludedTags: [],
fullSpread: false,
me: false,
noodle: false,
ranked: false,
verified: false,
})
const [mapFilter, setMapFilter] = useState<MapFilter>({
enabledTags: new Set<MapTag>(),
excludedTags: new Set<MapTag>()
});
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) {
<div className="w-full h-full flex flex-col items-center justify-center">
<nav className="w-full shrink-0 flex h-[35px] justify-center px-40 gap-2 mb-3">
<div className="h-full rounded-full bg-main-color-2 grow p-[6px]">
<input type="text" name="" id="" className="h-full w-full bg-main-color-1 rounded-full px-2" placeholder="Rechercher" />
<input type="text" className="h-full w-full bg-main-color-1 rounded-full px-2" placeholder="Rechercher" value={tabIndex === 0 ? mapSearch : playlistSearch} onChange={e => handleSearch(e.target.value)}/>
</div>
<BsmDropdownButton className="h-full relative z-[1] flex justify-center" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1" icon="search" text="Filtres" withBar={false}>
<FilterPanel className="absolute top-[calc(100%+3px)] bg-main-color-3 origin-top w-[500px] h-fit p-2 rounded-md shadow-md shadow-black" filter={{excludedTags: ["Accuracy"]}}/>
<FilterPanel className="absolute top-[calc(100%+3px)] bg-main-color-3 origin-top w-[500px] h-fit p-2 rounded-md shadow-md shadow-black" filter={mapFilter} onChange={setMapFilter}/>
</BsmDropdownButton>
<BsmDropdownButton className="h-full flex aspect-square relative rounded-full z-[1]" buttonClassName="rounded-full h-full w-full p-[6px]" icon="three-dots" withBar={false}>
<></>
@@ -48,7 +46,7 @@ export function MapsPlaylistsPanel({version, oneBlock = false}: Props) {
<div className="w-full h-full flex flex-col bg-main-color-2 rounded-md shadow-black shadow-md overflow-hidden">
{oneBlock && <TabNavBar className="!rounded-none shadow-sm" tabsText={["misc.maps", "Playlists"]} onTabChange={setTabIndex}/>}
<div className="w-full grow min-h-0 flex flex-row items-center transition-transform duration-300" style={{transform: `translate(${-(tabIndex * 100)}%, 0)`}}>
<LocalMapsListPanel className="w-full h-full shrink-0 flex flex-col" version={version}/>
<LocalMapsListPanel className="w-full h-full shrink-0 flex flex-col" version={version} filter={mapFilter} search={mapSearch}/>
<div className="w-full h-full grow shrink-0">b</div>
</div>
</div>
@@ -0,0 +1,63 @@
import { getTrackBackground, Range } from "react-range"
import { getCorrectTextColor } from "renderer/helpers/correct-text-color"
import { ThemeColor, useThemeColor } from "renderer/hooks/use-theme-color.hook"
type Props = {
colorType?: ThemeColor
values: number[],
onChange?: (values: number[]) => void,
onFinalChange?: (values: number[]) => void,
min: number,
max: number,
step?: number,
renderLabel?: (value: number) => JSX.Element,
}
export function BsmRange({colorType = "first-color", values, onChange, onFinalChange, min, max, renderLabel, step = 1} : Props) {
const color = useThemeColor(colorType);
const labelTextColor = getCorrectTextColor(color);
return (
<Range
values={values}
min={min}
max={max}
step={step}
onChange={(v) => onChange?.(v)}
onFinalChange={(v) => onFinalChange?.(v)}
renderTrack={({props, children}) => (
<div
onMouseDown={props.onMouseDown}
onTouchStart={props.onTouchStart}
className="w-full rounded-full h-1"
{...props}
style={{
...props.style,
background: getTrackBackground({
values: values,
colors: ["#ccc", color, "#ccc"],
min: min,
max: max
}),
}}
>
{children}
</div>
)}
renderThumb={({ index, props }) => (
<div
className="relative w-4 h-4 rounded-full shadow-center shadow-black brightness-125 flex justify-center outline-none"
{...props}
style={{
...props.style,
backgroundColor: color,
color: labelTextColor,
}}
>
{renderLabel && renderLabel(values[index])}
</div>
)}
/>
)
}
+5 -3
View File
@@ -3,8 +3,8 @@ import { DefaultConfigKey } from "renderer/config/default-configuration.config";
import { ConfigurationService } from "renderer/services/configuration.service";
export function useThemeColor(): {firstColor: string, secondColor: string};
export function useThemeColor(themeColor: "first-color"|"second-color"): string;
export function useThemeColor(themeColor?: "first-color"|"second-color"): string|{firstColor: string, secondColor: string}{
export function useThemeColor(themeColor: ThemeColor): string;
export function useThemeColor(themeColor?: ThemeColor): string|{firstColor: string, secondColor: string}{
const configService = ConfigurationService.getInstance();
if(themeColor){
@@ -40,4 +40,6 @@ export function useThemeColor(themeColor?: "first-color"|"second-color"): string
return {firstColor, secondColor}
}
}
export type ThemeColor = "first-color"|"second-color";
+37 -36
View File
@@ -1,40 +1,41 @@
import { MapStyle } from "shared/models/maps/beat-saver.model";
export const MAP_STYLES: MapStyle[] = [
"DanceStyle",
"Swing",
"Nightcore",
"Folk",
"Family",
"Ambient",
"Funk",
"Jazz",
"Classical",
"Soul",
"Speedcore",
"Punk",
"RB",
"Holiday",
"Vocaloid",
"JRock",
"Trance",
"DrumBass",
"Comedy",
"Instrumental",
"Hardcore",
"KPop",
"Indie",
"Techno",
"House",
"Game",
"Film",
"Alt",
"Dubstep",
"Metal",
"Anime",
"HipHop",
"JPop",
"Rock",
"Pop",
"Electronic"
"dancestyle",
"swing",
"nightcore",
"folk",
"family",
"ambient",
"funk",
"jazz",
"classical",
"soul",
"speedcore",
"punk",
"rb",
"holiday",
"vocaloid",
"jrock",
"trance",
"drumbass",
"comedy",
"instrumental",
"hardcore",
"kpop",
"indie",
"techno",
"house",
"game",
"film",
"alt",
"dubstep",
"metal",
"anime",
"hiphop",
"jpop",
"rock",
"pop",
"electronic",
"classical-orchestral"
];
+7 -7
View File
@@ -1,11 +1,11 @@
import { MapType } from "shared/models/maps/beat-saver.model";
export const MAP_TYPES: MapType[] = [
"Accuracy",
"Balanced",
"Challenge",
"Dance",
"Fitness",
"Speed",
"Tech"
"accuracy",
"balanced",
"challenge",
"dance",
"fitness",
"speed",
"tech"
];
+4 -4
View File
@@ -143,8 +143,8 @@ export interface BsvMapParitySummary {
export type BsvMapCharacteristic = ("Standard" | "OneSaber" | "NoArrows" | "90Degree" | "360Degree" | "Lightshow" | "Lawless")
export type BsvMapDifficultyType = ("Easy" | "Normal" | "Hard" | "Expert" | "ExpertPlus")
export type MapStyle = ("DanceStyle" | "Swing" | "Nightcore" | "Folk" | "Family" | "Ambient" | "Funk" | "Jazz" | "Classical" | "Soul" | "Speedcore" | "Punk" | "RB" | "Holiday" | "Vocaloid" | "JRock" | "Trance" | "DrumBass" | "Comedy" | "Instrumental" | "Hardcore" | "KPop" | "Indie" | "Techno" | "House" | "Game" | "Film" | "Alt" | "Dubstep" | "Metal" | "Anime" | "HipHop" | "JPop" | "Rock" | "Pop" | "Electronic")
export type MapType = ("Accuracy" | "Balanced" | "Challenge" | "Dance" | "Fitness" | "Speed" | "Tech")
export type MapStyle = ("dancestyle" | "swing" | "nightcore" | "folk" | "family" | "ambient" | "funk" | "jazz" | "classical" | "soul" | "speedcore" | "punk" | "rb" | "holiday" | "vocaloid" | "jrock" | "trance" | "drumbass" | "comedy" | "instrumental" | "hardcore" | "kpop" | "indie" | "techno" | "house" | "game" | "film" | "alt" | "dubstep" | "metal" | "anime" | "hiphop" | "jpop" | "rock" | "pop" | "electronic" | "classical-orchestral")
export type MapType = ("accuracy" | "balanced" | "challenge" | "dance" | "fitness" | "speed" | "tech")
export type MapTag = MapStyle | MapType
export interface MapFilter {
@@ -163,6 +163,6 @@ export interface MapFilter {
maxDuration?: number,
minNps?: number,
maxNps?: number,
enabledTags?: MapTag[],
excludedTags?: MapTag[]
enabledTags?: Set<MapTag>,
excludedTags?: Set<MapTag>
}