mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
create download maps modal
This commit is contained in:
@@ -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>
|
||||
|
||||
+4
-2
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
Reference in New Issue
Block a user