mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
we can now download maps from bsaver
This commit is contained in:
@@ -56,7 +56,18 @@ ipcMain.on("delete-maps", async (event, request: IpcRequest<{version: BSVersion,
|
||||
maps.deleteMaps(request.args.maps, request.args.version).then(() => {
|
||||
utils.ipcSend<void>(request.responceChannel, {success: true});
|
||||
}).catch(err => {
|
||||
utils.ipcSend<void>(request.responceChannel, {success: true, error: err});
|
||||
utils.ipcSend<void>(request.responceChannel, {success: false, error: err});
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on("download-map", async (event, request: IpcRequest<{zipUrl: string, version: BSVersion}>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const maps = LocalMapsManagerService.getInstance();
|
||||
|
||||
maps.downloadMap(request.args.zipUrl, request.args.version).then(() => {
|
||||
utils.ipcSend(request.responceChannel, {success: true});
|
||||
}).catch(err => {
|
||||
console.log(err);
|
||||
utils.ipcSend(request.responceChannel, {success: false, error: err});
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,8 @@ import { UtilsService } from "../utils.service";
|
||||
import crypto from "crypto";
|
||||
import { lstatSync, symlinkSync, unlinkSync, readdirSync } from "fs";
|
||||
import { copySync } from "fs-extra";
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { RequestService } from "../request.service";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
|
||||
@@ -24,11 +26,13 @@ export class LocalMapsManagerService {
|
||||
private readonly localVersion: BSLocalVersionService;
|
||||
private readonly installLocation: InstallationLocationService;
|
||||
private readonly utils: UtilsService;
|
||||
private readonly reqService: RequestService
|
||||
|
||||
private constructor(){
|
||||
this.localVersion = BSLocalVersionService.getInstance();
|
||||
this.installLocation = InstallationLocationService.getInstance();
|
||||
this.utils = UtilsService.getInstance();
|
||||
this.reqService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
private async getMapsFolderPath(version?: BSVersion): Promise<string>{
|
||||
@@ -73,6 +77,18 @@ export class LocalMapsManagerService {
|
||||
return {rawInfo, coverUrl, songUrl, hash};
|
||||
}
|
||||
|
||||
private async downloadMapZip(zipUrl: string): Promise<{zip: StreamZip.StreamZipAsync, zipPath: string}>{
|
||||
const fileName = path.basename(zipUrl);
|
||||
const tempPath = this.utils.getTempPath();
|
||||
this.utils.createFolderIfNotExist(this.utils.getTempPath());
|
||||
const dest = path.join(tempPath, fileName);
|
||||
|
||||
const zipPath = await this.reqService.downloadFile(zipUrl, dest);
|
||||
const zip = new StreamZip.async({file : zipPath});
|
||||
|
||||
return {zip, zipPath};
|
||||
}
|
||||
|
||||
public async getMaps(version?: BSVersion): Promise<BsmLocalMap[]>{
|
||||
const levelsFolder = await this.getMapsFolderPath(version);
|
||||
|
||||
@@ -146,6 +162,27 @@ export class LocalMapsManagerService {
|
||||
|
||||
}
|
||||
|
||||
public async downloadMap(zipUrl: string, version?: BSVersion){
|
||||
|
||||
console.log(zipUrl, version);
|
||||
|
||||
const mapsFolder = await this.getMapsFolderPath(version);
|
||||
|
||||
const {zip, zipPath} = await this.downloadMapZip(zipUrl);
|
||||
const zipName = path.parse(zipPath).name;
|
||||
|
||||
const mapPath = path.join(mapsFolder, zipName);
|
||||
|
||||
if(!zip){ throw `Cannot download ${zipUrl}`; }
|
||||
|
||||
this.utils.createFolderIfNotExist(mapPath);
|
||||
|
||||
await zip.extract(null, mapPath);
|
||||
await zip.close();
|
||||
|
||||
unlinkSync(zipPath);
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -27,6 +27,7 @@ export class RequestService {
|
||||
|
||||
public downloadFile(url: string, dest: string): Promise<string>{
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
const file = createWriteStream(dest);
|
||||
get(url, res => {
|
||||
res.pipe(file);
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Subscription } from "rxjs"
|
||||
import { MapItem, ParsedMapDiff } from "./map-item.component"
|
||||
import { BsvMapCharacteristic, MapFilter } from "shared/models/maps/beat-saver.model"
|
||||
import { useInView } from "framer-motion"
|
||||
import { MapsDownloaderService } from "renderer/services/maps-downloader.service"
|
||||
|
||||
type Props = {
|
||||
version: BSVersion,
|
||||
@@ -17,6 +18,7 @@ type Props = {
|
||||
export function LocalMapsListPanel({version, className, filter, search} : Props) {
|
||||
|
||||
const mapsManager = MapsManagerService.getInstance();
|
||||
const mapsDownloader = MapsDownloaderService.getInstance();
|
||||
|
||||
const ref = useRef()
|
||||
const isVisible = useInView(ref, {once: true});
|
||||
@@ -30,11 +32,13 @@ export function LocalMapsListPanel({version, className, filter, search} : Props)
|
||||
loadMaps();
|
||||
subs.push(mapsManager.versionLinked$.subscribe(loadMaps));
|
||||
subs.push(mapsManager.versionUnlinked$.subscribe(loadMaps));
|
||||
mapsDownloader.addOnMapDownloadedListener(loadMaps);
|
||||
}
|
||||
|
||||
return () => {
|
||||
setMaps(() => []);
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
mapsDownloader.removeOnMapDownloadedListene(loadMaps);
|
||||
}
|
||||
}, [isVisible, version]);
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import { map } from "rxjs/operators";
|
||||
import useDelayedState from "use-delayed-state";
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import equal from "fast-deep-equal/es6";
|
||||
import { getMapZipUrlFromHash } from "renderer/helpers/maps-utils";
|
||||
import { BsmBasicSpinner } from "../shared/bsm-basic-spinner/bsm-basic-spinner.component";
|
||||
|
||||
export type ParsedMapDiff = {type: BsvMapDifficultyType, name: string, stars: number}
|
||||
|
||||
@@ -34,12 +36,14 @@ export type MapItemProps = {
|
||||
likes: number,
|
||||
createdAt: string,
|
||||
selected?: boolean,
|
||||
downloading?: boolean,
|
||||
onDelete?: (hash: string) => void,
|
||||
onDownload?: (id: string) => void,
|
||||
onDownload?: (zipUrl: string) => void,
|
||||
onSelected?: (hash: string) => void,
|
||||
onCancelDownload?: (zipUrl: string) => void
|
||||
}
|
||||
|
||||
export const MapItem = memo(({hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, qualified, ranked, bpm, duration, likes, createdAt, selected, onDelete, onDownload, onSelected}: MapItemProps) => {
|
||||
export const MapItem = memo(({hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, qualified, ranked, bpm, duration, likes, createdAt, selected, downloading, onDelete, onDownload, onSelected, onCancelDownload}: MapItemProps) => {
|
||||
|
||||
const linkOpener = LinkOpenerService.getInstance();
|
||||
const audioPlayer = AudioPlayerService.getInstance();
|
||||
@@ -54,7 +58,7 @@ export const MapItem = memo(({hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
|
||||
const songPlaying = useObservable(audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl)));
|
||||
|
||||
const zipUrl = `https://r2cdn.beatsaver.com/${hash}.zip`;
|
||||
const zipUrl = getMapZipUrlFromHash(hash);
|
||||
const previewUrl = mapId ? `https://skystudioapps.com/bs-viewer/?url=${zipUrl}` : null;
|
||||
const mapUrl = mapId ? `https://beatsaver.com/maps/${mapId}` : null;
|
||||
const authorUrl = autorId ? `https://beatsaver.com/profile/${autorId}` : null;
|
||||
@@ -128,8 +132,8 @@ export const MapItem = memo(({hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.li className="relative h-[100px] min-w-[370px] 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) && onSelected && <motion.span className="glow-on-hover" animate={{opacity: 1}} transition={{duration: .1, ease: "easeIn"}}/>}
|
||||
<motion.li className="relative h-[100px] min-w-[370px] 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) && onSelected && <motion.span className="glow-on-hover !transition-none" animate={{opacity: 1}} transition={{duration: .2, ease: "linear"}}/>}
|
||||
<AnimatePresence>
|
||||
{(diffsPanelHovered || bottomBarHovered) && (
|
||||
<motion.ul key={hash} className="absolute top-[calc(100%-10px)] w-full h-fit max-h-[200%] pt-4 pb-2 px-2 overflow-y-scroll bg-main-color-3 brightness-125 rounded-md flex flex-col gap-3 scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 shadow-sm shadow-black" initial={{opacity: 0}} animate={{opacity: 1}} exit={{opacity: 0}} transition={{duration: .15}} onHoverStart={diffsPanelHoverStart} onHoverEnd={diffsPanelHoverEnd}>
|
||||
@@ -205,7 +209,10 @@ export const MapItem = memo(({hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
<span className="absolute w-[10px] h-[10px] bottom-0 right-full bg-inherit" style={{clipPath: 'path("M11 11 L11 0 L10 0 A10 10 0 0 1 0 10 L 0 11 Z")'}}/>
|
||||
|
||||
<div className="flex flex-col justify-center items-center gap-1 w-full h-full overflow-hidden opacity-0 group-hover:opacity-100">
|
||||
{onDelete && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-main-color-2" iconClassName="w-full h-full brightness-150" iconColor={color} icon="trash" withBar={false} onClick={e => {e.stopPropagation(); onDelete(hash)}}/>}
|
||||
{onDelete && !downloading && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-main-color-2" iconClassName="w-full h-full brightness-150" iconColor={color} icon="trash" withBar={false} onClick={e => {e.stopPropagation(); onDelete(hash)}}/>}
|
||||
{onDownload && !downloading && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-main-color-2" iconClassName="w-full h-full brightness-150" iconColor={color} icon="download" withBar={false} onClick={e => {e.stopPropagation(); onDownload(zipUrl)}}/>}
|
||||
{onCancelDownload && !downloading && <BsmButton className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-main-color-2" iconClassName="w-full h-full brightness-150" iconColor={"red"} icon="cross" withBar={false} onClick={e => {e.stopPropagation(); onCancelDownload(zipUrl)}}/>}
|
||||
{downloading && <BsmBasicSpinner className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-main-color-2 flex items-center justify-center" spinnerClassName="brightness-150" style={{color}} thikness="3px"/>}
|
||||
{previewUrl && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-main-color-2" iconClassName="w-full h-full brightness-150" iconColor={color} icon="eye" withBar={false} onClick={e => {e.stopPropagation(); openPreview()}}/>}
|
||||
{mapId && <BsmButton className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-main-color-2" iconClassName="w-full h-full brightness-150" iconColor={color} icon="twitch" withBar={false} onClick={e => {e.stopPropagation(); copyBsr()}}/>}
|
||||
</div>
|
||||
|
||||
@@ -1,26 +1,36 @@
|
||||
import { motion, useInView } from "framer-motion";
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FilterPanel } from "renderer/components/maps-mangement-components/filter-panel.component";
|
||||
import { MapItem, ParsedMapDiff } from "renderer/components/maps-mangement-components/map-item.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 { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { BSV_SORT_ORDER } from "renderer/partials/beat-saver/sort-order";
|
||||
import { BeatSaverService } from "renderer/services/beat-saver/beat-saver.service";
|
||||
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
|
||||
import { MapsManagerService } from "renderer/services/maps-manager.service";
|
||||
import { ModalComponent } from "renderer/services/modale.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { BsvMapCharacteristic, BsvMapDetail, MapFilter, SearchParams } from "shared/models/maps/beat-saver.model";
|
||||
import BeatWaitingImg from "../../../../../assets/images/apngs/beat-waiting.png"
|
||||
import BeatWaitingImg from "../../../../../assets/images/apngs/beat-waiting.png";
|
||||
import equal from "fast-deep-equal/es6";
|
||||
import { ProgressBarService } from "renderer/services/progress-bar.service";
|
||||
|
||||
export const DownloadMapsModal: ModalComponent<void, BSVersion> = ({data}) => {
|
||||
|
||||
const beatSaver = BeatSaverService.getInstance();
|
||||
const mapsManager = MapsManagerService.getInstance();
|
||||
const mapsDownloader = MapsDownloaderService.getInstance();
|
||||
const progressBar = ProgressBarService.getInstance();
|
||||
|
||||
const currentDownload = useObservable(mapsDownloader.currentMapDownload$);
|
||||
const mapsInQueue = useObservable(mapsDownloader.mapsInQueue$)
|
||||
const [filter, setFilter] = useState<MapFilter>({});
|
||||
const [query, setQuery] = useState("");
|
||||
const [maps, setMaps] = useState<BsvMapDetail[]>([]);
|
||||
const [sortOrder, setSortOrder] = useState(BSV_SORT_ORDER.at(0));
|
||||
|
||||
const [ownedMapHashs, setOwnedMapHashs] = useState<string[]>([]);
|
||||
const [searchParams, setSearchParams] = useState<SearchParams>({
|
||||
sortOrder: sortOrder,
|
||||
filter: filter,
|
||||
@@ -33,15 +43,34 @@ export const DownloadMapsModal: ModalComponent<void, BSVersion> = ({data}) => {
|
||||
const sortOptions: BsmSelectOption[] = (() => {
|
||||
return BSV_SORT_ORDER.map(sort => ({text: sort, value: sort}));
|
||||
})();
|
||||
|
||||
const loadMaps = (params: SearchParams) => {
|
||||
beatSaver.searchMaps(params).then((maps => setMaps(prev => [...prev, ...maps])));
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
loadMaps(searchParams);
|
||||
}, [searchParams]);
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
mapsManager.getMaps(data, false).toPromise().then(maps => setOwnedMapHashs(maps.map(map => map.hash)));
|
||||
|
||||
const onMapDownloaded = (map: BsvMapDetail, verion: BSVersion) => {
|
||||
if(!equal(verion, data) || !map?.versions){ return; }
|
||||
const downloadedHash = map.versions.at(0).hash;
|
||||
setOwnedMapHashs((prev) => [...prev, downloadedHash]);
|
||||
}
|
||||
mapsDownloader.addOnMapDownloadedListener(onMapDownloaded);
|
||||
|
||||
if(mapsDownloader.isDownloading){
|
||||
progressBar.setStyle(mapsDownloader.progressBarStyle);
|
||||
}
|
||||
|
||||
return () => {
|
||||
mapsDownloader.removeOnMapDownloadedListene(onMapDownloaded);
|
||||
progressBar.setStyle(null);
|
||||
}
|
||||
}, [])
|
||||
|
||||
const loadMaps = (params: SearchParams) => {
|
||||
beatSaver.searchMaps(params).then((maps => setMaps(prev => [...prev, ...maps])));
|
||||
}
|
||||
|
||||
const extractMapDiffs = (map: BsvMapDetail): Map<BsvMapCharacteristic, ParsedMapDiff[]> => {
|
||||
const res = new Map<BsvMapCharacteristic, ParsedMapDiff[]>();
|
||||
@@ -57,6 +86,11 @@ export const DownloadMapsModal: ModalComponent<void, BSVersion> = ({data}) => {
|
||||
}
|
||||
|
||||
const renderMap = (map: BsvMapDetail) => {
|
||||
|
||||
const isMapOwned = map.versions.some(version => ownedMapHashs.includes(version.hash));
|
||||
const isDownloading = map.id === currentDownload?.map?.id;
|
||||
const inQueue = mapsInQueue.some(toDownload => equal(toDownload.version, data) && toDownload.map.id === map.id);
|
||||
|
||||
return (
|
||||
<MapItem
|
||||
autor={map.metadata.levelAuthorName}
|
||||
@@ -75,10 +109,21 @@ export const DownloadMapsModal: ModalComponent<void, BSVersion> = ({data}) => {
|
||||
diffs={extractMapDiffs(map)}
|
||||
songUrl={map.versions.at(0).previewURL}
|
||||
key={map.id}
|
||||
onDownload={(!isMapOwned && !inQueue) && (() => {handleDownloadMap(map)})}
|
||||
onCancelDownload={(inQueue && !isDownloading) && (() => {handleCancelDownload(map)})}
|
||||
downloading={isDownloading}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const handleDownloadMap = (map: BsvMapDetail) => {
|
||||
mapsDownloader.addMapToDownload({map, version: data});
|
||||
}
|
||||
|
||||
const handleCancelDownload = (map: BsvMapDetail) => {
|
||||
mapsDownloader.removeMapToDownload({map, version: data});
|
||||
}
|
||||
|
||||
const handleSearch = () => {
|
||||
const searchParams: SearchParams = {
|
||||
sortOrder: sortOrder,
|
||||
@@ -103,7 +148,7 @@ export const DownloadMapsModal: ModalComponent<void, BSVersion> = ({data}) => {
|
||||
<BsmDropdownButton className="shrink-0 h-full relative z-[1] flex justify-start" 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-[450px] 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" value={query} onChange={e => setQuery(e.target.value.trim())}/>
|
||||
<input className="h-full bg-main-color-1 rounded-full px-2 grow pb-0.5" type="text" name="" id="" placeholder="Rechercher une map" value={query} onChange={e => setQuery(e.target.value.trim())}/>
|
||||
<BsmButton className="shrink-0 aspect-square rounded-full p-1 !bg-main-color-1" icon="search" withBar={false} onClick={e => {e.preventDefault(); handleSearch()}}/>
|
||||
<BsmSelect className="bg-main-color-1 rounded-full px-2 pb-0.5" options={sortOptions} onChange={setSortOrder}/>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,8 @@ export function BsmProgressBar() {
|
||||
const visible = useObservable(progressBarService.visible$);
|
||||
const style = useObservable(progressBarService.style$);
|
||||
|
||||
console.log("STYLES", style);
|
||||
|
||||
const progressLabel = (() => {
|
||||
if(!progressData){ return ""; }
|
||||
if(progressData?.label){ return progressData.label; }
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
.loader {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { CSSProperties } from "react"
|
||||
import "./bsm-basic-spinner.component.css"
|
||||
|
||||
type Props = {
|
||||
className?: string,
|
||||
style?: CSSProperties,
|
||||
spinnerClassName?: string,
|
||||
thikness?: string
|
||||
}
|
||||
|
||||
export function BsmBasicSpinner({className, style, spinnerClassName, thikness = "5px"}: Props) {
|
||||
return (
|
||||
<div className={className} style={style}>
|
||||
<span className={`${spinnerClassName} loader`} style={{border: `${thikness} solid currentColor`, borderBottomColor: "transparent"}}/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -41,11 +41,12 @@ import { NinetyDregreeIcon } from "./icons/ninety-dregree-icon.component";
|
||||
import { ThreeSixtyDegreeIcon } from "./icons/three-sixty-degree-icon.component";
|
||||
import { LinkIcon } from "./icons/link-icon.component";
|
||||
import { UnlinkIcon } from "./icons/unlink-icon.component";
|
||||
import { DownloadIcon } from "./icons/download-icon.component";
|
||||
|
||||
export type BsmIconType = BsvMapCharacteristic | (
|
||||
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|"twitch"|"eye"|"play"|"checkCircleIcon"|
|
||||
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|"github"|"close"|"thumbUpFill"|"timerFill"|"pause"|
|
||||
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|"bsMapDifficulty"|"link"|"unlink"|
|
||||
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|"bsMapDifficulty"|"link"|"unlink"|"download"|
|
||||
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"
|
||||
);
|
||||
|
||||
@@ -93,6 +94,7 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
|
||||
if(icon === "360Degree"){ return <ThreeSixtyDegreeIcon className={className} style={style}/> }
|
||||
if(icon === "link"){ return <LinkIcon className={className} style={style}/> }
|
||||
if(icon === "unlink"){ return <UnlinkIcon className={className} style={style}/> }
|
||||
if(icon === "download"){ return <DownloadIcon className={className} style={style}/> }
|
||||
return <TrashIcon className={className} style={style}/>
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function DownloadIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40">
|
||||
<path fill="currentColor" d="M20 26q-.292 0-.583-.125-.292-.125-.542-.333l-6.292-6.334q-.458-.458-.437-1.125.021-.666.479-1.125.458-.458 1.104-.458.646 0 1.104.458l3.584 3.584V7.458q0-.666.458-1.125.458-.458 1.125-.458t1.125.458q.458.459.458 1.125v13.084l3.584-3.584q.458-.458 1.125-.458.666 0 1.125.458.458.459.458 1.125 0 .667-.458 1.125l-6.292 6.334q-.25.208-.542.333Q20.292 26 20 26ZM9.292 33.875q-1.292 0-2.23-.937-.937-.938-.937-2.23v-4.583q0-.667.458-1.125.459-.458 1.125-.458.667 0 1.125.458.459.458.459 1.125v4.583h21.416v-4.583q0-.667.459-1.125.458-.458 1.125-.458.666 0 1.125.458.458.458.458 1.125v4.583q0 1.292-.937 2.23-.938.937-2.23.937Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
|
||||
export function getMapZipUrlFromMapDetails(map: BsvMapDetail){
|
||||
const hash = map.versions.at(0).hash;
|
||||
return getMapZipUrlFromHash(hash);
|
||||
}
|
||||
|
||||
export function getMapZipUrlFromHash(hash: string){
|
||||
return `https://r2cdn.beatsaver.com/${hash}.zip`;
|
||||
}
|
||||
@@ -45,13 +45,19 @@ export class BeatSaverService {
|
||||
return new Observable(observer => {
|
||||
(async () => {
|
||||
|
||||
if(mapDetails.length > 0){
|
||||
observer.next(mapDetails);
|
||||
}
|
||||
|
||||
for(const hashs of chunkHash){
|
||||
|
||||
const res = await this.bsaverApi.getMapsDetailsByHashs(hashs);
|
||||
|
||||
if(res.status === 200){
|
||||
mapDetails.push(...Object.values<BsvMapDetail>(res.data));
|
||||
mapDetails.forEach(detail => this.cachedMapsDetails.set(detail.versions.at(0).hash.toLowerCase(), detail));
|
||||
mapDetails.push(...Object.values<BsvMapDetail>(res.data).filter(detail => !!detail));
|
||||
mapDetails.forEach(detail => {
|
||||
this.cachedMapsDetails.set(detail.versions.at(0).hash.toLowerCase(), detail);
|
||||
});
|
||||
}
|
||||
|
||||
if(mapDetails.length > 0){
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { DownloadMapsModal } from "renderer/components/modal/modal-types/download-maps-modal.component";
|
||||
import { map, filter } from "rxjs/operators";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { ModalResponse, ModalService } from "./modale.service";
|
||||
import { Observable } from "rxjs";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { BsvMapDetail } from "shared/models/maps";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { getMapZipUrlFromMapDetails } from "renderer/helpers/maps-utils";
|
||||
import { OsDiagnosticService } from "./os-diagnostic.service";
|
||||
import { timer } from "rxjs";
|
||||
import equal from "fast-deep-equal/es6";
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export class MapsDownloaderService {
|
||||
|
||||
@@ -12,15 +24,111 @@ export class MapsDownloaderService {
|
||||
}
|
||||
|
||||
private readonly modals: ModalService;
|
||||
private readonly progressBar: ProgressBarService;
|
||||
private readonly ipc: IpcService;
|
||||
private readonly os: OsDiagnosticService;
|
||||
|
||||
private readonly mapsQueue$: BehaviorSubject<MapDownload[]> = new BehaviorSubject([]);
|
||||
private readonly currentDownload$: BehaviorSubject<MapDownload> = new BehaviorSubject(null);
|
||||
private queueMaxLenght = 0;
|
||||
private downloadedListerners: ((map: BsvMapDetail, version: BSVersion) => void)[] = [];
|
||||
public readonly progressBarStyle: CSSProperties = {zIndex: 100000, position: "fixed", bottom: "10px", right: 0};
|
||||
|
||||
private constructor(){
|
||||
this.modals = ModalService.getInsance();
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
this.ipc = IpcService.getInstance();
|
||||
this.os = OsDiagnosticService.getInstance();
|
||||
|
||||
this.mapsQueue$.pipe(filter(queue => queue.length === 1 && !this.isDownloading)).subscribe(() => this.startDownloadMaps());
|
||||
this.mapsQueue$.pipe(filter(queue => queue.length === 0)).subscribe(() => this.queueMaxLenght = 0);
|
||||
}
|
||||
|
||||
public openDownloadMapModal(version?: BSVersion): Promise<ModalResponse<void>>{
|
||||
private async startDownloadMaps(){
|
||||
|
||||
this.progressBar.show(this.downloadProgress$, true, this.progressBarStyle);
|
||||
|
||||
await timer(2000).toPromise();
|
||||
|
||||
while(this.mapsQueue$.value.at(0)){
|
||||
const toDownload = this.mapsQueue$.value.at(0);
|
||||
this.currentDownload$.next(toDownload);
|
||||
const downloaded = await this.downloadMap(toDownload.map, toDownload.version);
|
||||
|
||||
if(downloaded){
|
||||
this.downloadedListerners.forEach(func => func(toDownload.map, toDownload.version));
|
||||
}
|
||||
|
||||
const newArr = [...this.mapsQueue$.value];
|
||||
newArr.shift();
|
||||
this.mapsQueue$.next(newArr);
|
||||
}
|
||||
|
||||
await timer(500).toPromise();
|
||||
|
||||
return this.modals.openModal(DownloadMapsModal, version);
|
||||
|
||||
this.currentDownload$.next(null);
|
||||
this.progressBar.hide(true);
|
||||
}
|
||||
|
||||
private async downloadMap(map: BsvMapDetail, version: BSVersion): Promise<boolean>{
|
||||
if(this.os.isOffline){ return false }
|
||||
const res = await this.ipc.send("download-map", {args: {zipUrl: getMapZipUrlFromMapDetails(map), version}});
|
||||
return res.success;
|
||||
}
|
||||
|
||||
public async openDownloadMapModal(version?: BSVersion): Promise<ModalResponse<void>>{
|
||||
const res = await this.modals.openModal(DownloadMapsModal, version);
|
||||
this.progressBar.setStyle(null);
|
||||
return res;
|
||||
}
|
||||
|
||||
public addMapToDownload(downloadMap: MapDownload){
|
||||
if(this.mapsQueue$.value.length === 0 && !this.progressBar.require()){ return; }
|
||||
this.queueMaxLenght++;
|
||||
this.mapsQueue$.next([...this.mapsQueue$.value, downloadMap]);
|
||||
}
|
||||
|
||||
public removeMapToDownload(downloadMap: MapDownload){
|
||||
const newArr = [...this.mapsQueue$.value];
|
||||
const index = newArr.findIndex(toDownload => toDownload.map.id === downloadMap.map.id && equal(toDownload.version, downloadMap.version));
|
||||
if(index < 0){ return; }
|
||||
newArr.splice(index, 1);
|
||||
this.mapsQueue$.next(newArr);
|
||||
}
|
||||
|
||||
public get downloadProgress$(): Observable<ProgressionInterface> {
|
||||
return this.mapsQueue$.pipe(map<MapDownload[], ProgressionInterface>(download => {
|
||||
let progress = this.queueMaxLenght === 0 ? 100 : ((100 / this.queueMaxLenght) * (this.queueMaxLenght - download.length)) + .1;
|
||||
progress = progress > 100 ? 100 : progress;
|
||||
return {progression: (progress), label: download.at(0)?.map.name || ""};
|
||||
}));
|
||||
}
|
||||
|
||||
public get currentMapDownload$(): Observable<MapDownload> {
|
||||
return this.currentDownload$.asObservable();
|
||||
}
|
||||
|
||||
public get mapsInQueue$(): Observable<MapDownload[]>{
|
||||
return this.mapsQueue$.asObservable();
|
||||
}
|
||||
|
||||
public addOnMapDownloadedListener(func: (map: BsvMapDetail, version: BSVersion) => void){
|
||||
this.downloadedListerners.push(func);
|
||||
}
|
||||
|
||||
public removeOnMapDownloadedListene(func: (map: BsvMapDetail, version: BSVersion) => void){
|
||||
const funcIndex = this.downloadedListerners.indexOf(func);
|
||||
if(funcIndex < 0){ return; }
|
||||
this.downloadedListerners.splice(funcIndex, 1);
|
||||
}
|
||||
|
||||
public get isDownloading(): boolean{
|
||||
return !!this.currentDownload$.value;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface MapDownload {
|
||||
map: BsvMapDetail
|
||||
version: BSVersion
|
||||
}
|
||||
@@ -34,12 +34,16 @@ export class MapsManagerService {
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
}
|
||||
|
||||
public getMaps(version?: BSVersion): Observable<BsmLocalMap[]>{
|
||||
public getMaps(version?: BSVersion, withDetails = true): Observable<BsmLocalMap[]>{
|
||||
return new Observable(obs => {
|
||||
this.ipcService.send<BsmLocalMap[], BSVersion>("get-version-maps", {args: version}).then(res => {
|
||||
if(!res.success){ return obs.next(null);}
|
||||
obs.next(res.data);
|
||||
|
||||
if(!withDetails){ return obs.complete(); }
|
||||
|
||||
console.log("GET DETAILS");
|
||||
|
||||
this.bsaver.getMapDetailsFromHashs(res.data.map(localMap => localMap.hash)).pipe(finalize(() => obs.complete())).subscribe(mapsDetails => {
|
||||
mapsDetails.forEach(details => {
|
||||
res.data.find(localMap => localMap.hash === details.versions.find(details => details?.hash === localMap.hash)?.hash).bsaverInfo = details
|
||||
|
||||
@@ -86,6 +86,11 @@ export class ProgressBarService{
|
||||
return true;
|
||||
}
|
||||
|
||||
public setStyle(style: CSSProperties){
|
||||
console.log("SET STYLE", style);
|
||||
this._style$.next(style);
|
||||
}
|
||||
|
||||
public get progressData$(): Observable<ProgressionInterface>{ return this._progression$.asObservable(); }
|
||||
public get progress$(): Observable<number>{ return this._progression$.pipe(map(data => data.progression)); }
|
||||
public get progressLabel(): Observable<string>{ return this._progression$.pipe(map(data => data?.label)); }
|
||||
|
||||
Reference in New Issue
Block a user