mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
map filter and search works
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
+107
-8
@@ -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) && (
|
||||
|
||||
+17
-19
@@ -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>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -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";
|
||||
@@ -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"
|
||||
];
|
||||
@@ -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"
|
||||
];
|
||||
@@ -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>
|
||||
}
|
||||
Reference in New Issue
Block a user