From 7c88ea46e0a577162a58a5af3c2a32798c35f386 Mon Sep 17 00:00:00 2001 From: silentrald Date: Tue, 10 Sep 2024 00:03:02 +0800 Subject: [PATCH] [feat-75] support multiple drag and drop zip files for maps fixed issue in map downloads where download listener in MapsDownloaderService was not being removed --- assets/jsons/translations/de.json | 3 +- assets/jsons/translations/en.json | 3 +- assets/jsons/translations/es.json | 3 +- assets/jsons/translations/fr.json | 3 +- assets/jsons/translations/ja.json | 3 +- assets/jsons/translations/ru.json | 3 +- assets/jsons/translations/zh-tw.json | 3 +- assets/jsons/translations/zh.json | 3 +- src/main/ipcs/bs-maps-ipcs.ts | 4 +- .../maps/local-maps-manager.service.ts | 34 ++++++++++- .../maps-playlists-panel.component.tsx | 32 +++++++--- .../maps/local-maps-list-panel.component.tsx | 18 ++++++ src/renderer/services/maps-manager.service.ts | 61 ++++++++++++++++--- src/shared/models/ipc/ipc-routes.ts | 2 +- 14 files changed, 144 insertions(+), 31 deletions(-) diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index 2e5afa25..34abf2ed 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -512,7 +512,8 @@ "error": "An error occurred while importing the map" }, "msgs": { - "success": "Imported \"{mapName}\" map successfully", + "success": "Imported all maps successfully", + "some-success": "Imported some maps successfully", "only-accept-zip": "Only zip files are supported", "not-found-zip": "Zip file does not exists", "invalid-zip": "Invalid zip file contents" diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 62cf79c3..4fce9120 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -519,7 +519,8 @@ "error": "An error occurred while importing the map" }, "msgs": { - "success": "Imported \"{mapName}\" map successfully", + "success": "Imported all maps successfully", + "some-success": "Imported some maps successfully", "only-accept-zip": "Only zip files are supported", "not-found-zip": "Zip file does not exists", "invalid-zip": "Invalid zip file contents" diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index 81aadd83..d30384a6 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -512,7 +512,8 @@ "error": "An error occurred while importing the map" }, "msgs": { - "success": "Imported \"{mapName}\" map successfully", + "success": "Imported all maps successfully", + "some-success": "Imported some maps successfully", "only-accept-zip": "Only zip files are supported", "not-found-zip": "Zip file does not exists", "invalid-zip": "Invalid zip file contents" diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index a69d8945..54cb5906 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -512,7 +512,8 @@ "error": "An error occurred while importing the map" }, "msgs": { - "success": "Imported \"{mapName}\" map successfully", + "success": "Imported all maps successfully", + "some-success": "Imported some maps successfully", "only-accept-zip": "Only zip files are supported", "not-found-zip": "Zip file does not exists", "invalid-zip": "Invalid zip file contents" diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index af0cacad..6de0014d 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -512,7 +512,8 @@ "error": "An error occurred while importing the map" }, "msgs": { - "success": "Imported \"{mapName}\" map successfully", + "success": "Imported all maps successfully", + "some-success": "Imported some maps successfully", "only-accept-zip": "Only zip files are supported", "not-found-zip": "Zip file does not exists", "invalid-zip": "Invalid zip file contents" diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index 5e29a651..9958ade9 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -512,7 +512,8 @@ "error": "An error occurred while importing the map" }, "msgs": { - "success": "Imported \"{mapName}\"map successfully", + "success": "Imported all maps successfully", + "some-success": "Imported some maps successfully", "only-accept-zip": "Only zip files are supported", "not-found-zip": "Zip file does not exists", "invalid-zip": "Invalid zip file contents" diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index 741a5d73..8dfa7393 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -512,7 +512,8 @@ "error": "An error occurred while importing the map" }, "msgs": { - "success": "Imported \"{mapName}\" map successfully", + "success": "Imported all maps successfully", + "some-success": "Imported some maps successfully", "only-accept-zip": "Only zip files are supported", "not-found-zip": "Zip file does not exists", "invalid-zip": "Invalid zip file contents" diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index 52a36321..1ee16b3c 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -512,7 +512,8 @@ "error": "An error occurred while importing the map" }, "msgs": { - "success": "Imported \"{mapName}\" map successfully", + "success": "Imported all maps successfully", + "some-success": "Imported some maps successfully", "only-accept-zip": "Only zip files are supported", "not-found-zip": "Zip file does not exists", "invalid-zip": "Invalid zip file contents" diff --git a/src/main/ipcs/bs-maps-ipcs.ts b/src/main/ipcs/bs-maps-ipcs.ts index 46248b2b..2ab16487 100644 --- a/src/main/ipcs/bs-maps-ipcs.ts +++ b/src/main/ipcs/bs-maps-ipcs.ts @@ -22,9 +22,9 @@ ipc.on("export-maps", async (args, reply) => { reply(await maps.exportMaps(args.version, args.maps, args.outPath)); }); -ipc.on("bs-maps.import-map", async (args, reply) => { +ipc.on("bs-maps.import-maps", async (args, reply) => { const maps = LocalMapsManagerService.getInstance(); - reply(from(maps.importMap(args.path, args.version))); + reply(maps.importMaps(args.paths, args.version)); }) ipc.on("bs-maps.download-map", async (args, reply) => { diff --git a/src/main/services/additional-content/maps/local-maps-manager.service.ts b/src/main/services/additional-content/maps/local-maps-manager.service.ts index 2e5595e5..8d81d5ef 100644 --- a/src/main/services/additional-content/maps/local-maps-manager.service.ts +++ b/src/main/services/additional-content/maps/local-maps-manager.service.ts @@ -328,7 +328,37 @@ export class LocalMapsManagerService { return null; } - public async importMap(zipPath: string, version?: BSVersion): Promise { + public importMaps(zipPaths: string[], version?: BSVersion): Observable { + const progress: Progression = { + total: zipPaths.length, + current: 0, + data: "", + }; + + return new Observable>(observer => { + (async () => { + for (const zipPath of zipPaths) { + ++progress.current; + progress.data = zipPath; + observer.next(progress); + + try { + await this.importMap(zipPath, version); + } catch (error: any) { + log.error(`Could not import "${zipPath}"`, error); + this.ipc.send<{map: string, error: string}>( + "map-not-imported", + this.windows.getWindows("index.html").at(0), + { map: zipPath, error: error.code } + ); + } + } + observer.complete(); + })(); + }); + } + + private async importMap(zipPath: string, version?: BSVersion): Promise { try { if (!pathExistsSync(zipPath)) { throw new CustomError(`Zip file "${zipPath}" does not exist`, "not-found-zip"); @@ -365,7 +395,7 @@ export class LocalMapsManagerService { localMap.songDetails = this.songDetailsCache.getSongDetails(localMap.hash); this.ipc.send<{map: BsmLocalMap, version?: BSVersion}>( - "map-downloaded", + "map-imported", this.windows.getWindows("index.html").at(0), { map: localMap, version } ); diff --git a/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx index a9cb1d8a..95eceeaf 100644 --- a/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps-playlists-panel.component.tsx @@ -104,8 +104,16 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { return playlistsManager.unlinkVersion(version); } - const handleFileDrop = async (file: File) => { - if (file.type !== "application/zip") { + const handleFileDrop = async (files: FileList) => { + const paths: string[] = []; + for (let i = 0; i < files.length; ++i) { + const file = files[i]; + if (file.type === "application/zip") { + paths.push(file.path); + } + } + + if (paths.length === 0) { notifications.notifyError({ title: "notifications.maps.import-map.titles.error", desc: "notifications.maps.import-map.msgs.zip-accept-only" @@ -113,15 +121,23 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { return; } - const localMap = await mapsManager.importMap(file.path, version); - if (localMap) { + const importCount = await mapsManager.importMaps(paths, version); + if (importCount === 0) { + return; + } + + if (importCount < paths.length) { notifications.notifySuccess({ title: "notifications.maps.import-map.titles.success", - desc: t("notifications.maps.import-map.msgs.success", { - mapName: localMap.rawInfo._songName - }) + desc: "notifications.maps.import-map.msgs.some-success", }); + return; } + + notifications.notifySuccess({ + title: "notifications.maps.import-map.titles.success", + desc: "notifications.maps.import-map.msgs.success", + }); } const dropDownItems = ((): DropDownItem[] => { @@ -201,7 +217,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { { - handleFileDrop(event.dataTransfer.files[0]); + handleFileDrop(event.dataTransfer.files); }} overlay={
diff --git a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx index 75248ea6..a43835ca 100644 --- a/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx +++ b/src/renderer/components/maps-playlists-panel/maps/local-maps-list-panel.component.tsx @@ -113,14 +113,32 @@ export const LocalMapsListPanel = forwardRef(({ ve } setMaps((maps$.value ? [map, ...maps$.value] : [map])); }}); + mapsManager.addImportListener(importListener); } return () => { sub?.unsubscribe(); + mapsManager.removeImportListener(importListener); } }, [isActiveOnce, version]) + const importListener = (importMap: BsmLocalMap, targetVersion?: BSVersion) => { + if (!equal(targetVersion, version)) { + return; + } + + const mapsCopy = maps ? [ ...maps ] : []; + + // importMap can collide with existing map + const index = maps.findIndex(map => map.songDetails.name === importMap.songDetails.name); + if (index > -1) { + mapsCopy.splice(index, 1); + } + + setMaps([importMap, ...mapsCopy]); + }; + const loadMaps = () => { setMaps(null); loadPercent$.next(0); diff --git a/src/renderer/services/maps-manager.service.ts b/src/renderer/services/maps-manager.service.ts index 55788bb9..6c321cf5 100644 --- a/src/renderer/services/maps-manager.service.ts +++ b/src/renderer/services/maps-manager.service.ts @@ -39,6 +39,9 @@ export class MapsManagerService { private readonly lastLinkedVersion$: Subject = new Subject(); private readonly lastUnlinkedVersion$: Subject = new Subject(); + private importListeners: ((map: BsmLocalMap, version?: BSVersion) => void)[] = []; + private importCount = 0; + private constructor() { this.ipcService = IpcService.getInstance(); this.modal = ModalService.getInstance(); @@ -46,6 +49,23 @@ export class MapsManagerService { this.notifications = NotificationService.getInstance(); this.config = ConfigurationService.getInstance(); this.linker = VersionFolderLinkerService.getInstance(); + + this.ipcService.watch<{map: BsmLocalMap, version?: BSVersion}>("map-imported") + .subscribe(data => { + ++this.importCount; + this.importListeners.forEach(listener => listener(data.map, data.version)); + }); + + // NOTE: Generic notify call from server side + this.ipcService.watch<{map: string, error: string}>("map-not-imported") + .subscribe(data => { + this.notifications.notifyError({ + title: "notifications.maps.import-map.titles.error", + desc: ["not-found-zip", "invalid-zip"].includes(data.error) + ? `notifications.maps.import-map.msgs.${data.error}` + : data.error + }); + }); } public getMaps(version?: BSVersion): Observable { @@ -196,24 +216,34 @@ export class MapsManagerService { }) } - public async importMap(path: string, version?: BSVersion): Promise { + public async importMaps(paths: string[], version?: BSVersion): Promise { try { if (!this.progressBar.require()) { - return null; + return 0; } - return await lastValueFrom(this.ipcService.sendV2( - "bs-maps.import-map", - { path, version } - )); + // Incremented by receiving an ipc call on "map-imported" + this.importCount = 0; + + const importProgress$: Observable = this.ipcService.sendV2( + "bs-maps.import-maps", + { paths, version } + ) + .pipe(map(progress => ({ + progression: (progress.current / progress.total) * 100, + label: progress.data + } as ProgressionInterface))); + + this.progressBar.show(importProgress$); + await lastValueFrom(importProgress$); + return this.importCount; } catch (error: any) { this.notifications.notifyError({ title: "notifications.maps.import-map.titles.error", - desc: ["not-found-zip", "invalid-zip"].includes(error?.code) - ? `notifications.maps.import-map.msgs.${error.code}` - : "Unknown" }); - return null; + return 0; + } finally { + this.progressBar.hide(); } } @@ -233,6 +263,17 @@ export class MapsManagerService { return lastValueFrom(this.ipcService.sendV2("unregister-maps-deep-link")); } + public addImportListener(listener: (map: BsmLocalMap, version?: BSVersion) => void): void { + this.importListeners.push(listener); + } + + public removeImportListener(listener: (map: BsmLocalMap, version?: BSVersion) => void): void { + const index = this.importListeners.indexOf(listener); + if (index > -1) { + this.importListeners.splice(index, 1); + } + } + public get versionLinked$(): Observable { return this.lastLinkedVersion$.asObservable(); } diff --git a/src/shared/models/ipc/ipc-routes.ts b/src/shared/models/ipc/ipc-routes.ts index 412f6a22..bcdefe20 100644 --- a/src/shared/models/ipc/ipc-routes.ts +++ b/src/shared/models/ipc/ipc-routes.ts @@ -58,7 +58,7 @@ export interface IpcChannelMapping { "load-version-maps": { request: BSVersion, response: BsmLocalMapsProgress}; "delete-maps": { request: BsmLocalMap[], response: DeleteMapsProgress }; "export-maps": { request: { version: BSVersion; maps: BsmLocalMap[]; outPath: string }, response: Progression }; - "bs-maps.import-map": { request: { path: string; version: BSVersion | undefined }, response: BsmLocalMap }; + "bs-maps.import-maps": { request: { paths: string[]; version: BSVersion | undefined }, response: Progression }; "bs-maps.download-map": { request: { map: BsvMapDetail; version: BSVersion | undefined }, response: BsmLocalMap }; "last-downloaded-map": { request: void, response: { version?: BSVersion, map: BsmLocalMap } }; "one-click-install-map": { request: BsvMapDetail, response: void };