mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[feat-76] support drag and drop for mod files
This commit is contained in:
@@ -116,6 +116,10 @@
|
||||
"title": "Mods already installed",
|
||||
"description": "All selected mods are already installed"
|
||||
}
|
||||
},
|
||||
"drop-zone": {
|
||||
"text": "Import your mods",
|
||||
"subtext": "Drop your \"zip\" or \"dll\" files here to import them"
|
||||
}
|
||||
},
|
||||
"dropdown": {
|
||||
@@ -526,6 +530,17 @@
|
||||
"no-mods": "No mods are installed in this version 😑"
|
||||
}
|
||||
}
|
||||
},
|
||||
"import-mod": {
|
||||
"titles": {
|
||||
"success": "Mods import completed",
|
||||
"error": "An error occurred during the mods import"
|
||||
},
|
||||
"msgs": {
|
||||
"success": "Mods successfully imported.",
|
||||
"some-success": "Some mods were successfully imported.",
|
||||
"no-dlls": "The file(s) do not contain any \"dll\" files."
|
||||
}
|
||||
}
|
||||
},
|
||||
"maps": {
|
||||
|
||||
@@ -4,27 +4,32 @@ import { from } from "rxjs";
|
||||
|
||||
const ipc = IpcService.getInstance();
|
||||
|
||||
ipc.on("get-available-mods", (args, reply) => {
|
||||
ipc.on("bs-mods.get-available-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(from(modsManager.getAvailableMods(args)));
|
||||
});
|
||||
|
||||
ipc.on("get-installed-mods", (args, reply) => {
|
||||
ipc.on("bs-mods.get-installed-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(from(modsManager.getInstalledMods(args)));
|
||||
});
|
||||
|
||||
ipc.on("install-mods", (args, reply) => {
|
||||
ipc.on("bs-mods.import-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(modsManager.importMods(args.paths, args.version));
|
||||
});
|
||||
|
||||
ipc.on("bs-mods.install-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(modsManager.installMods(args.mods, args.version));
|
||||
});
|
||||
|
||||
ipc.on("uninstall-mods", (args, reply) => {
|
||||
ipc.on("bs-mods.uninstall-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(modsManager.uninstallMods(args.mods, args.version));
|
||||
});
|
||||
|
||||
ipc.on("uninstall-all-mods", (args, reply) => {
|
||||
ipc.on("bs-mods.uninstall-all-mods", (args, reply) => {
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
reply(modsManager.uninstallAllMods(args));
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ import { deleteFolder, pathExist, Progression, unlinkPath } from "../../helpers/
|
||||
import { lastValueFrom, Observable } from "rxjs";
|
||||
import recursiveReadDir from "recursive-readdir";
|
||||
import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
import { pathExistsSync } from "fs-extra";
|
||||
import { copyFile, pathExistsSync } from "fs-extra";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { popElement } from "shared/helpers/array.helpers";
|
||||
import { LinuxService } from "../linux.service";
|
||||
@@ -19,6 +19,7 @@ import { tryit } from "shared/helpers/error.helpers";
|
||||
import crypto from "crypto";
|
||||
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
|
||||
import { bsmSpawn } from "main/helpers/os.helpers";
|
||||
import { ExternalMod } from "shared/models/mods/mod.interface";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
@@ -320,6 +321,108 @@ export class BsModsManagerService {
|
||||
return Array.from(modsDict.values());
|
||||
}
|
||||
|
||||
private async importMod(modPath: string, destination: string): Promise<string[]> {
|
||||
const extensionName = path.extname(modPath).toLowerCase();
|
||||
|
||||
if (extensionName === ".dll") {
|
||||
log.info("Copying", `${modPath}`, "to", `"${destination}"`);
|
||||
const filename = path.basename(modPath, ".dll");
|
||||
const copied = await copyFile(modPath, path.join(destination, filename))
|
||||
.then(() => true)
|
||||
.catch(error => {
|
||||
log.warn("Could not copy", `"${modPath}"`, error);
|
||||
return false;
|
||||
});
|
||||
return copied ? [ filename ] : [];
|
||||
}
|
||||
|
||||
if (extensionName !== ".zip") {
|
||||
log.warn("Mod file is not a dll or zip file");
|
||||
return [];
|
||||
}
|
||||
|
||||
log.info("Extracting", `"${modPath}"`, "to", `"${destination}"`);
|
||||
const zip = await BsmZipExtractor.fromPath(modPath);
|
||||
|
||||
try {
|
||||
let hasDll = false;
|
||||
for await (const entry of zip.entries()) {
|
||||
if (entry.fileName.endsWith(".dll")) {
|
||||
hasDll = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasDll) {
|
||||
log.warn("No \"dll\" found in zip", modPath);
|
||||
return [];
|
||||
}
|
||||
|
||||
return await zip.extract(destination);
|
||||
} catch (error) {
|
||||
log.warn("Could not extract", `"${modPath}"`, error);
|
||||
return [];
|
||||
} finally {
|
||||
zip.close();
|
||||
}
|
||||
}
|
||||
|
||||
public importMods(paths: string[], version: BSVersion): Observable<Progression<ExternalMod>> {
|
||||
return new Observable<Progression<ExternalMod>>(obs => {
|
||||
const progress: Progression<ExternalMod> = { total: 0, current: 0 };
|
||||
const externalMod: ExternalMod = {
|
||||
name: "",
|
||||
files: [],
|
||||
};
|
||||
let modsInstalledCount = 0;
|
||||
const abortController = new AbortController();
|
||||
|
||||
(async () => {
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const modsPendingFolder = path.join(versionPath, ModsInstallFolder.PENDING);
|
||||
|
||||
for (const modPath of paths) {
|
||||
if (abortController.signal?.aborted) {
|
||||
log.info("Mods import has been cancelled");
|
||||
return;
|
||||
}
|
||||
|
||||
progress.total = paths.length;
|
||||
obs.next(progress);
|
||||
|
||||
if (!pathExistsSync(modPath)) { continue; }
|
||||
|
||||
const modFiles = await this.importMod(modPath, modsPendingFolder);
|
||||
if (modFiles.length > 0) {
|
||||
++modsInstalledCount;
|
||||
|
||||
externalMod.name = path.basename(modPath, path.extname(modPath));
|
||||
externalMod.files = modFiles;
|
||||
progress.data = externalMod;
|
||||
} else {
|
||||
progress.data = undefined;
|
||||
}
|
||||
++progress.current;
|
||||
obs.next(progress);
|
||||
}
|
||||
})()
|
||||
.then(() => {
|
||||
if (modsInstalledCount === 0) {
|
||||
throw new CustomError("No \"dll\" found in any of the files dropped", "no-dlls");
|
||||
}
|
||||
log.info("Successfully imported", modsInstalledCount, "mods from", paths.length, "files");
|
||||
})
|
||||
.catch(error => {
|
||||
obs.error(error);
|
||||
})
|
||||
.finally(() => obs.complete());
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
|
||||
const progress = { current: 0, total: mods.length };
|
||||
|
||||
|
||||
@@ -21,10 +21,13 @@ import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { NotificationService } from "renderer/services/notification.service";
|
||||
import { noop } from "shared/helpers/function.helpers";
|
||||
import { UninstallAllModsModal } from "renderer/components/modal/modal-types/uninstall-all-mods-modal.component";
|
||||
import { Dropzone } from "renderer/components/shared/dropzone.component";
|
||||
|
||||
export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; onDisclamerDecline: () => void }) {
|
||||
const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer";
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
const modsManager = useService(BsModsManagerService);
|
||||
const configService = useService(ConfigurationService);
|
||||
const notification = useService(NotificationService);
|
||||
@@ -166,6 +169,10 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
}).catch(noop).finally(() => setInstalling(() => false));
|
||||
};
|
||||
|
||||
const importMods = (files: string[]): void => {
|
||||
modsManager.importMods(files, version);
|
||||
};
|
||||
|
||||
const uninstallMod = (mod: Mod): void => {
|
||||
setUninstalling(() => true);
|
||||
lastValueFrom(modsManager.uninstallMod(mod, version)).catch(noop).finally(() => {
|
||||
@@ -192,7 +199,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
setModsSelected(() => []);
|
||||
}
|
||||
|
||||
const loadMods = (): Promise<void> => {
|
||||
const loadMods = async (): Promise<void> => {
|
||||
if (os.isOffline) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
@@ -265,6 +272,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
</ModStatus>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grow overflow-y-scroll w-full min-h-0 scrollbar-default p-0 m-0">
|
||||
@@ -297,7 +305,18 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
|
||||
return (
|
||||
<div ref={ref} className="shrink-0 w-full h-full px-8 pb-7 flex justify-center">
|
||||
<div className="relative flex flex-col grow-0 bg-light-main-color-2 dark:bg-main-color-2 size-full rounded-md shadow-black shadow-center overflow-hidden">{renderContent()}</div>
|
||||
<Dropzone
|
||||
className="w-full h-full shrink-0"
|
||||
onFiles={importMods}
|
||||
text={t("pages.version-viewer.mods.drop-zone.text")}
|
||||
subtext={t("pages.version-viewer.mods.drop-zone.subtext")}
|
||||
filters={[
|
||||
{ name: ".zip", extensions: ["zip"] },
|
||||
{ name: ".dll", extensions: ["dll"] }
|
||||
]}
|
||||
>
|
||||
<div className="relative flex flex-col grow-0 bg-light-main-color-2 dark:bg-main-color-2 size-full rounded-md shadow-black shadow-center overflow-hidden">{renderContent()}</div>
|
||||
</Dropzone>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Observable, BehaviorSubject, throwError, of } from "rxjs";
|
||||
import { catchError, tap } from "rxjs/operators";
|
||||
import { Observable, BehaviorSubject, throwError, of, lastValueFrom } from "rxjs";
|
||||
import { catchError, map, tap } from "rxjs/operators";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { Mod } from "shared/models/mods";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
@@ -32,11 +33,11 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
public getAvailableMods(version: BSVersion): Observable<Mod[]> {
|
||||
return this.ipcService.sendV2("get-available-mods", version);
|
||||
return this.ipcService.sendV2("bs-mods.get-available-mods", version);
|
||||
}
|
||||
|
||||
public getInstalledMods(version: BSVersion): Observable<Mod[]> {
|
||||
return this.ipcService.sendV2("get-installed-mods", version);
|
||||
return this.ipcService.sendV2("bs-mods.get-installed-mods", version);
|
||||
}
|
||||
|
||||
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
|
||||
@@ -46,7 +47,7 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
return new Observable<Progression>(obs => {
|
||||
const install$ = this.ipcService.sendV2("install-mods", { mods, version });
|
||||
const install$ = this.ipcService.sendV2("bs-mods.install-mods", { mods, version });
|
||||
this.progressBar.show(install$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
|
||||
|
||||
const sub = install$.pipe(
|
||||
@@ -77,7 +78,7 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
return new Observable<Progression>(obs => {
|
||||
const uninstall$ = this.ipcService.sendV2("uninstall-mods", { mods: [mod], version });
|
||||
const uninstall$ = this.ipcService.sendV2("bs-mods.uninstall-mods", { mods: [mod], version });
|
||||
this.progressBar.show(uninstall$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
|
||||
|
||||
const sub = uninstall$.pipe(
|
||||
@@ -102,13 +103,57 @@ export class BsModsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
public async importMods(paths: string[], version: BSVersion): Promise<void> {
|
||||
if (!this.progressBar.require()) {
|
||||
throw new Error("Action already in progress");
|
||||
}
|
||||
|
||||
// TODO: Check for conflicting mod files, and ask the user if they want to overwrite the files
|
||||
|
||||
const import$ = this.ipcService.sendV2("bs-mods.import-mods", {
|
||||
paths, version
|
||||
});
|
||||
|
||||
this.progressBar.show(import$.pipe(
|
||||
catchError(() => of()),
|
||||
map(progress => ({
|
||||
progression: (progress.current / progress.total) * 100,
|
||||
label: progress.data?.name
|
||||
}) as ProgressionInterface)
|
||||
));
|
||||
|
||||
return lastValueFrom(import$)
|
||||
.then(progress => {
|
||||
if (progress.current === 0) {
|
||||
return;
|
||||
}
|
||||
this.notifications.notifySuccess({
|
||||
title: "notifications.mods.import-mod.titles.success",
|
||||
desc: progress.current === progress.total
|
||||
? "notifications.mods.import-mod.msgs.success"
|
||||
: "notifications.mods.import-mod.msgs.some-success",
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
this.notifications.notifyError({
|
||||
title: "notifications.mods.import-mod.titles.error",
|
||||
desc: ["no-dlls"].includes(error?.code)
|
||||
? `notifications.mods.import-mod.msgs.${error.code}`
|
||||
: "misc.unknown",
|
||||
});
|
||||
})
|
||||
.finally(() => {
|
||||
this.progressBar.hide();
|
||||
});
|
||||
}
|
||||
|
||||
public uninstallAllMods(version: BSVersion): Observable<Progression> {
|
||||
if (!this.progressBar.require()) {
|
||||
return throwError(() => new Error("Action already in progress"));
|
||||
}
|
||||
|
||||
return new Observable<Progression>(obs => {
|
||||
const uninstall$ = this.ipcService.sendV2("uninstall-all-mods", version);
|
||||
const uninstall$ = this.ipcService.sendV2("bs-mods.uninstall-all-mods", version);
|
||||
this.progressBar.show(uninstall$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
|
||||
|
||||
const sub = uninstall$.pipe(
|
||||
@@ -133,4 +178,5 @@ export class BsModsManagerService {
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { Supporter } from "../supporters";
|
||||
import { AppWindow } from "../window-manager/app-window.model";
|
||||
import { LocalBPList, LocalBPListsDetails } from "../playlists/local-playlist.models";
|
||||
import { StaticConfigGetIpcRequestResponse, StaticConfigKeys, StaticConfigSetIpcRequest } from "main/services/static-configuration.service";
|
||||
import { ExternalMod } from "../mods/mod.interface";
|
||||
|
||||
export type IpcReplier<T> = (data: Observable<T>) => void;
|
||||
|
||||
@@ -78,11 +79,12 @@ export interface IpcChannelMapping {
|
||||
"delete-models": { request: BsmLocalModel[], response: Progression<BsmLocalModel[]> };
|
||||
|
||||
/* ** bs-mods-ipcs ** */
|
||||
"get-available-mods": { request: BSVersion, response: Mod[] };
|
||||
"get-installed-mods": { request: BSVersion, response: Mod[] };
|
||||
"install-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
|
||||
"uninstall-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
|
||||
"uninstall-all-mods": { request: BSVersion, response: Progression };
|
||||
"bs-mods.get-available-mods": { request: BSVersion, response: Mod[] };
|
||||
"bs-mods.get-installed-mods": { request: BSVersion, response: Mod[] };
|
||||
"bs-mods.import-mods": { request: { paths: string[]; version: BSVersion; }, response: Progression<ExternalMod> };
|
||||
"bs-mods.install-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
|
||||
"bs-mods.uninstall-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
|
||||
"bs-mods.uninstall-all-mods": { request: BSVersion, response: Progression };
|
||||
|
||||
/* ** bs-playlist-ipcs ** */
|
||||
"one-click-install-playlist": { request: string, response: Progression<DownloadPlaylistProgressionData> };
|
||||
|
||||
@@ -34,3 +34,9 @@ export interface FileHashes {
|
||||
hash: string;
|
||||
file: string;
|
||||
}
|
||||
|
||||
// Any mods that are not supported in beatmods
|
||||
export interface ExternalMod {
|
||||
name: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user