From c88d93b0abdd99569934e02d377e9c5651b3f398 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Tue, 21 Feb 2023 00:33:33 +0100 Subject: [PATCH] maps optimisation finished + new ipc system + maps progress bars now accurate --- package-lock.json | 43 ++++++++- package.json | 2 + src/main/ipcs/bs-maps-ipcs.ts | 61 ++++-------- src/main/models/archive.class.ts | 78 +++++++++++++++ src/main/preload.ts | 12 ++- .../local-maps-manager.service.ts | 96 ++++++++++++------- .../local-playlists-manager.service.ts | 2 +- src/main/services/bs-installer.service.ts | 2 +- src/main/services/ipc.service.ts | 65 +++++++++++++ .../beat-saver/beat-saver.service.ts | 5 +- src/main/services/window-manager.service.ts | 6 +- .../local-maps-list-panel.component.tsx | 96 ++++++++++++++++--- .../maps-playlists-panel.component.tsx | 2 +- .../download-maps-modal.component.tsx | 25 +++-- .../text-progress-bar.component.tsx | 20 ++++ .../components/shared/bsm-image.component.tsx | 2 +- src/renderer/preload.d.ts | 9 +- src/renderer/services/ipc.service.ts | 28 ++++++ .../services/maps-downloader.service.ts | 22 ++--- src/renderer/services/maps-manager.service.ts | 70 ++++++-------- src/shared/models/archive.interface.ts | 4 + .../models/ipc/ipc-request.interface.ts | 9 +- .../models/ipc/ipc-response.interface.ts | 6 +- .../models/maps/bsm-local-map.interface.ts | 11 +++ 24 files changed, 498 insertions(+), 178 deletions(-) create mode 100644 src/main/models/archive.class.ts create mode 100644 src/main/services/ipc.service.ts create mode 100644 src/renderer/components/progress-bar/text-progress-bar.component.tsx create mode 100644 src/shared/models/archive.interface.ts diff --git a/package-lock.json b/package-lock.json index 69d89f18..f06803bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@tippyjs/react": "^4.2.6", "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", + "@types/recursive-readdir": "^2.2.1", "archiver": "^5.3.1", "dateformat": "^5.0.3", "dot-prop": "^7.2.0", @@ -34,6 +35,7 @@ "react-router-dom": "^6.3.0", "react-virtualized-auto-sizer": "^1.0.7", "react-window": "^1.8.8", + "recursive-readdir": "^2.2.3", "regedit": "^5.1.1", "rfdc": "^1.3.0", "rxjs": "^7.5.6", @@ -2793,8 +2795,7 @@ "node_modules/@types/node": { "version": "17.0.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", - "dev": true + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -2889,6 +2890,14 @@ "@types/react": "*" } }, + "node_modules/@types/recursive-readdir": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/recursive-readdir/-/recursive-readdir-2.2.1.tgz", + "integrity": "sha512-Xd+Ptc4/F2ueInqy5yK2FI5FxtwwbX2+VZpcg+9oYsFJVen8qQKGapCr+Bi5wQtHU1cTXT8s+07lo/nKPgu8Gg==", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -15749,6 +15758,17 @@ "node": ">= 0.10" } }, + "node_modules/recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "dependencies": { + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -21082,8 +21102,7 @@ "@types/node": { "version": "17.0.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.23.tgz", - "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==", - "dev": true + "integrity": "sha512-UxDxWn7dl97rKVeVS61vErvw086aCYhDLyvRQZ5Rk65rZKepaFdm53GeqXaKBuOhED4e9uWq34IC3TdSdJJ2Gw==" }, "@types/parse-json": { "version": "4.0.0", @@ -21178,6 +21197,14 @@ "@types/react": "*" } }, + "@types/recursive-readdir": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@types/recursive-readdir/-/recursive-readdir-2.2.1.tgz", + "integrity": "sha512-Xd+Ptc4/F2ueInqy5yK2FI5FxtwwbX2+VZpcg+9oYsFJVen8qQKGapCr+Bi5wQtHU1cTXT8s+07lo/nKPgu8Gg==", + "requires": { + "@types/node": "*" + } + }, "@types/responselike": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.0.tgz", @@ -30786,6 +30813,14 @@ "resolve": "^1.9.0" } }, + "recursive-readdir": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", + "requires": { + "minimatch": "^3.0.5" + } + }, "redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", diff --git a/package.json b/package.json index 714e5408..6f364b39 100644 --- a/package.json +++ b/package.json @@ -228,6 +228,7 @@ "@tippyjs/react": "^4.2.6", "@types/react-virtualized-auto-sizer": "^1.0.1", "@types/react-window": "^1.8.5", + "@types/recursive-readdir": "^2.2.1", "archiver": "^5.3.1", "dateformat": "^5.0.3", "dot-prop": "^7.2.0", @@ -250,6 +251,7 @@ "react-router-dom": "^6.3.0", "react-virtualized-auto-sizer": "^1.0.7", "react-window": "^1.8.8", + "recursive-readdir": "^2.2.3", "regedit": "^5.1.1", "rfdc": "^1.3.0", "rxjs": "^7.5.6", diff --git a/src/main/ipcs/bs-maps-ipcs.ts b/src/main/ipcs/bs-maps-ipcs.ts index 88f9733e..2abc2284 100644 --- a/src/main/ipcs/bs-maps-ipcs.ts +++ b/src/main/ipcs/bs-maps-ipcs.ts @@ -1,24 +1,21 @@ -import { ipcMain } from "electron"; + import { LocalMapsManagerService } from "../services/additional-content/local-maps-manager.service"; import { UtilsService } from "../services/utils.service"; import { BSVersion } from "shared/bs-version.interface"; import { IpcRequest } from "shared/models/ipc"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { BsvMapDetail } from "shared/models/maps"; +import { IpcService } from "../services/ipc.service"; +import { from } from "rxjs"; -ipcMain.on('get-version-maps', (event, request: IpcRequest) => { - const utilsService = UtilsService.getInstance(); +const ipc = IpcService.getInstance(); + +ipc.on('load-version-maps', async (request: IpcRequest, reply) => { const localMaps = LocalMapsManagerService.getInstance(); - - localMaps.getMaps(request.args).then(maps => { - utilsService.ipcSend(request.responceChannel, {success: true, data: maps}); - }).catch(() => { - utilsService.ipcSend(request.responceChannel, {success: false}); - }) - + reply(localMaps.getMaps(request.args)); }); -ipcMain.on("verion-have-maps-linked", async (event, request: IpcRequest) => { +ipc.on("verion-have-maps-linked", async (request: IpcRequest) => { const utils = UtilsService.getInstance(); const maps = LocalMapsManagerService.getInstance(); @@ -26,7 +23,7 @@ ipcMain.on("verion-have-maps-linked", async (event, request: IpcRequest) => { +ipc.on("link-version-maps", async (request: IpcRequest<{version: BSVersion, keepMaps: boolean}>) => { const utils = UtilsService.getInstance(); const maps = LocalMapsManagerService.getInstance(); @@ -38,7 +35,7 @@ ipcMain.on("link-version-maps", async (event, request: IpcRequest<{version: BSVe }); -ipcMain.on("unlink-version-maps", async (event, request: IpcRequest<{version: BSVersion, keepMaps: boolean}>) => { +ipc.on("unlink-version-maps", async (request: IpcRequest<{version: BSVersion, keepMaps: boolean}>) => { const utils = UtilsService.getInstance(); const maps = LocalMapsManagerService.getInstance(); @@ -50,40 +47,22 @@ ipcMain.on("unlink-version-maps", async (event, request: IpcRequest<{version: BS }); -ipcMain.on("delete-maps", async (event, request: IpcRequest<{maps: BsmLocalMap[]}>) => { - const utils = UtilsService.getInstance(); +ipc.on("delete-maps", async (request: IpcRequest, reply) => { const maps = LocalMapsManagerService.getInstance(); - - maps.deleteMaps(request.args.maps).then(() => { - utils.ipcSend(request.responceChannel, {success: true}); - }).catch(err => { - utils.ipcSend(request.responceChannel, {success: false, error: err}); - }); + reply(maps.deleteMaps(request.args)); }); -ipcMain.on("export-maps", async (event, request: IpcRequest<{version: BSVersion, maps: BsmLocalMap[], outPath: string}>) => { - const utils = UtilsService.getInstance(); +ipc.on("export-maps", async (request: IpcRequest<{version: BSVersion, maps: BsmLocalMap[], outPath: string}>, reply) => { const maps = LocalMapsManagerService.getInstance(); - - maps.exportMaps(request.args.version, request.args.maps, request.args.outPath).then(() => { - utils.ipcSend(request.responceChannel, {success: true}); - }).catch(err => { - utils.ipcSend(request.responceChannel, {success: false, error: err}); - }); + reply(await maps.exportMaps(request.args.version, request.args.maps, request.args.outPath)); }); -ipcMain.on("download-map", async (event, request: IpcRequest<{map: BsvMapDetail, version: BSVersion}>) => { - const utils = UtilsService.getInstance(); +ipc.on("download-map", async (request: IpcRequest<{map: BsvMapDetail, version: BSVersion}>, reply) => { const maps = LocalMapsManagerService.getInstance(); - - maps.downloadMap(request.args.map, request.args.version).then(() => { - utils.ipcSend(request.responceChannel, {success: true}); - }).catch(err => { - utils.ipcSend(request.responceChannel, {success: false, error: err}); - }); + reply(from(maps.downloadMap(request.args.map, request.args.version))) }); -ipcMain.on("one-click-install-map", async (event, request: IpcRequest) => { +ipc.on("one-click-install-map", async (request: IpcRequest) => { const utils = UtilsService.getInstance(); const maps = LocalMapsManagerService.getInstance(); @@ -94,7 +73,7 @@ ipcMain.on("one-click-install-map", async (event, request: IpcRequest) => { +ipc.on("register-maps-deep-link", async (request: IpcRequest) => { const maps = LocalMapsManagerService.getInstance(); const utils = UtilsService.getInstance(); @@ -109,7 +88,7 @@ ipcMain.on("register-maps-deep-link", async (event, request: IpcRequest) = }); -ipcMain.on("unregister-maps-deep-link", async (event, request: IpcRequest) => { +ipc.on("unregister-maps-deep-link", async (request: IpcRequest) => { const maps = LocalMapsManagerService.getInstance(); const utils = UtilsService.getInstance(); @@ -124,7 +103,7 @@ ipcMain.on("unregister-maps-deep-link", async (event, request: IpcRequest) }); -ipcMain.on("is-map-deep-links-enabled", async (event, request: IpcRequest) => { +ipc.on("is-map-deep-links-enabled", async (request: IpcRequest) => { const maps = LocalMapsManagerService.getInstance(); const utils = UtilsService.getInstance(); diff --git a/src/main/models/archive.class.ts b/src/main/models/archive.class.ts new file mode 100644 index 00000000..841dcbcb --- /dev/null +++ b/src/main/models/archive.class.ts @@ -0,0 +1,78 @@ +import archiver from "archiver"; +import { createWriteStream } from "fs"; +import { Observable } from "rxjs"; +import recursive from "recursive-readdir"; +import { lstatSync } from "fs"; +import * as _path from "path"; +import { ArchiveProgress } from "shared/models/archive.interface"; + +export class Archive{ + + private archive: archiver.Archiver; + private readonly output: string; + + private readonly directories: string[] = []; + private readonly files: string[] = []; + + constructor(output: string, options?: archiver.ArchiverOptions){ + this.output = output; + this.initArchive(options); + } + + private initArchive(options?: archiver.ArchiverOptions): void{ + const defaultOptions: archiver.ArchiverOptions = { + zlib: {level: 9} + }; + + this.archive = archiver("zip", {...defaultOptions, ...options}); + } + + private async loadTotalFiles(): Promise{ + let totalFiles = this.files.length; + for(const path of this.directories){ + const files = await recursive(path); + totalFiles += files.length; + } + return totalFiles; + } + + public addDirectory(path: string, destPath?: string|false): void{ + this.directories.push(path); + destPath = destPath === false ? false : _path.basename(path); + this.archive.directory(path, destPath); + } + + public addFile(path: string, destPath?: string): void{ + this.files.push(path); + destPath = destPath ||= _path.basename(path); + this.archive.file(path, {name: destPath}); + } + + + public finalize(): Observable{ + + const progress: ArchiveProgress = { + totalFiles: 0, + prossesedFiles: 0 + }; + + return new Observable(observer => { + (async () => { + + progress.totalFiles = await this.loadTotalFiles(); + + this.archive.on("entry", e => { + if(e.stats.isDirectory()){ return; } + progress.prossesedFiles++; + observer.next(progress); + }); + + this.archive.pipe(createWriteStream(this.output)); + + this.archive.on("error", err => observer.error(err)); + this.archive.finalize().then(() => observer.complete()); + })(); + }); + } + +} \ No newline at end of file diff --git a/src/main/preload.ts b/src/main/preload.ts index 69143b69..00ec0e3e 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -1,21 +1,23 @@ import { contextBridge, ipcRenderer, IpcRendererEvent } from 'electron'; - -export type Channels = any; +import { IpcChannel } from 'shared/models/ipc/ipc-response.interface'; contextBridge.exposeInMainWorld('electron', { ipcRenderer: { - sendMessage(channel: Channels, args: unknown[]) { + sendMessage(channel: IpcChannel, args: unknown[]) { ipcRenderer.send(channel, args); }, - on(channel: Channels, func: (...args: unknown[]) => void) { + on(channel: IpcChannel, func: (...args: unknown[]) => void) { const subscription = (_event: IpcRendererEvent, ...args: unknown[]) => func(...args); ipcRenderer.on(channel, subscription); return () => ipcRenderer.removeListener(channel, subscription); }, - once(channel: Channels, func: (...args: unknown[]) => void) { + once(channel: IpcChannel, func: (...args: unknown[]) => void) { ipcRenderer.once(channel, (_event, ...args) => func(...args)); }, + removeAllListeners(channel: IpcChannel) { + ipcRenderer.removeAllListeners(channel); + } }, }); diff --git a/src/main/services/additional-content/local-maps-manager.service.ts b/src/main/services/additional-content/local-maps-manager.service.ts index 5b31ef11..972593c5 100644 --- a/src/main/services/additional-content/local-maps-manager.service.ts +++ b/src/main/services/additional-content/local-maps-manager.service.ts @@ -1,13 +1,13 @@ import path from "path"; import { BSVersion } from "shared/bs-version.interface"; import { BsvMapDetail, RawMapInfoData } from "shared/models/maps"; -import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; +import { BsmLocalMap, BsmLocalMapsProgress, DeleteMapsProgress } from "shared/models/maps/bsm-local-map.interface"; import { BSLocalVersionService } from "../bs-local-version.service"; import { InstallationLocationService } from "../installation-location.service"; import { UtilsService } from "../utils.service"; import crypto from "crypto"; import { lstatSync, symlinkSync, unlinkSync, readdirSync, createWriteStream } from "fs"; -import { copySync } from "fs-extra"; +import { copy, copySync } from "fs-extra"; import StreamZip from "node-stream-zip"; import { RequestService } from "../request.service"; import sanitize from "sanitize-filename"; @@ -17,6 +17,9 @@ import log from 'electron-log'; import { WindowManagerService } from "../window-manager.service"; import { ipcMain } from "electron"; import { IpcRequest } from 'shared/models/ipc'; +import { Observable } from "rxjs"; +import { defer } from "rxjs"; +import { Archive } from "../../models/archive.class"; export class LocalMapsManagerService { @@ -62,6 +65,7 @@ export class LocalMapsManagerService { } public async getMapsFolderPath(version?: BSVersion): Promise{ + if(version){ return path.join(await this.localVersion.getVersionPath(version), this.LEVELS_ROOT_FOLDER, this.CUSTOM_LEVELS_FOLDER); } const sharedMapsPath = path.join(this.installLocation.sharedMapsPath, this.CUSTOM_LEVELS_FOLDER); if(!(await this.utils.pathExist(sharedMapsPath))){ @@ -125,14 +129,36 @@ export class LocalMapsManagerService { } - public async getMaps(version?: BSVersion): Promise{ - const levelsFolder = await this.getMapsFolderPath(version); + public getMaps(version?: BSVersion): Observable{ - const levelsPath = (await this.utils.pathExist(levelsFolder)) ? this.utils.listDirsInDir(levelsFolder, true) : []; + const progression: BsmLocalMapsProgress = { + total: 0, + loaded: 0, + maps: [] + }; - const mapsInfo = await Promise.all(levelsPath.map(levelPath => this.loadMapInfoFromPath(levelPath))); + return new Observable(observer => { + (async () => { + const levelsFolder = await this.getMapsFolderPath(version); - return mapsInfo.filter(info => !!info); + const levelsPaths = (await this.utils.pathExist(levelsFolder)) ? this.utils.listDirsInDir(levelsFolder, true) : []; + + progression.total = levelsPaths.length; + + for(const levelPath of levelsPaths){ + const mapInfo = await this.loadMapInfoFromPath(levelPath); + if(mapInfo){ + progression.maps.push(mapInfo); + progression.loaded = progression.maps.length; + observer.next({...progression, maps: []}); + } + } + + observer.next(progression); + + observer.complete(); + })(); + }); } public async versionIsLinked(version: BSVersion): Promise{ @@ -174,25 +200,37 @@ export class LocalMapsManagerService { this.utils.createFolderIfNotExist(versionMapsPath); if(keepMaps){ - copySync(sharedMapsPath, versionMapsPath); + await copy(sharedMapsPath, versionMapsPath); } } - public async deleteMaps(maps: BsmLocalMap[]){ + public deleteMaps(maps: BsmLocalMap[]): Observable{ const mapsFolders = maps.map(map => map.path); const mapsHashsToDelete = maps.map(map => map.hash); - for(const folder of mapsFolders){ - const { hash } = await this.loadMapInfoFromPath(folder); - if(!mapsHashsToDelete.includes(hash)){ continue; } - await this.utils.deleteFolder(folder); - } - + return new Observable(observer => { + (async () => { + const progress: DeleteMapsProgress = { total: maps.length, deleted: 0 }; + try{ + for(const folder of mapsFolders){ + const detail = await this.loadMapInfoFromPath(folder); + if(!mapsHashsToDelete.includes(detail?.hash)){ continue; } + await this.utils.deleteFolder(folder); + progress.deleted++; + observer.next(progress); + } + }catch(e){ + console.log(e); + observer.error(e); + } + observer.complete(); + })(); + }); } - public async downloadMap(map: BsvMapDetail, version?: BSVersion): Promise{ + public async downloadMap(map: BsvMapDetail, version?: BSVersion): Promise{ if(!map.versions.at(0).hash){ throw "Cannot download map, no hash found"; } @@ -215,34 +253,26 @@ export class LocalMapsManagerService { unlinkSync(zipPath); - return mapPath; + const localMap = await this.loadMapInfoFromPath(mapPath); + localMap.bsaverInfo = map; + + return localMap; } public async exportMaps(version: BSVersion, maps: BsmLocalMap[], outPath: string){ - const output = createWriteStream(outPath); - const archive = archiver("zip", {zlib: {level: 9}}); - - archive.pipe(output); - archive.on("error", (e) => {throw e}); + const archive = new Archive(outPath); if(!maps || maps.length === 0){ - const mapsFolder = await this.getMapsFolderPath(version); - archive.directory(mapsFolder, false); - + archive.addDirectory(mapsFolder, false); } else{ - const mapsFolders = maps.map(map => map.path); - - for(const folder of mapsFolders){ - archive.directory(folder, path.basename(folder)); - } - + mapsFolders.forEach(maps => archive.addDirectory(maps)); } - await archive.finalize(); + return archive.finalize() } @@ -260,7 +290,7 @@ export class LocalMapsManagerService { this.utils.createFolderIfNotExist(versionMapsPath); - copySync(downloadedMap, path.join(versionMapsPath, path.basename(downloadedMap)), {overwrite: true}); + copySync(downloadedMap.path, path.join(versionMapsPath, path.basename(downloadedMap.path)), {overwrite: true}); } diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts index 3955efb5..ca40a768 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -163,7 +163,7 @@ export class LocalPlaylistsManagerService { ...res.value, current: null, downloadedMaps: [...res.value.downloadedMaps, map], - mapsPath: [...res.value.mapsPath, mapPath], + mapsPath: [...res.value.mapsPath, mapPath.path], progression }); diff --git a/src/main/services/bs-installer.service.ts b/src/main/services/bs-installer.service.ts index 39ba74e1..9f5f1452 100644 --- a/src/main/services/bs-installer.service.ts +++ b/src/main/services/bs-installer.service.ts @@ -28,7 +28,7 @@ export class BSInstallerService{ this.localVersionService = BSLocalVersionService.getInstance(); this.windows = WindowManagerService.getInstance(); - this.windows.getWindows("index.html")?.on("close", () => { + this.windows.getWindow("index.html")?.on("close", () => { this.killDownloadProcess(); }); } diff --git a/src/main/services/ipc.service.ts b/src/main/services/ipc.service.ts new file mode 100644 index 00000000..7f7665ea --- /dev/null +++ b/src/main/services/ipc.service.ts @@ -0,0 +1,65 @@ +import { IpcResponse, IpcRequest } from "shared/models/ipc"; +import { ipcMain } from "electron"; +import { Observable } from "rxjs"; +import { IpcChannel, IpcCompleteChannel, IpcErrorChannel } from "shared/models/ipc/ipc-response.interface"; +import { AppWindow } from "shared/models/window-manager/app-window.model"; +import { WindowManagerService } from "./window-manager.service"; +import { IpcReplier } from "shared/models/ipc/ipc-request.interface"; + +export class IpcService { + + private static instance: IpcService; + + public static getInstance(): IpcService { + if (!IpcService.instance) { IpcService.instance = new IpcService(); } + return IpcService.instance; + } + + private readonly windows: WindowManagerService; + + private constructor() { + this.windows = WindowManagerService.getInstance(); + } + + private getErrorChannel(channel: IpcChannel): IpcErrorChannel { + return `${channel}_error`; + } + + private getCompleteChannel(channel: IpcChannel): IpcCompleteChannel { + return `${channel}_complete`; + } + + private buildProxyListener(listener: (req: IpcRequest, replier: IpcReplier) => void) { + + return (event: Electron.IpcMainEvent, req: IpcRequest) => { + const window = this.windows.getAppWindowFromWebContents(event.sender); + const replier = (data: Observable) => this.connectStream(req.responceChannel, window, data); + listener(req, replier); + } + + } + + public send(channel: IpcChannel, window: AppWindow, response?: T|Error): void{ + this.windows.getWindow(window).webContents.send(channel, response); + } + + public connectStream(channel: IpcChannel, window: AppWindow, observable: Observable): void{ + observable.subscribe(data => { + this.send(channel, window, data); + }, error => { + console.log("LAAAA 2", error); + this.send(this.getErrorChannel(channel), window, error); + }, () => { + this.send(this.getCompleteChannel(channel), window); + }); + } + + public on(channel: IpcChannel, listener: (req: IpcRequest, replier: IpcReplier) => void): void{ + ipcMain.on(channel, this.buildProxyListener(listener)); + } + + public once(channel: IpcChannel, listener: (req: IpcRequest, replier: IpcReplier) => void): void{ + ipcMain.once(channel, this.buildProxyListener(listener)); + } + +} \ No newline at end of file diff --git a/src/main/services/thrid-party/beat-saver/beat-saver.service.ts b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts index 660e98d1..087f2861 100644 --- a/src/main/services/thrid-party/beat-saver/beat-saver.service.ts +++ b/src/main/services/thrid-party/beat-saver/beat-saver.service.ts @@ -32,8 +32,7 @@ export class BeatSaverService { return res; }, [] as BsvMapDetail[]); - for(const hashs of chunkHash){ - + await Promise.all(chunkHash.map(async hashs => { const res = await this.bsaverApi.getMapsDetailsByHashs(hashs); if(res.status === 200){ @@ -42,7 +41,7 @@ export class BeatSaverService { this.cachedMapsDetails.set(detail.versions.at(0).hash.toLowerCase(), detail); }); } - } + })); return mapDetails; } diff --git a/src/main/services/window-manager.service.ts b/src/main/services/window-manager.service.ts index e81a3ec4..c0e59752 100644 --- a/src/main/services/window-manager.service.ts +++ b/src/main/services/window-manager.service.ts @@ -75,8 +75,12 @@ export class WindowManagerService{ }); } - public getWindows(window: AppWindow): BrowserWindow{ + public getWindow(window: AppWindow): BrowserWindow{ return this.windows.get(window); } + public getAppWindowFromWebContents(sender: Electron.WebContents): AppWindow{ + return Array.from(this.windows.entries()).find(([key, value]) => value.webContents.id === sender.id)[0]; + } + } \ No newline at end of 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 6ab4ea1c..53e659a7 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 @@ -9,7 +9,15 @@ import { MapsDownloaderService } from "renderer/services/maps-downloader.service import { VariableSizeList } from "react-window" import { MapsRow } from "./maps-row.component" import { BehaviorSubject } from "rxjs" -import { debounceTime } from "rxjs/operators" +import { debounceTime, last, map, mergeMap, throttleTime } from "rxjs/operators" +import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service" +import { OsDiagnosticService } from "renderer/services/os-diagnostic.service" +import { useTranslation } from "renderer/hooks/use-translation.hook" +import BeatWaitingImg from "../../../../assets/images/apngs/beat-waiting.png" +import BeatConflict from "../../../../assets/images/apngs/beat-conflict.png" +import { BsmImage } from "../shared/bsm-image.component" +import { BsmButton } from "../shared/bsm-button.component" +import TextProgressBar from "../progress-bar/text-progress-bar.component" type Props = { version: BSVersion, @@ -22,22 +30,34 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc const mapsManager = MapsManagerService.getInstance(); const mapsDownloader = MapsDownloaderService.getInstance(); + const bsaver = BeatSaverService.getInstance(); + const os = OsDiagnosticService.getInstance(); + const t = useTranslation(); const ref = useRef(null) const isVisible = useInView(ref, {once: true}); const [maps, setMaps] = useState(null); const [subs] = useState([]); - const [selectedMaps$] = useState>(new BehaviorSubject([])); + const [selectedMaps$] = useState(new BehaviorSubject([])); const [itemPerRow, setItemPerRow] = useState(2); const [listHeight, setListHeight] = useState(0); + const [loadPercent$] = useState(new BehaviorSubject(0)); + useImperativeHandle(forwardRef ,()=>({ deleteMaps(){ - const mapsToDelete = selectedMaps$.value.length === 0 ? maps : selectedMaps$.value - mapsManager.deleteMaps(mapsToDelete, version).then(res => res && removeMapsFromList(mapsToDelete)); + const mapsToDelete = selectedMaps$.value.length === 0 ? maps : selectedMaps$.value; + mapsManager.deleteMaps(mapsToDelete, version).then(res => { + if(!res){ return; } + removeMapsFromList(mapsToDelete); + selectedMaps$.next([]); + }); }, exportMaps(){ - mapsManager.exportMaps(version, selectedMaps$.value) + mapsManager.exportMaps(version, selectedMaps$.value); + }, + getMaps(){ + return maps; } }), [selectedMaps$.value, maps, version]); @@ -47,13 +67,17 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc loadMaps(); subs.push(mapsManager.versionLinked$.subscribe(loadMaps)); subs.push(mapsManager.versionUnlinked$.subscribe(loadMaps)); - mapsDownloader.addOnMapDownloadedListener(loadMaps); + mapsDownloader.addOnMapDownloadedListener((map, targerVersion) => { + if(targerVersion !== version){ return; } + setMaps(maps => maps ? [map, ...maps] : [map]); + }); } return () => { setMaps(() => null); + loadPercent$.next(0); subs.forEach(s => s.unsubscribe()); - mapsDownloader.removeOnMapDownloadedListene(loadMaps); + mapsDownloader.removeOnMapDownloadedListener(loadMaps); } }, [isVisible, version]); @@ -86,7 +110,32 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc }, [isVisible, itemPerRow]) const loadMaps = () => { - subs.push(mapsManager.getMaps(version).subscribe(localMaps => setMaps(() => [...localMaps]))); + + setMaps(() => null); + loadPercent$.next(0); + + const loadMapsObs$ = mapsManager.getMaps(version); + + loadMapsObs$.pipe(map(progess => { + console.log(progess); + return Math.floor(((progess.loaded / progess.total) * 100)); + })).subscribe(percent => loadPercent$.next(percent)); + + subs.push(loadMapsObs$.pipe( + last(), + mergeMap(async progress => { + if(os.isOffline){ return progress.maps; } + const maps = progress.maps; + const details = await bsaver.getMapDetailsFromHashs(maps.map(map => map.hash)); + return maps.map(map => { + map.bsaverInfo = details.find(d => d.versions.at(0).hash === map.hash); + return map; + }) + }) + ).subscribe({ + next: maps => setMaps(() => maps), + complete: () => loadPercent$.next(0) + })); } const removeMapsFromList = (mapsToRemove: BsmLocalMap[]) => { @@ -100,8 +149,6 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc const onMapSelected = useCallback((map: BsmLocalMap) => { - console.log(map); - const mapsCopy = [...selectedMaps$.value]; if(mapsCopy.some(selectedMap => selectedMap.hash === map.hash)){ const i = mapsCopy.findIndex(selectedMap => selectedMap.hash === map.hash); @@ -111,12 +158,9 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc mapsCopy.push(map); } - console.log(mapsCopy); - selectedMaps$.next(mapsCopy); - }, [selectedMaps$.value]); - console.log(selectedMaps$.value); + }, [selectedMaps$.value]); const isMapFitFilter = (map: BsmLocalMap): boolean => { @@ -257,6 +301,30 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc return res; })(); + if(!maps){ + return ( +
+
+ + {t("modals.download-maps.loading-maps")} + +
+
+ ); + } + + if(!maps.length){ + return ( +
+
+ + {t("pages.version-viewer.maps.tabs.maps.empty-maps.text")} + {e.preventDefault(); mapsDownloader.openDownloadMapModal(version)}}/> +
+
+ ); + } + return (
108} itemCount={preppedMaps.length} itemData={preppedMaps} layout="vertical" style={{scrollbarGutter: "stable both-edges"}} itemKey={(i, data) => data[i].map(map => map.hash).join()}> diff --git a/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx index 6606288c..3e5e1484 100644 --- a/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx +++ b/src/renderer/components/maps-mangement-components/maps-playlists-panel.component.tsx @@ -59,7 +59,7 @@ export function MapsPlaylistsPanel({version}: Props) { } const handleMapsAddClick = () => { - mapsDownloader.openDownloadMapModal(version); + mapsDownloader.openDownloadMapModal(version, mapsRef.current.getMaps?.()); } const renderTab = (props: DetailedHTMLProps, HTMLLIElement>, text: string, index: number): JSX.Element => { 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 911310b1..b7c831cf 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 @@ -19,11 +19,11 @@ import equal from "fast-deep-equal/es6"; import { ProgressBarService } from "renderer/services/progress-bar.service"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"; +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; -export const DownloadMapsModal: ModalComponent = ({data}) => { +export const DownloadMapsModal: ModalComponent = ({resolver, data: { ownedMaps, version }}) => { const beatSaver = BeatSaverService.getInstance(); - const mapsManager = MapsManagerService.getInstance(); const mapsDownloader = MapsDownloaderService.getInstance(); const progressBar = ProgressBarService.getInstance(); const os = OsDiagnosticService.getInstance(); @@ -36,7 +36,7 @@ export const DownloadMapsModal: ModalComponent = ({data}) => { const [query, setQuery] = useState(""); const [maps, setMaps] = useState([]); const [sortOrder, setSortOrder] = useState(BSV_SORT_ORDER.at(0)); - const [ownedMapHashs, setOwnedMapHashs] = useState([]); + const [ownedMapHashs, setOwnedMapHashs] = useState(ownedMaps?.map(map => map.hash) ?? []); const [loading, setLoading] = useState(false); const isOnline = useObservable(os.isOnline$); const [searchParams, setSearchParams] = useState({ @@ -58,13 +58,12 @@ export const DownloadMapsModal: ModalComponent = ({data}) => { 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; + const onMapDownloaded = (map: BsmLocalMap, targerVersion: BSVersion) => { + if(!equal(targerVersion, version)){ return; } + const downloadedHash = map.hash; setOwnedMapHashs((prev) => [...prev, downloadedHash]); } + mapsDownloader.addOnMapDownloadedListener(onMapDownloaded); if(mapsDownloader.isDownloading){ @@ -72,7 +71,7 @@ export const DownloadMapsModal: ModalComponent = ({data}) => { } return () => { - mapsDownloader.removeOnMapDownloadedListene(onMapDownloaded); + mapsDownloader.removeOnMapDownloadedListener(onMapDownloaded); progressBar.setStyle(null); } }, []) @@ -99,7 +98,7 @@ export const DownloadMapsModal: ModalComponent = ({data}) => { 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); + const inQueue = mapsInQueue.some(toDownload => equal(toDownload.version, version) && toDownload.map.id === map.id); return ( = ({data}) => { } const handleDownloadMap = useCallback((map: BsvMapDetail) => { - mapsDownloader.addMapToDownload({map, version: data}) + mapsDownloader.addMapToDownload({map, version}) }, []); const handleCancelDownload = useCallback((map: BsvMapDetail) => { - mapsDownloader.removeMapToDownload({map, version: data}); + mapsDownloader.removeMapToDownload({map, version}); }, []); const handleSortChange = (newSort: string) => { @@ -170,7 +169,7 @@ export const DownloadMapsModal: ModalComponent = ({data}) => { {e.preventDefault(); handleSearch()}}/>
-
    +
      {maps.length === 0 ? (
       diff --git a/src/renderer/components/progress-bar/text-progress-bar.component.tsx b/src/renderer/components/progress-bar/text-progress-bar.component.tsx new file mode 100644 index 00000000..87d996ef --- /dev/null +++ b/src/renderer/components/progress-bar/text-progress-bar.component.tsx @@ -0,0 +1,20 @@ +import { CSSProperties } from "react"; +import { useObservable } from "renderer/hooks/use-observable.hook"; +import { Observable } from "rxjs" + +type Props = { + value$: Observable, + className?: string, + style?: CSSProperties +} + +export default function TextProgressBar({value$, className, style}: Props) { + + const value = useObservable(value$); + + const prefix = typeof value === "number" ? "%" : ""; + + return ( + {`${value}${prefix}`} + ) +} diff --git a/src/renderer/components/shared/bsm-image.component.tsx b/src/renderer/components/shared/bsm-image.component.tsx index 3fc99e5d..038cdbb6 100644 --- a/src/renderer/components/shared/bsm-image.component.tsx +++ b/src/renderer/components/shared/bsm-image.component.tsx @@ -37,6 +37,6 @@ export const BsmImage = forwardRef(({className, image, errorImage, placeholder, return ( // @ts-ignore - onClick?.(e)} alt=" "/> + onClick?.(e)} alt=" " decoding="async"/> ) }) diff --git a/src/renderer/preload.d.ts b/src/renderer/preload.d.ts index b8b7e9d0..bce2f371 100644 --- a/src/renderer/preload.d.ts +++ b/src/renderer/preload.d.ts @@ -1,15 +1,16 @@ -import { Channels, ReplyChannels } from 'main/preload'; +import { IpcChannel } from "shared/models/ipc/ipc-response.interface"; declare global { interface Window { electron: { ipcRenderer: { - sendMessage(channel: Channels, args: any): void; + sendMessage(channel: IpcChannel, args: any): void; on( - channel: ReplyChannels, + channel: IpcChannel, func: (...args: any) => void ): (() => void) | undefined; - once(channel: ReplyChannels, func: (...args: any) => void): void; + once(channel: IpcChannel, func: (...args: any) => void): void; + removeAllListeners(channel: IpcChannel): void; }; }; } diff --git a/src/renderer/services/ipc.service.ts b/src/renderer/services/ipc.service.ts index f8bbe1b1..2f9ee7c1 100644 --- a/src/renderer/services/ipc.service.ts +++ b/src/renderer/services/ipc.service.ts @@ -1,5 +1,7 @@ +import { defaultIfEmpty } from "rxjs/operators"; import { Observable } from "rxjs"; import { IpcRequest, IpcResponse } from "shared/models/ipc"; +import { identity } from "rxjs"; export class IpcService { @@ -47,4 +49,30 @@ export class IpcService { return obs; } + // TODO : Convert all IPCs calls to V2 + + public sendV2(channel: string, request?: IpcRequest, defaultValue?: T): Observable{ + if(!request){ request = {args: null, responceChannel: null}; } + if(!request.responceChannel){ request.responceChannel = `${channel}_responce_${new Date().getTime()}`; } + + const completeChannel = `${request.responceChannel}_complete`; + const errorChannel = `${request.responceChannel}_error`; + + const obs = new Observable(observer => { + window.electron.ipcRenderer.on(request.responceChannel, (res: T) => observer.next(res)); + window.electron.ipcRenderer.on(errorChannel, (err: Error) => observer.error(err)); + window.electron.ipcRenderer.on(completeChannel, () => observer.complete()); + }).pipe(defaultValue ? defaultIfEmpty(defaultValue) : identity); + + window.electron.ipcRenderer.once(completeChannel, () => { + window.electron.ipcRenderer.removeAllListeners(request.responceChannel); + window.electron.ipcRenderer.removeAllListeners(errorChannel); + window.electron.ipcRenderer.removeAllListeners(completeChannel); + }); + + window.electron.ipcRenderer.sendMessage(channel, request); + + return obs; + } + } \ No newline at end of file diff --git a/src/renderer/services/maps-downloader.service.ts b/src/renderer/services/maps-downloader.service.ts index 297319b0..ddeb3db8 100644 --- a/src/renderer/services/maps-downloader.service.ts +++ b/src/renderer/services/maps-downloader.service.ts @@ -10,6 +10,7 @@ import { IpcService } from "./ipc.service"; import { OsDiagnosticService } from "./os-diagnostic.service"; import equal from "fast-deep-equal/es6"; import { CSSProperties } from "react"; +import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; export class MapsDownloaderService { @@ -28,7 +29,7 @@ export class MapsDownloaderService { private readonly mapsQueue$: BehaviorSubject = new BehaviorSubject([]); private readonly currentDownload$: BehaviorSubject = new BehaviorSubject(null); private queueMaxLenght = 0; - private downloadedListerners: ((map: BsvMapDetail, version: BSVersion) => void)[] = []; + private downloadedListerners: ((map: BsmLocalMap, version: BSVersion) => void)[] = []; public readonly progressBarStyle: CSSProperties = {zIndex: 100000, position: "fixed", bottom: "10px", right: 0}; private constructor(){ @@ -50,10 +51,10 @@ export class MapsDownloaderService { 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); + const downloaded = await this.downloadMap(toDownload.map, toDownload.version).toPromise(); if(downloaded){ - this.downloadedListerners.forEach(func => func(toDownload.map, toDownload.version)); + this.downloadedListerners.forEach(func => func(downloaded, toDownload.version)); } const newArr = [...this.mapsQueue$.value]; @@ -67,14 +68,13 @@ export class MapsDownloaderService { 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: {map, version}}); - return res.success; + private downloadMap(map: BsvMapDetail, version: BSVersion): Observable{ + if(this.os.isOffline){ return null } + return this.ipc.sendV2("download-map", {args: {map, version}}); } - public async openDownloadMapModal(version?: BSVersion): Promise>{ - const res = await this.modals.openModal(DownloadMapsModal, version); + public async openDownloadMapModal(version?: BSVersion, ownedMaps: BsmLocalMap[] = []): Promise>{ + const res = await this.modals.openModal(DownloadMapsModal, {version, ownedMaps}); this.progressBar.setStyle(null); return res; } @@ -109,11 +109,11 @@ export class MapsDownloaderService { return this.mapsQueue$.asObservable(); } - public addOnMapDownloadedListener(func: (map: BsvMapDetail, version: BSVersion) => void){ + public addOnMapDownloadedListener(func: (map: BsmLocalMap, version: BSVersion) => void){ this.downloadedListerners.push(func); } - public removeOnMapDownloadedListene(func: (map: BsvMapDetail, version: BSVersion) => void){ + public removeOnMapDownloadedListener(func: (map: BsmLocalMap, version: BSVersion) => void){ const funcIndex = this.downloadedListerners.indexOf(func); if(funcIndex < 0){ return; } this.downloadedListerners.splice(funcIndex, 1); diff --git a/src/renderer/services/maps-manager.service.ts b/src/renderer/services/maps-manager.service.ts index 49382ebd..b4ba9147 100644 --- a/src/renderer/services/maps-manager.service.ts +++ b/src/renderer/services/maps-manager.service.ts @@ -2,8 +2,7 @@ import { LinkMapsModal } from "renderer/components/modal/modal-types/link-maps-m import { UnlinkMapsModal } from "renderer/components/modal/modal-types/unlink-maps-modal.component"; import { Subject, Observable } from "rxjs"; import { BSVersion } from "shared/bs-version.interface"; -import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; -import { BeatSaverService } from "./thrird-partys/beat-saver.service"; +import { BsmLocalMapsProgress, BsmLocalMap, DeleteMapsProgress } from "shared/models/maps/bsm-local-map.interface"; import { IpcService } from "./ipc.service"; import { ModalExitCode, ModalService } from "./modale.service"; import { DeleteMapsModal } from "renderer/components/modal/modal-types/delete-maps-modal.component"; @@ -11,6 +10,10 @@ import { ProgressBarService } from "./progress-bar.service"; import { OpenSaveDialogOption } from "shared/models/ipc"; import { NotificationService } from "./notification.service"; import { ConfigurationService } from "./configuration.service"; +import { ArchiveProgress } from "shared/models/archive.interface"; +import { map, last, catchError } from "rxjs/operators"; +import { of } from "rxjs"; +import { ProgressionInterface } from "shared/models/progress-bar"; export class MapsManagerService { @@ -24,7 +27,6 @@ export class MapsManagerService { public static readonly REMEMBER_CHOICE_DELETE_MAP_KEY = "not-confirm-delete-map" private readonly ipcService: IpcService; - private readonly bsaver: BeatSaverService; private readonly modal: ModalService; private readonly progressBar: ProgressBarService; private readonly notifications: NotificationService; @@ -35,32 +37,14 @@ export class MapsManagerService { private constructor(){ this.ipcService = IpcService.getInstance(); - this.bsaver = BeatSaverService.getInstance(); this.modal = ModalService.getInsance(); this.progressBar = ProgressBarService.getInstance(); this.notifications = NotificationService.getInstance(); this.config = ConfigurationService.getInstance(); } - 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.complete();} - - obs.next(res.data); - - if(!withDetails){ return obs.complete(); } - - this.bsaver.getMapDetailsFromHashs(res.data.map(localMap => localMap.hash)).then(mapsDetails => { - mapsDetails.forEach(details => { - const corespondingMap = res.data.find(localMap => localMap.hash === details.versions.find(details => details?.hash === localMap.hash)?.hash); - if(!corespondingMap){ return; } - res.data.find(localMap => localMap.hash === details.versions.find(details => details?.hash === localMap.hash)?.hash).bsaverInfo = details - }); - obs.next(res.data); - }) - }); - }); + public getMaps(version?: BSVersion): Observable{ + return this.ipcService.sendV2("load-version-maps", {args: version}, {loaded: 0, total: 0, maps: []}); } public versionHaveMapsLinked(version: BSVersion): Promise{ @@ -129,17 +113,15 @@ export class MapsManagerService { const showProgressBar = this.progressBar.require(); - if(showProgressBar){ - this.progressBar.showFake(.008); - } + const progress$ = this.ipcService.sendV2("delete-maps", {args: maps}).pipe(map(progress => (progress.deleted / progress.total) * 100)); - const res = await this.ipcService.send("delete-maps", {args: {maps}}); + progress$.subscribe(console.log); - if(showProgressBar){ - this.progressBar.hide(true); - } + showProgressBar && this.progressBar.show(progress$, true); - return res.success; + progress$.toPromise().finally(() => this.progressBar.hide(true)); + + return progress$.pipe(last(), map(() => true), catchError(() => of(false))).toPromise().catch(() => false); } public async exportMaps(version: BSVersion, maps?: BsmLocalMap[]): Promise{ @@ -152,18 +134,22 @@ export class MapsManagerService { if(!resFile.success){ return; } - this.progressBar.showFake(.008); - - const resExport = await this.ipcService.send("export-maps", {args: {version, maps, outPath: resFile.data}}); - if(!resExport.success && resExport.error.title){ - const {title, msg} = resExport.error; - this.notifications.notifyError({title, desc: msg}); - } - else { + const exportProgress$: Observable = await this.ipcService.sendV2("export-maps", {args: {version, maps, outPath: resFile.data}}).pipe( + map(p => { + return { progression: (p.prossesedFiles / p.totalFiles) * 100, label: `${p.prossesedFiles} / ${p.totalFiles}` } as ProgressionInterface + }) + ); + + this.progressBar.show(exportProgress$, true); + + await exportProgress$.toPromise().catch(e => { + this.notifications.notifyError({title: "notifications.types.error", desc: e.message}); + }).then(() => { this.notifications.notifySuccess({title: "Export terminé 🎉", duration: 3000}); - } - this.progressBar.complete(); - this.progressBar.hide(true); + this.progressBar.complete(); + this.progressBar.hide(true); + }); + } public async isDeepLinksEnabled(): Promise{ diff --git a/src/shared/models/archive.interface.ts b/src/shared/models/archive.interface.ts new file mode 100644 index 00000000..438801b3 --- /dev/null +++ b/src/shared/models/archive.interface.ts @@ -0,0 +1,4 @@ +export interface ArchiveProgress{ + totalFiles: number; + prossesedFiles: number; +} \ No newline at end of file diff --git a/src/shared/models/ipc/ipc-request.interface.ts b/src/shared/models/ipc/ipc-request.interface.ts index bcf754c9..c97881eb 100644 --- a/src/shared/models/ipc/ipc-request.interface.ts +++ b/src/shared/models/ipc/ipc-request.interface.ts @@ -1,4 +1,9 @@ +import { Observable } from "rxjs"; +import { IpcResponse } from "./ipc-response.interface"; + export interface IpcRequest{ args?: T, - responceChannel?: string -} \ No newline at end of file + responceChannel?: string, +} + +export type IpcReplier= (data: Observable) => void; \ No newline at end of file diff --git a/src/shared/models/ipc/ipc-response.interface.ts b/src/shared/models/ipc/ipc-response.interface.ts index ac8c512e..e9a3b175 100644 --- a/src/shared/models/ipc/ipc-response.interface.ts +++ b/src/shared/models/ipc/ipc-response.interface.ts @@ -4,4 +4,8 @@ export interface IpcResponse{ data?: T, error?: BsmException, success: boolean, -} \ No newline at end of file +} + +export type IpcChannel = string; +export type IpcErrorChannel = IpcChannel & `${string}_error`; +export type IpcCompleteChannel = IpcChannel & `${string}_complete`; \ No newline at end of file diff --git a/src/shared/models/maps/bsm-local-map.interface.ts b/src/shared/models/maps/bsm-local-map.interface.ts index 2c353465..e4d3e1d6 100644 --- a/src/shared/models/maps/bsm-local-map.interface.ts +++ b/src/shared/models/maps/bsm-local-map.interface.ts @@ -8,4 +8,15 @@ export interface BsmLocalMap { rawInfo: RawMapInfoData, bsaverInfo?: BsvMapDetail path: string +} + +export interface BsmLocalMapsProgress { + total: number, + loaded: number, + maps: BsmLocalMap[], +} + +export interface DeleteMapsProgress { + total: number, + deleted: number } \ No newline at end of file