create download maps modal

This commit is contained in:
MathieuG-P
2022-12-04 01:08:18 +01:00
parent abdc05298b
commit 341e8aed99
13 changed files with 201 additions and 27 deletions
@@ -1,8 +1,8 @@
import { MapFilter, MapTag } from "shared/models/maps/beat-saver.model"
import {motion} from "framer-motion"
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 { MAP_TYPES } from "renderer/partials/maps/map-tags/map-types"
import { MAP_STYLES } from "renderer/partials/maps/map-tags/map-styles"
import { BsmCheckbox } from "../shared/bsm-checkbox.component"
import { diffColors } from "./map-item.component"
import { min_to_s } from "renderer/helpers/time-utils"
@@ -13,7 +13,7 @@ export type Props = {
className?: string,
ref?: MutableRefObject<undefined>
playlist?: boolean,
filter?: MapFilter
filter: MapFilter
onChange?: (filter: MapFilter) => void
}
@@ -65,7 +65,7 @@ export function FilterPanel({className, ref, playlist = false, filter, onChange}
}
const onNpssChange = ([min, max]: number[]) => {
const newFilter: MapFilter = {...(filter ?? {}), minNps: min, maxNps: max};
const newFilter: MapFilter = {...filter, minNps: min, maxNps: max};
if(max === MAX_NPS){
delete newFilter["maxNps"];
}
@@ -73,7 +73,7 @@ export function FilterPanel({className, ref, playlist = false, filter, onChange}
}
const onDurationsChange = ([min, max]: number[]) => {
const newFilter: MapFilter = {...(filter ?? {}), minDuration: min, maxDuration: max};
const newFilter: MapFilter = {...filter, minDuration: min, maxDuration: max};
if(max === MAX_DURATION){
delete newFilter["maxDuration"];
}
@@ -128,40 +128,40 @@ export function FilterPanel({className, ref, playlist = false, filter, onChange}
<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 cursor-pointer" onClick={e => handleCheckbox("automapper")}>
<div className="flex justify-start items-center h-[22px] 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 cursor-pointer" onClick={e => handleCheckbox("ranked")}>
<div className="flex justify-start items-center h-[22px] 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 cursor-pointer" onClick={e => handleCheckbox("curated")}>
<div className="flex justify-start items-center h-[22px] 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 cursor-pointer" onClick={e => handleCheckbox("verified")}>
<div className="flex justify-start items-center h-[22px] 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 cursor-pointer" onClick={e => handleCheckbox("fullSpread")}>
<div className="flex justify-start items-center h-[22px] 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 cursor-pointer" onClick={e => handleCheckbox("chroma")}>
<div className="flex justify-start items-center h-[22px] 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 cursor-pointer" onClick={e => handleCheckbox("noodle")}>
<div className="flex justify-start items-center h-[22px] 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 cursor-pointer" onClick={e => handleCheckbox("me")}>
<div className="flex justify-start items-center h-[22px] 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 cursor-pointer" onClick={e => handleCheckbox("cinema")}>
<div className="flex justify-start items-center h-[22px] 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>
@@ -10,6 +10,7 @@ import { useThemeColor } from "renderer/hooks/use-theme-color.hook"
import { MapsManagerService } from "renderer/services/maps-manager.service"
import { motion, Variants } from "framer-motion";
import ReactTooltip from 'react-tooltip';
import { MapsDownloaderService } from "renderer/services/maps-downloader.service"
type Props = {
oneBlock?: boolean,
@@ -19,6 +20,7 @@ type Props = {
export function MapsPlaylistsPanel({version, oneBlock = false}: Props) {
const mapsService = MapsManagerService.getInstance();
const mapsDownloader = MapsDownloaderService.getInstance();
const [tabIndex, setTabIndex] = useState(0);
const [mapFilter, setMapFilter] = useState<MapFilter>({
@@ -58,7 +60,7 @@ export function MapsPlaylistsPanel({version, oneBlock = false}: Props) {
}
const handleMapsAddClick = () => {
mapsDownloader.openDownloadMapModal(version);
}
const handlePlaylistAddClick = () => {
@@ -90,7 +92,7 @@ export function MapsPlaylistsPanel({version, oneBlock = false}: Props) {
</motion.div>
<motion.div variants={variants} whileHover="hover" whileTap="tap" initial={{rotate: 0}} className="absolute block p-1 right-11 h-[calc(100%-5px)] aspect-square blur-0 hover:brightness-75" data-tip data-for="add-tooltip">
<span className="absolute top-0 left-0 h-full w-full rounded-full opacity-20" style={{backgroundColor: mainColor}}/>
<BsmButton className="p-0.5 absolute top-0 left-0 h-full w-full !bg-transparent" iconClassName="" icon="add" withBar={false} style={{color: mainColor}} onClick={e => {e.stopPropagation(); onClickLink(index)}}/>
<BsmButton className="p-0.5 absolute top-0 left-0 h-full w-full !bg-transparent" iconClassName="" icon="add" withBar={false} style={{color: mainColor}} onClick={e => {e.stopPropagation(); onClickAdd(index)}}/>
</motion.div>
<ReactTooltip id="add-tooltip" effect="solid" padding="5px" >
<span className="whitespace-nowrap">Ajouté des maps</span> {/* TODO TRANSLATE */}
@@ -0,0 +1,49 @@
import { useEffect, useState } from "react";
import { FilterPanel } from "renderer/components/maps-mangement-components/filter-panel.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component";
import { BSV_SORT_ORDER } from "renderer/partials/beat-saver/sort-order";
import { BeatSaverService } from "renderer/services/beat-saver/beat-saver.service";
import { ModalComponent } from "renderer/services/modale.service";
import { BSVersion } from "shared/bs-version.interface";
import { BsvMapDetail, MapFilter } from "shared/models/maps/beat-saver.model";
export const DownloadMapsModal: ModalComponent<void, BSVersion> = ({data}) => {
const beatSaver = BeatSaverService.getInstance();
const [filter, setFilter] = useState<MapFilter>(null);
const [maps, setMaps] = useState<BsvMapDetail[]>(null);
const [page, setPage] = useState(0);
const [sortOrder, setSortOrder] = useState(BSV_SORT_ORDER.at(0));
const sortOptions: BsmSelectOption[] = (() => {
return BSV_SORT_ORDER.map(sort => ({text: sort, value: sort}));
})();
console.log(maps);
useEffect(() => {
beatSaver.searchMaps({
page, sortOrder
}).then(setMaps);
}, [])
return (
<form className="text-gray-800 dark:text-gray-200 flex flex-col">
<div className="flex h-7 gap-2">
<BsmDropdownButton className="shrink-0 h-full relative z-[1] flex justify-center" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1 !bg-main-color-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={filter} onChange={setFilter}/>
</BsmDropdownButton>
<input className="h-full bg-main-color-1 rounded-full px-2 grow pb-0.5" type="text" name="" id="" placeholder="Rechercher"/>
<BsmButton className="shrink-0 aspect-square rounded-full p-1 !bg-main-color-1" icon="search" withBar={false}/>
<BsmSelect className="bg-main-color-1 rounded-full px-2 pb-0.5" options={sortOptions} onChange={setSortOrder}/>
</div>
<ul>
</ul>
</form>
)
}
@@ -20,8 +20,10 @@ export function Modal() {
<div className="top-0 absolute w-screen h-screen flex content-center items-center justify-center z-50">
<motion.span key="modal-overlay" onClick={() => modalSevice.resolve({exitCode: ModalExitCode.NO_CHOICE})} className="absolute top-0 bottom-0 right-0 left-0 bg-black" initial={{opacity: 0}} animate={{opacity: ModalComponent && .60}} exit={{opacity: 0}} transition={{duration: .2}}/>
<motion.div key="modal" initial={{y: "100vh"}} animate={{y: 0}} exit={{y: "100vh"}}>
<div className="relative p-4 text-gray-800 dark:text-gray-200 overflow-hidden rounded-md shadow-lg shadow-black bg-gradient-to-br from-light-main-color-3 to-light-main-color-2 dark:from-main-color-3 dark:to-main-color-2">
<span className="absolute top-0 w-full left-0 h-1" style={{backgroundImage: `linear-gradient(to right, ${firstColor}, ${secondColor})`}}/>
<div className="relative p-4 text-gray-800 dark:text-gray-200 rounded-md shadow-lg shadow-black bg-gradient-to-br from-light-main-color-3 to-light-main-color-2 dark:from-main-color-3 dark:to-main-color-2">
<div className="absolute top-0 w-full left-0 h-1 rounded-t-md overflow-hidden">
<span className="block w-full h-full" style={{backgroundImage: `linear-gradient(to right, ${firstColor}, ${secondColor})`}}/>
</div>
<ModalComponent resolver={resolver} data={modalData}/>
</div>
</motion.div>
@@ -0,0 +1,28 @@
import { ChangeEvent, CSSProperties } from "react"
type Props = {
className?: string,
style?: CSSProperties,
options?: BsmSelectOption[],
onChange?: (value: any) => void
}
export function BsmSelect({className, style, options, onChange}: Props) {
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
onChange?.(event.target.value);
}
return (
<select className={className} style={style} onChange={handleChange}>
{options && options.map(option => (
<option key={option.value} value={option.value}>{option.text}</option>
))}
</select>
)
}
export interface BsmSelectOption{
text: string,
value: string
}
@@ -3,7 +3,6 @@ import { useState } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import BSLogo from '../../../assets/images/apngs/bs-logo.png';
import { TabNavBar } from 'renderer/components/shared/tab-nav-bar.component';
import wipGif from "../../../assets/images/gifs/wip.gif"
import { BsmDropdownButton } from 'renderer/components/shared/bsm-dropdown-button.component';
import { BsmImage } from 'renderer/components/shared/bsm-image.component';
import { BSUninstallerService } from '../services/bs-uninstaller.service';
@@ -17,7 +16,6 @@ import { LaunchSlide } from 'renderer/components/version-viewer/slides/launch/la
import { ModsSlide } from 'renderer/components/version-viewer/slides/mods/mods-slide.component';
import { UninstallModal } from 'renderer/components/modal/modal-types/uninstall-modal.component';
import { MapsPlaylistsPanel } from 'renderer/components/maps-mangement-components/maps-playlists-panel.component';
import { LocalMapsListPanel } from 'renderer/components/maps-mangement-components/local-maps-list-panel.component';
export function VersionViewer() {
@@ -0,0 +1,8 @@
import { SearchOrder } from "shared/models/maps/beat-saver.model";
export const BSV_SORT_ORDER: SearchOrder[] = [
"Latest",
"Relevance",
"Rating",
"Curated"
];
@@ -15,6 +15,57 @@ export class BeatSaverApiService {
private constructor(){}
private mapFilterToUrlParams(filter: MapFilter): URLSearchParams{
if(!filter){ return new URLSearchParams(); }
const enbledTagsString = Array.from(filter.enabledTags);
const excludedTagsString = Array.from(filter.excludedTags).map(tag => `!${tag}`);
const tags = [...enbledTagsString, excludedTagsString].join("|");
const params = {
automapper: String(filter.automapper ?? ""),
chroma: String(filter.chroma ?? ""),
cinema: String(filter.cinema ?? ""),
me: String(filter.me ?? ""),
noodle: String(filter.noodle ?? ""),
ranked: String(filter.ranked ?? ""),
verified: String(filter.ranked ?? ""),
curated: String(filter.curated ?? ""),
fullSpread : String(filter.fullSpread ?? ""),
from: String(filter.from ?? ""),
to: String(filter.to ?? ""),
tags: tags,
minDuration: String(filter.minDuration ?? ""),
maxDuration: String(filter.maxDuration ?? ""),
minNps: String(filter.minNps ?? ""),
maxNps: String(filter.maxNps ?? ""),
};
return new URLSearchParams(params);
}
private searchParamsToUrlParams(search: SearchParams): URLSearchParams{
if(!search){ return new URLSearchParams(); }
const searchParams = {
includeEmpty: String(search.includeEmpty ?? ""),
sortOrder: String(search.sortOrder ?? ""),
q: String(search.q ?? "")
};
const filterUrlParms = this.mapFilterToUrlParams(search.filter);
return new URLSearchParams({
...searchParams,
...Object.fromEntries(filterUrlParms)
});
}
public async getMapsDetailsByHashs<T extends string>(hashs: T[]): Promise<ApiResult<Record<Lowercase<T>, BsvMapDetail>>>{
if(hashs.length > 50){ throw "too musch map hashs"; }
@@ -37,8 +88,24 @@ export class BeatSaverApiService {
}
public async searchMaps(params: SearchParams): Promise<ApiResult<SearchResponse>>{
public async searchMaps(search: SearchParams): Promise<ApiResult<SearchResponse>>{
const url = new URL(`${this.bsaverApiUrl}/search/text/${search?.page ?? 0}`);
url.search = this.searchParamsToUrlParams(search).toString();
const res = await fetch(url);
console.log(res);
if(!res.ok){
return {status: res.status, data: null};
}
const data = await res.json();
return {status: res.status, data};
}
}
@@ -2,6 +2,7 @@ import { splitIntoChunk } from "renderer/helpers/array-tools";
import { of } from "rxjs";
import { Observable } from "rxjs";
import { BsvMapDetail } from "shared/models/maps";
import { SearchParams } from "shared/models/maps/beat-saver.model";
import { OsDiagnosticService } from "../os-diagnostic.service";
import { BeatSaverApiService } from "./beat-saver-api.service";
@@ -61,8 +62,20 @@ export class BeatSaverService {
observer.complete();
})()
})
});
}
public searchMaps(search: SearchParams): Promise<BsvMapDetail[]>{
return this.bsaverApi.searchMaps(search).then(res => {
return res.status === 200 ? res.data.docs : [];
}).catch(err => {
console.log(err);
return [];
});
}
}
@@ -1,4 +1,6 @@
import { DownloadMapsModal } from "renderer/components/modal/modal-types/download-maps-modal.component";
import { BSVersion } from "shared/bs-version.interface";
import { ModalResponse, ModalService } from "./modale.service";
export class MapsDownloaderService {
@@ -9,12 +11,16 @@ export class MapsDownloaderService {
return MapsDownloaderService.instance;
}
private constructor(){
private readonly modals: ModalService;
private constructor(){
this.modals = ModalService.getInsance();
}
public openDownloadMapModal(version?: BSVersion){
public openDownloadMapModal(version?: BSVersion): Promise<ModalResponse<void>>{
return this.modals.openModal(DownloadMapsModal, version);
}
}
+3 -2
View File
@@ -173,10 +173,11 @@ export interface SearchResponse {
}
export interface SearchParams {
order: SearchOrder,
sortOrder: SearchOrder,
filter?: MapFilter,
page?: number,
q?: string
q?: string,
includeEmpty?: boolean
}
export type SearchOrder = "Latest"|"Relevance"|"Rating"|"Curated";