[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
This commit is contained in:
silentrald
2024-09-10 00:03:02 +08:00
parent 0f3de55e9e
commit 7c88ea46e0
14 changed files with 144 additions and 31 deletions
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -1
View File
@@ -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"
+2 -2
View File
@@ -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) => {
@@ -328,7 +328,37 @@ export class LocalMapsManagerService {
return null;
}
public async importMap(zipPath: string, version?: BSVersion): Promise<BsmLocalMap> {
public importMaps(zipPaths: string[], version?: BSVersion): Observable<Progression> {
const progress: Progression<string> = {
total: zipPaths.length,
current: 0,
data: "",
};
return new Observable<Progression<string>>(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<BsmLocalMap> {
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 }
);
@@ -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) {
<Dropzone
className="w-full h-full shrink-0"
onDrop={event => {
handleFileDrop(event.dataTransfer.files[0]);
handleFileDrop(event.dataTransfer.files);
}}
overlay={
<div className="text-3xl pointer-events-none">
@@ -113,14 +113,32 @@ export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ 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);
+51 -10
View File
@@ -39,6 +39,9 @@ export class MapsManagerService {
private readonly lastLinkedVersion$: Subject<BSVersion> = new Subject();
private readonly lastUnlinkedVersion$: Subject<BSVersion> = 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<BsmLocalMapsProgress> {
@@ -196,24 +216,34 @@ export class MapsManagerService {
})
}
public async importMap(path: string, version?: BSVersion): Promise<BsmLocalMap | null> {
public async importMaps(paths: string[], version?: BSVersion): Promise<number> {
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<ProgressionInterface> = 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<BSVersion> {
return this.lastLinkedVersion$.asObservable();
}
+1 -1
View File
@@ -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 };