diff --git a/src/main/ipcs/bs-maps-ipcs.ts b/src/main/ipcs/bs-maps-ipcs.ts index 80217aca..146f4a1f 100644 --- a/src/main/ipcs/bs-maps-ipcs.ts +++ b/src/main/ipcs/bs-maps-ipcs.ts @@ -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(request.responceChannel, {success: true}); }).catch(err => { - utils.ipcSend(request.responceChannel, {success: true, error: err}); + utils.ipcSend(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}); + }); }); \ No newline at end of file diff --git a/src/main/services/maps/local-maps-manager.service.ts b/src/main/services/maps/local-maps-manager.service.ts index 2d83781c..900be858 100644 --- a/src/main/services/maps/local-maps-manager.service.ts +++ b/src/main/services/maps/local-maps-manager.service.ts @@ -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{ @@ -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{ 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); + } + } \ No newline at end of file diff --git a/src/main/services/request.service.ts b/src/main/services/request.service.ts index 03ce76fc..5c2c2237 100644 --- a/src/main/services/request.service.ts +++ b/src/main/services/request.service.ts @@ -27,6 +27,7 @@ export class RequestService { public downloadFile(url: string, dest: string): Promise{ return new Promise((resolve, reject) => { + const file = createWriteStream(dest); get(url, res => { res.pipe(file); diff --git a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx index f6cabc9b..994bfeea 100644 --- a/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/local-maps-list-panel.component.tsx @@ -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]); diff --git a/src/renderer/components/maps-mangement-components/map-item.component.tsx b/src/renderer/components/maps-mangement-components/map-item.component.tsx index 3cac5baf..0be1fe39 100644 --- a/src/renderer/components/maps-mangement-components/map-item.component.tsx +++ b/src/renderer/components/maps-mangement-components/map-item.component.tsx @@ -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 ( - 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 && } + 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 && } {(diffsPanelHovered || bottomBarHovered) && ( @@ -205,7 +209,10 @@ export const MapItem = memo(({hash, title, autor, songAutor, coverUrl, songUrl,
- {onDelete && {e.stopPropagation(); onDelete(hash)}}/>} + {onDelete && !downloading && {e.stopPropagation(); onDelete(hash)}}/>} + {onDownload && !downloading && {e.stopPropagation(); onDownload(zipUrl)}}/>} + {onCancelDownload && !downloading && {e.stopPropagation(); onCancelDownload(zipUrl)}}/>} + {downloading && } {previewUrl && {e.stopPropagation(); openPreview()}}/>} {mapId && {e.stopPropagation(); copyBsr()}}/>}
diff --git a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx index 4215d433..dd77aa5d 100644 --- a/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/download-maps-modal.component.tsx @@ -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 = ({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({}); const [query, setQuery] = useState(""); const [maps, setMaps] = useState([]); const [sortOrder, setSortOrder] = useState(BSV_SORT_ORDER.at(0)); - + const [ownedMapHashs, setOwnedMapHashs] = useState([]); const [searchParams, setSearchParams] = useState({ sortOrder: sortOrder, filter: filter, @@ -33,15 +43,34 @@ export const DownloadMapsModal: ModalComponent = ({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 => { const res = new Map(); @@ -57,6 +86,11 @@ export const DownloadMapsModal: ModalComponent = ({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 ( = ({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 = ({data}) => { - setQuery(e.target.value.trim())}/> + setQuery(e.target.value.trim())}/> {e.preventDefault(); handleSearch()}}/> diff --git a/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx b/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx index d5150484..67de04ba 100644 --- a/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx +++ b/src/renderer/components/progress-bar/bsm-progress-bar.component.tsx @@ -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; } diff --git a/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.css b/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.css new file mode 100644 index 00000000..62be5b72 --- /dev/null +++ b/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.css @@ -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); + } +} \ No newline at end of file diff --git a/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.tsx b/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.tsx new file mode 100644 index 00000000..c5d9d7bd --- /dev/null +++ b/src/renderer/components/shared/bsm-basic-spinner/bsm-basic-spinner.component.tsx @@ -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 ( +
+ +
+ ) +} diff --git a/src/renderer/components/svgs/bsm-icon.component.tsx b/src/renderer/components/svgs/bsm-icon.component.tsx index 78f11de1..f3a879ab 100644 --- a/src/renderer/components/svgs/bsm-icon.component.tsx +++ b/src/renderer/components/svgs/bsm-icon.component.tsx @@ -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 } if(icon === "link"){ return } if(icon === "unlink"){ return } + if(icon === "download"){ return } return } diff --git a/src/renderer/components/svgs/icons/download-icon.component.tsx b/src/renderer/components/svgs/icons/download-icon.component.tsx new file mode 100644 index 00000000..b7924067 --- /dev/null +++ b/src/renderer/components/svgs/icons/download-icon.component.tsx @@ -0,0 +1,9 @@ +import { CSSProperties } from "react"; + +export function DownloadIcon(props: {className?: string, style?: CSSProperties}) { + return ( + + + + ) +} diff --git a/src/renderer/helpers/maps-utils.ts b/src/renderer/helpers/maps-utils.ts new file mode 100644 index 00000000..a13745bb --- /dev/null +++ b/src/renderer/helpers/maps-utils.ts @@ -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`; +} \ No newline at end of file diff --git a/src/renderer/services/beat-saver/beat-saver.service.ts b/src/renderer/services/beat-saver/beat-saver.service.ts index 1c2d0071..d1ca9e4b 100644 --- a/src/renderer/services/beat-saver/beat-saver.service.ts +++ b/src/renderer/services/beat-saver/beat-saver.service.ts @@ -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(res.data)); - mapDetails.forEach(detail => this.cachedMapsDetails.set(detail.versions.at(0).hash.toLowerCase(), detail)); + mapDetails.push(...Object.values(res.data).filter(detail => !!detail)); + mapDetails.forEach(detail => { + this.cachedMapsDetails.set(detail.versions.at(0).hash.toLowerCase(), detail); + }); } if(mapDetails.length > 0){ diff --git a/src/renderer/services/maps-downloader.service.ts b/src/renderer/services/maps-downloader.service.ts index 4634a32b..7a03fffa 100644 --- a/src/renderer/services/maps-downloader.service.ts +++ b/src/renderer/services/maps-downloader.service.ts @@ -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 = new BehaviorSubject([]); + private readonly currentDownload$: BehaviorSubject = 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>{ + 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{ + 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>{ + 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 { + return this.mapsQueue$.pipe(map(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 { + return this.currentDownload$.asObservable(); + } + + public get mapsInQueue$(): Observable{ + 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 } \ No newline at end of file diff --git a/src/renderer/services/maps-manager.service.ts b/src/renderer/services/maps-manager.service.ts index 56068906..bc9caf15 100644 --- a/src/renderer/services/maps-manager.service.ts +++ b/src/renderer/services/maps-manager.service.ts @@ -34,12 +34,16 @@ export class MapsManagerService { this.progressBar = ProgressBarService.getInstance(); } - public getMaps(version?: BSVersion): Observable{ + public getMaps(version?: BSVersion, withDetails = true): Observable{ return new Observable(obs => { this.ipcService.send("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 diff --git a/src/renderer/services/progress-bar.service.ts b/src/renderer/services/progress-bar.service.ts index 8d3546c8..509e8f86 100644 --- a/src/renderer/services/progress-bar.service.ts +++ b/src/renderer/services/progress-bar.service.ts @@ -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{ return this._progression$.asObservable(); } public get progress$(): Observable{ return this._progression$.pipe(map(data => data.progression)); } public get progressLabel(): Observable{ return this._progression$.pipe(map(data => data?.label)); }