we can now download maps from bsaver

This commit is contained in:
MathieuG-P
2022-12-06 00:52:37 +01:00
parent 9dce72250e
commit 8930009398
16 changed files with 309 additions and 24 deletions
+12 -1
View File
@@ -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);
}
}
+1
View File
@@ -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>
)
}
+10
View File
@@ -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)); }