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 8749074f..0ceb5baf 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 @@ -14,7 +14,7 @@ import { BsmButton } from "../shared/bsm-button.component"; import { MapIcon } from "../svgs/icons/map-icon.component"; import { PlaylistIcon } from "../svgs/icons/playlist-icon.component"; import { useObservable } from "renderer/hooks/use-observable.hook"; -import { BehaviorSubject, of } from "rxjs"; +import { BehaviorSubject, lastValueFrom, of } from "rxjs"; import { LocalPlaylistsListPanel, LocalPlaylistsListRef } from "./playlists/local-playlists-list-panel.component"; import { PlaylistsManagerService } from "renderer/services/playlists-manager.service"; import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; @@ -25,6 +25,7 @@ import { LocalPlaylistFilter, LocalPlaylistFilterPanel } from "./playlists/local import { noop } from "shared/helpers/function.helpers"; import { Dropzone } from "../shared/dropzone.component"; import { NotificationService } from "renderer/services/notification.service"; +import { logRenderError } from "renderer"; type Props = { readonly version?: BSVersion; @@ -122,7 +123,8 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) { return; } - await mapsManager.importMaps(paths, version); + const import$ = mapsManager.importMaps(paths, version); + return lastValueFrom(import$).catch(logRenderError); } const dropDownItems = ((): DropDownItem[] => { 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 4b2b3288..f4a2a642 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 @@ -5,7 +5,7 @@ import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"; import { Subscription, BehaviorSubject, lastValueFrom } from "rxjs"; import { MapFilter } from "shared/models/maps/beat-saver.model"; import { MapsDownloaderService } from "renderer/services/maps-downloader.service"; -import { last, tap } from "rxjs/operators"; +import { bufferTime, last, tap } from "rxjs/operators"; import { useTranslation } from "renderer/hooks/use-translation.hook"; import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"; import { BsmImage } from "../../shared/bsm-image.component"; @@ -104,46 +104,28 @@ export const LocalMapsListPanel = forwardRef(({ ve useEffect(() => { - let sub: Subscription; + const subs: Subscription[] = []; if(isActiveOnce){ - sub = mapsDownloader.lastDownloadedMap$.subscribe({ next: ({map, version: targetVersion}) => { + subs.push(mapsDownloader.lastDownloadedMap$.subscribe({ next: ({map, version: targetVersion}) => { if (!equal(targetVersion, version)) { return; } setMaps((maps$.value ? [map, ...maps$.value] : [map])); - }}); - mapsManager.addImportListener(importListener); + }})); + + subs.push(mapsManager.$onMapImported(version).pipe(bufferTime(500)).subscribe({ next: newMaps => { + const maps = (maps$.value ?? []).filter(map => !newMaps.some(newMap => newMap.path === map.path)); + setMaps([...newMaps, ...maps]); + }})); } return () => { - sub?.unsubscribe(); - mapsManager.removeImportListener(importListener); + subs.forEach(s => s.unsubscribe()); } }, [isActiveOnce, version]) - const importListener = (importMaps: BsmLocalMap[], targetVersion?: BSVersion) => { - if (!equal(targetVersion, version)) { - return; - } - - if (!maps$.value) { - setMaps(importMaps); - return; - } - - const mapsCopy = [ ...maps$.value ]; - for (const importMap of importMaps) { - const index = mapsCopy.findIndex(map => map.hash === importMap.hash); - if (index > -1) { - mapsCopy.splice(index, 1); - } - } - - setMaps([...importMaps, ...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 f73c4529..5494e785 100644 --- a/src/renderer/services/maps-manager.service.ts +++ b/src/renderer/services/maps-manager.service.ts @@ -9,12 +9,13 @@ import { DeleteMapsModal } from "renderer/components/modal/modal-types/delete-ma import { ProgressBarService } from "./progress-bar.service"; import { NotificationService } from "./notification.service"; import { ConfigurationService } from "./configuration.service"; -import { map, last, catchError, tap } from "rxjs/operators"; +import { map, last, catchError, filter, distinctUntilChanged } from "rxjs/operators"; import { ProgressionInterface } from "shared/models/progress-bar"; import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service"; import { SongDetails } from "shared/models/maps"; import { Progression } from "main/helpers/fs.helpers"; import { DeleteDuplicateMapsModal } from "renderer/components/modal/modal-types/delete-duplicate-maps-modal.component"; +import equal from "fast-deep-equal"; export class MapsManagerService { private static instance: MapsManagerService; @@ -29,8 +30,6 @@ export class MapsManagerService { public static readonly REMEMBER_CHOICE_DELETE_MAP_KEY = "not-confirm-delete-map"; public static readonly RELATIVE_MAPS_FOLDER = window.electron.path.join("Beat Saber_Data", "CustomLevels"); - private readonly IMPORT_BATCH_SIZE = 8; - private readonly ipcService: IpcService; private readonly modal: ModalService; private readonly progressBar: ProgressBarService; @@ -41,7 +40,7 @@ export class MapsManagerService { private readonly lastLinkedVersion$: Subject = new Subject(); private readonly lastUnlinkedVersion$: Subject = new Subject(); - private importListeners = new Set<((maps: BsmLocalMap[], version?: BSVersion) => void)>(); + private readonly lastImportedMap$: Subject<{ version: BSVersion, map: BsmLocalMap }> = new Subject(); private constructor() { this.ipcService = IpcService.getInstance(); @@ -200,73 +199,62 @@ export class MapsManagerService { }) } - public async importMaps(paths: string[], version?: BSVersion): Promise { - try { - if (!this.progressBar.require()) { - return; - } - - const importObserver$ = this.ipcService.sendV2( - "bs-maps.import-maps", - { paths, version } - ); - - this.progressBar.show(importObserver$.pipe( - catchError(() => of()), - map(progress => ({ - progression: (progress.current / progress.total) * 100, - label: progress.data?.songDetails?.name - } as ProgressionInterface)) - )); - - // Processing - let importCount = 0; - let importTotal = 0; - const mapBatch: BsmLocalMap[] = []; // For batching - - await lastValueFrom(importObserver$.pipe( - tap(progress => { - if (!progress.data) { - importTotal = progress.total; - return; - } - - ++importCount; - mapBatch.push(progress.data); - if (mapBatch.length >= this.IMPORT_BATCH_SIZE) { - const currentBatch = mapBatch.splice(0, this.IMPORT_BATCH_SIZE); - this.importListeners.forEach( - listeners => listeners(currentBatch, version) - ); - } - }), - )); - if (mapBatch.length > 0) { - this.importListeners.forEach(listeners => listeners(mapBatch, version)); - } - - // Done processing - if (importCount === importTotal) { - this.notifications.notifySuccess({ - title: "notifications.maps.import-map.titles.success", - desc: "notifications.maps.import-map.msgs.success", - }); - } else if (importCount > 0) { - this.notifications.notifySuccess({ - title: "notifications.maps.import-map.titles.success", - desc: "notifications.maps.import-map.msgs.some-success", - }); - } - } catch (error: any) { - this.notifications.notifyError({ - title: "notifications.maps.import-map.titles.error", - desc: ["invalid-zip"].includes(error?.code) - ? `notifications.maps.import-map.msgs.${error.code}` - : "misc.unknown" - }); - } finally { - this.progressBar.hide(); + public importMaps(paths: string[], version?: BSVersion): Observable> { + if(!this.progressBar.require()){ + return throwError(() => new Error("Another operation is already in progress (importMaps)")); } + + return new Observable>(obs => { + const subs: Subscription[] = []; + (async () => { + const import$ = this.ipcService.sendV2("bs-maps.import-maps", { paths, version }); + + this.progressBar.show(import$.pipe( + catchError(() => of()), + map(progress => ({ + progression: (progress.current / progress.total) * 100, + label: progress.data?.songDetails?.name + } as ProgressionInterface)) + )); + + subs.push(import$.subscribe(obs)); + + subs.push(import$.pipe(catchError(() => of()), map(progress => progress?.data), filter(Boolean), distinctUntilChanged((a, b) => equal(a, b))).subscribe({ next: map => { + this.lastImportedMap$.next({ version, map }); + }})); + + return lastValueFrom(import$); + })() + .then(progress => { + if (progress.current === progress.total) { + this.notifications.notifySuccess({ + title: "notifications.maps.import-map.titles.success", + desc: "notifications.maps.import-map.msgs.success", + }); + } else if (progress.current > 0) { + this.notifications.notifySuccess({ + title: "notifications.maps.import-map.titles.success", + desc: "notifications.maps.import-map.msgs.some-success", + }); + } + }).catch(err => { + this.notifications.notifyError({ + title: "notifications.maps.import-map.titles.error", + desc: ["invalid-zip"].includes(err?.code) + ? `notifications.maps.import-map.msgs.${err.code}` + : "misc.unknown" + }); + obs.error(err); + }) + .finally(() => obs.complete()); + + return () => { + if(subs.length){ + this.progressBar.hide(); + subs.forEach(s => s.unsubscribe()); + } + } + }); } public getMapsInfoFromHashs(hashs: string[]): Observable { @@ -285,14 +273,6 @@ export class MapsManagerService { return lastValueFrom(this.ipcService.sendV2("unregister-maps-deep-link")); } - public addImportListener(listener: (maps: BsmLocalMap[], version?: BSVersion) => void): void { - this.importListeners.add(listener); - } - - public removeImportListener(listener: (maps: BsmLocalMap[], version?: BSVersion) => void): void { - this.importListeners.delete(listener); - } - public get versionLinked$(): Observable { return this.lastLinkedVersion$.asObservable(); } @@ -301,6 +281,10 @@ export class MapsManagerService { return this.lastUnlinkedVersion$.asObservable(); } + public $onMapImported(version: BSVersion): Observable { + return this.lastImportedMap$.pipe(filter(newMap => equal(newMap.version, version)), map(m => m.map)); + } + public $mapsFolderLinkState(version: BSVersion): Observable { return this.linker.$folderLinkedState(version, MapsManagerService.RELATIVE_MAPS_FOLDER); }