Merge pull request #594 from Zagrios/chore/some-reworks-to-remove-watch-functions

[chore] Some reworks to remove watch functions
This commit is contained in:
MathieuG-P
2024-09-29 19:41:59 +02:00
committed by GitHub
16 changed files with 335 additions and 294 deletions
+5
View File
@@ -27,6 +27,11 @@ ipc.on("download-map", async (args, reply) => {
reply(from(maps.downloadMap(args.map, args.version)));
});
ipc.on("last-downloaded-map", (_, reply) => {
const maps = LocalMapsManagerService.getInstance();
reply(maps.lastDownloadedMap$);
})
ipc.on("one-click-install-map", (args, reply) => {
const maps = LocalMapsManagerService.getInstance();
reply(from(maps.oneClickDownloadMap(args)))
+3 -3
View File
@@ -16,15 +16,15 @@ ipc.on("get-installed-mods", (args, reply) => {
ipc.on("install-mods", (args, reply) => {
const modsManager = BsModsManagerService.getInstance();
reply(from(modsManager.installMods(args.mods, args.version)));
reply(modsManager.installMods(args.mods, args.version));
});
ipc.on("uninstall-mods", (args, reply) => {
const modsManager = BsModsManagerService.getInstance();
reply(from(modsManager.uninstallMods(args.mods, args.version)));
reply(modsManager.uninstallMods(args.mods, args.version));
});
ipc.on("uninstall-all-mods", (args, reply) => {
const modsManager = BsModsManagerService.getInstance();
reply(from(modsManager.uninstallAllMods(args)));
reply(modsManager.uninstallAllMods(args));
});
+1 -1
View File
@@ -6,7 +6,7 @@ const ipc = IpcService.getInstance();
ipc.on("download-update", (_, reply) => {
const updaterService = AutoUpdaterService.getInstance();
reply(from(updaterService.downloadUpdate()));
reply(updaterService.downloadUpdate());
});
ipc.on("check-update", (_, reply) => {
@@ -14,7 +14,7 @@ import sanitize from "sanitize-filename";
import { DeepLinkService } from "../../deep-link.service";
import log from "electron-log";
import { WindowManagerService } from "../../window-manager.service";
import { Observable, lastValueFrom } from "rxjs";
import { Observable, Subject, lastValueFrom } from "rxjs";
import { Archive } from "../../../models/archive.class";
import { Progression, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers";
import { readFile } from "fs/promises";
@@ -23,7 +23,6 @@ import { allSettled } from "../../../../shared/helpers/promise.helpers";
import { splitIntoChunk } from "../../../../shared/helpers/array.helpers";
import { SongDetailsCacheService } from "./song-details-cache.service";
import { SongCacheService } from "./song-cache.service";
import { IpcService } from "../../ipc.service";
import { pathToFileURL } from "url";
import { sToMs } from "../../../../shared/helpers/time.helpers";
import { FieldRequired } from "shared/helpers/type.helpers";
@@ -57,7 +56,8 @@ export class LocalMapsManagerService {
private readonly linker: FolderLinkerService;
private readonly songDetailsCache: SongDetailsCacheService;
private readonly songCache: SongCacheService;
private readonly ipc: IpcService;
private readonly _lastDownloadedMap = new Subject<{ map: BsmLocalMap, version?: BSVersion }>();
private constructor() {
this.localVersion = BSLocalVersionService.getInstance();
@@ -69,7 +69,6 @@ export class LocalMapsManagerService {
this.linker = FolderLinkerService.getInstance();
this.songDetailsCache = SongDetailsCacheService.getInstance();
this.songCache = SongCacheService.getInstance();
this.ipc = IpcService.getInstance();
const handleOneClick = (mapId: string, isHash = false) => {
this.windows.openWindow(`oneclick-download-map.html?mapId=${mapId}&isHash=${isHash}`);
@@ -344,7 +343,7 @@ export class LocalMapsManagerService {
const localMap = await this.loadMapInfoFromPath(mapPath);
localMap.songDetails = this.songDetailsCache.getSongDetails(localMap.hash);
this.ipc.send<{map: BsmLocalMap, version?: BSVersion}>("map-downloaded", this.windows.getWindows("index.html").at(0), { map: localMap, version });
this._lastDownloadedMap.next({ map: localMap, version });
return localMap;
}
@@ -392,4 +391,8 @@ export class LocalMapsManagerService {
public isDeepLinksEnabled(): boolean {
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistered(link));
}
public get lastDownloadedMap$(): Observable<{ map: BsmLocalMap, version?: BSVersion }> {
return this._lastDownloadedMap.asObservable();
}
}
+35 -27
View File
@@ -1,13 +1,12 @@
import { autoUpdater } from "electron-updater";
import { autoUpdater, CancellationToken, ProgressInfo } from "electron-updater";
import log from "electron-log";
import { UtilsService } from "./utils.service";
import { gt } from "semver";
import { Progression } from "main/helpers/fs.helpers";
import { Observable } from "rxjs";
export class AutoUpdaterService {
private static instance: AutoUpdaterService;
private readonly utilsService: UtilsService;
public static getInstance(): AutoUpdaterService {
if (!AutoUpdaterService.instance) {
AutoUpdaterService.instance = new AutoUpdaterService();
@@ -18,37 +17,46 @@ export class AutoUpdaterService {
constructor() {
autoUpdater.logger = log;
autoUpdater.autoDownload = false;
this.utilsService = UtilsService.getInstance();
}
public isUpdateAvailable(): Promise<boolean> {
return new Promise(resolve => {
autoUpdater
.checkForUpdates()
.then(info => {
const needUpdate = (() => {
if (!info?.updateInfo) {
return false;
}
return gt(info.updateInfo.version, autoUpdater.currentVersion.version);
})();
resolve(needUpdate);
})
.catch(() => resolve(false));
});
return autoUpdater.checkForUpdates().then(info => {
return !!info?.updateInfo && gt(info.updateInfo.version, autoUpdater.currentVersion.version);
}).catch(() => false);
}
public downloadUpdate(): Promise<boolean> {
autoUpdater.removeAllListeners("download-progress");
autoUpdater.addListener("download-progress", info => {
this.utilsService.ipcSend("update-download-progress", { success: true, data: info.percent });
});
public downloadUpdate(): Observable<Progression> {
return new Observable<Progression>(observer => {
return autoUpdater.downloadUpdate().then(res => !!res && !!res.length);
observer.next({ current: 0, total: 0 });
const progressListener = (progress: ProgressInfo) => {
observer.next({ current: progress.transferred, total: progress.total });
};
const downloadedListener = () => {
observer.next({ current: 100, total: 100 });
};
autoUpdater.addListener("download-progress", progressListener);
autoUpdater.addListener("update-downloaded", downloadedListener);
const cancelToken = new CancellationToken();
autoUpdater.downloadUpdate(cancelToken)
.catch(err => observer.error(err))
.finally(() => observer.complete());
return () => {
cancelToken.cancel();
autoUpdater.removeListener("download-progress", progressListener);
autoUpdater.removeListener("update-downloaded", downloadedListener);
}
});
}
public quitAndInstall() {
autoUpdater.quitAndInstall();
log.info("Quit and install");
return autoUpdater.quitAndInstall();
}
}
@@ -1,39 +1,32 @@
import { BSVersion } from "shared/bs-version.interface";
import { DownloadLink, InstallModsResult, Mod, ModInstallProgression, UninstallModsResult } from "shared/models/mods";
import { DownloadLink, Mod } from "shared/models/mods";
import { BeatModsApiService } from "./beat-mods-api.service";
import { BSLocalVersionService } from "../bs-local-version.service";
import path from "path";
import { UtilsService } from "../utils.service";
import md5File from "md5-file";
import { RequestService } from "../request.service";
import { spawn } from "child_process";
import { BS_EXECUTABLE } from "../../constants";
import log from "electron-log";
import { deleteFolder, pathExist, unlinkPath } from "../../helpers/fs.helpers";
import { lastValueFrom } from "rxjs";
import { deleteFolder, pathExist, Progression, unlinkPath } from "../../helpers/fs.helpers";
import { lastValueFrom, Observable } from "rxjs";
import JSZip from "jszip";
import { extractZip } from "../../helpers/zip.helpers";
import recursiveReadDir from "recursive-readdir";
import { sToMs } from "../../../shared/helpers/time.helpers";
import { ensureDir, pathExistsSync } from "fs-extra";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { popElement } from "shared/helpers/array.helpers";
export class BsModsManagerService {
private static instance: BsModsManagerService;
private readonly beatModsApi: BeatModsApiService;
private readonly bsLocalService: BSLocalVersionService;
private readonly utilsService: UtilsService;
private readonly requestService: RequestService;
private manifestMatches: Mod[];
private nbModsToInstall = 0;
private nbInstalledMods = 0;
private nbModsToUninstall = 0;
private nbUninstalledMods = 0;
public static getInstance(): BsModsManagerService {
if (!BsModsManagerService.instance) {
BsModsManagerService.instance = new BsModsManagerService();
@@ -44,7 +37,6 @@ export class BsModsManagerService {
private constructor() {
this.beatModsApi = BeatModsApiService.getInstance();
this.bsLocalService = BSLocalVersionService.getInstance();
this.utilsService = UtilsService.getInstance();
this.requestService = RequestService.getInstance();
}
@@ -187,7 +179,6 @@ export class BsModsManagerService {
private async installMod(mod: Mod, version: BSVersion): Promise<boolean> {
log.info("INSTALL MOD", mod.name, "for version", `${version.BSVersion} - ${version.name}`);
this.utilsService.ipcSend<ModInstallProgression>("mod-installed", { success: true, data: { name: mod.name, progression: ((this.nbInstalledMods + 1) / this.nbModsToInstall) * 100 } });
const download = this.getModDownload(mod, version);
@@ -246,10 +237,6 @@ export class BsModsManagerService {
}))
: extracted;
if(res){
this.nbInstalledMods++;
}
return res;
}
@@ -275,9 +262,6 @@ export class BsModsManagerService {
}
private async uninstallMod(mod: Mod, version: BSVersion): Promise<void> {
this.nbUninstalledMods++;
this.utilsService.ipcSend<ModInstallProgression>("mod-uninstalled", { success: true, data: { name: mod.name, progression: (this.nbUninstalledMods / this.nbModsToUninstall) * 100 } });
if (mod.name.toLowerCase() === "bsipa") {
return this.uninstallBSIPA(mod, version);
}
@@ -322,81 +306,92 @@ export class BsModsManagerService {
return Array.from(modsDict.values());
}
public async installMods(mods: Mod[], version: BSVersion): Promise<InstallModsResult> {
if (!mods?.length) {
throw CustomError.fromError(new Error("No mods to install"), "no-mods");
}
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
const progress = { current: 0, total: mods.length };
const bsipa = mods.find(mod => mod.name.toLowerCase() === "bsipa");
if (bsipa) {
mods = mods.filter(mod => mod.name.toLowerCase() !== "bsipa");
}
return new Observable<Progression>(obs => {
(async () => {
if (!mods?.length) {
throw CustomError.throw(new Error("No mods to install"), "no-mods", mods);
}
this.nbModsToInstall = mods.length + (bsipa ? 1 : 0);
this.nbInstalledMods = 0;
obs.next(progress);
if (bsipa) {
const installed = await this.installMod(bsipa, version).catch(err => {
log.error("INSTALL BSIPA", err);
return false;
});
if (!installed) {
throw CustomError.fromError(new Error("Unable to install BSIPA"), "cannot-install-bsipa");
}
}
const bsipa = popElement(mod => mod.name.toLowerCase() === "bsipa", mods);
for (const mod of mods) {
await this.installMod(mod, version);
}
if(bsipa){
const bsipaInstalled = await this.installMod(bsipa, version).catch(err => {
log.error("Error while installing BSIPA", err);
});
return {
nbModsToInstall: this.nbModsToInstall,
nbInstalledMods: this.nbInstalledMods,
};
if(!bsipaInstalled){
throw CustomError.throw(new Error("BSIPA failed to install"), "cannot-install-bsipa");
}
progress.current++;
obs.next(progress);
}
for (const mod of mods) {
await this.installMod(mod, version);
progress.current++;
obs.next(progress);
}
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
});
}
public async uninstallMods(mods: Mod[], version: BSVersion): Promise<UninstallModsResult> {
if (!mods?.length) {
throw CustomError.fromError(new Error("No mods to uninstall"), "no-mods");
}
public uninstallMods(mods: Mod[], version: BSVersion): Observable<Progression> {
const progress = { current: 0, total: mods.length };
this.nbModsToUninstall = mods.length;
this.nbUninstalledMods = 0;
return new Observable<Progression>(obs => {
(async () => {
if (!mods?.length) {
throw CustomError.throw(new Error("No mods to uninstall"), "no-mods", mods);
}
for (const mod of mods) {
await this.uninstallMod(mod, version);
}
obs.next(progress);
return {
nbModsToUninstall: this.nbModsToUninstall,
nbUninstalledMods: this.nbUninstalledMods,
};
for (const mod of mods) {
await this.uninstallMod(mod, version);
progress.current++;
obs.next(progress);
}
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
});
}
public async uninstallAllMods(version: BSVersion): Promise<UninstallModsResult> {
const mods = await this.getInstalledMods(version);
public uninstallAllMods(version: BSVersion): Observable<Progression> {
return new Observable<Progression>(obs => {
(async () => {
const mods = await this.getInstalledMods(version).catch(err => {
log.error(err);
return [];
});
if (!mods?.length) {
throw CustomError.fromError(new Error("This version has to mods to uninstall"), "no-mods");
}
const progress = { current: 0, total: mods.length };
this.nbModsToUninstall = mods.length;
this.nbUninstalledMods = 0;
obs.next(progress);
for (const mod of mods) {
await this.uninstallMod(mod, version);
}
for (const mod of mods) {
await this.uninstallMod(mod, version);
progress.current++;
obs.next(progress);
}
const versionPath = await this.bsLocalService.getVersionPath(version);
const versionPath = await this.bsLocalService.getVersionPath(version);
await deleteFolder(path.join(versionPath, ModsInstallFolder.PLUGINS));
await deleteFolder(path.join(versionPath, ModsInstallFolder.LIBS));
await deleteFolder(path.join(versionPath, ModsInstallFolder.IPA));
return {
nbModsToUninstall: this.nbModsToUninstall,
nbUninstalledMods: this.nbUninstalledMods,
};
await deleteFolder(path.join(versionPath, ModsInstallFolder.PLUGINS));
await deleteFolder(path.join(versionPath, ModsInstallFolder.LIBS));
await deleteFolder(path.join(versionPath, ModsInstallFolder.IPA));
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
});
}
}
@@ -87,25 +87,27 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
setMaps(null);
loadPercent$.next(0);
subs.forEach(s => s.unsubscribe());
mapsDownloader.removeOnMapDownloadedListener(loadMaps);
};
}, [isActiveOnce, version, linked]);
useEffect(() => {
let sub: Subscription;
if(isActiveOnce){
mapsDownloader.addOnMapDownloadedListener((map, targetVersion) => {
sub = mapsDownloader.lastDownloadedMap$.subscribe({ next: ({map, version: targetVersion}) => {
if (!equal(targetVersion, version)) {
return;
}
setMaps((maps ? [map, ...maps] : [map]));
});
setMaps((maps$.value ? [map, ...maps$.value] : [map]));
}});
}
return () => {
mapsDownloader.removeOnMapDownloadedListener(loadMaps);
sub?.unsubscribe();
}
}, [isActiveOnce, version, maps])
}, [isActiveOnce, version])
const loadMaps = () => {
setMaps(null);
@@ -67,22 +67,21 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
}, [searchParams]);
useEffect(() => {
const onMapDownloaded = (map: BsmLocalMap, targetVersion: BSVersion) => {
const sub = mapsDownloader.lastDownloadedMap$.subscribe({ next: ({ map, version: targetVersion }) => {
if (!equal(targetVersion, version)) {
return;
}
const downloadedHash = map.hash;
setOwnedMapHashs(prev => [...prev, downloadedHash]);
};
mapsDownloader.addOnMapDownloadedListener(onMapDownloaded);
}});
if (mapsDownloader.isDownloading) {
progressBar.setStyle(mapsDownloader.progressBarStyle);
}
return () => {
mapsDownloader.removeOnMapDownloadedListener(onMapDownloaded);
sub.unsubscribe();
progressBar.setStyle(null);
};
}, []);
@@ -3,22 +3,26 @@ import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { Mod } from "shared/models/mods/mod.interface";
import { CSSProperties, MouseEvent, useMemo, useRef } from "react";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { PageStateService } from "renderer/services/page-state.service";
import useDoubleClick from "use-double-click";
import { gt } from "semver";
import { useService } from "renderer/hooks/use-service.hook";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
type Props = { className?: string; mod: Mod; installedVersion: string; isDependency?: boolean; isSelected?: boolean; onChange?: (val: boolean) => void; wantInfo?: boolean; onWantInfo?: (mod: Mod) => void };
type Props = {
className?: string;
mod: Mod;
installedVersion: string;
isDependency?: boolean;
isSelected?: boolean;
onChange?: (val: boolean) => void;
wantInfo?: boolean;
onWantInfo?: (mod: Mod) => void;
disabled?: boolean;
onUninstall?: () => void;
};
export function ModItem({ className, mod, installedVersion, isDependency, isSelected, onChange, wantInfo, onWantInfo }: Props) {
const modsManager = useService(BsModsManagerService);
const pageState = useService(PageStateService);
export function ModItem({ className, mod, installedVersion, isDependency, isSelected, onChange, wantInfo, onWantInfo, disabled, onUninstall }: Props) {
const themeColor = useThemeColor("second-color");
const uninstalling = useObservable(() => modsManager.isUninstalling$);
const clickRef = useRef();
const isChecked = useMemo(() => isDependency || isSelected || mod.required, [isDependency, isSelected, mod.required]);
@@ -37,10 +41,6 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
const wantInfoStyle: CSSProperties = wantInfo ? { borderColor: themeColor } : { borderColor: "transparent" };
const isOutDated = installedVersion ? gt(mod.version, installedVersion) : false;
const uninstall = () => {
modsManager.uninstallMod(mod, pageState.getState());
};
const handleWantInfo = (e: MouseEvent) => {
e.preventDefault();
onWantInfo(mod);
@@ -53,7 +53,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
return (
<li ref={clickRef} className={`${className} group`}>
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-inherit ml-3 border-2 border-r-0 z-[1] group-hover:brightness-90" style={wantInfoStyle}>
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.required || isDependency} checked={isChecked} />
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.required || isDependency || disabled} checked={isChecked} />
</div>
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2 blur-none group-hover:brightness-90" style={wantInfoStyle}>
{mod.name}
@@ -72,11 +72,11 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
<BsmButton
className="z-[1] h-7 w-7 p-[5px] rounded-full group-hover:brightness-90"
icon="trash"
disabled={uninstalling}
disabled={disabled}
withBar={false}
onClick={e => {
e.stopPropagation();
uninstall();
onUninstall?.();
}}
/>
)}
@@ -3,18 +3,22 @@ import { useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { PageStateService } from "renderer/services/page-state.service";
import { Mod } from "shared/models/mods/mod.interface";
import { ModItem } from "./mod-item.component";
import { useService } from "renderer/hooks/use-service.hook";
type Props = { modsMap: Map<string, Mod[]>; installed: Map<string, Mod[]>; modsSelected: Mod[]; onModChange: (selected: boolean, mod: Mod) => void; moreInfoMod?: Mod; onWantInfos: (mod: Mod) => void };
type Props = {
modsMap: Map<string, Mod[]>;
installed: Map<string, Mod[]>;
modsSelected: Mod[];
onModChange: (selected: boolean, mod: Mod) => void;
moreInfoMod?: Mod;
onWantInfos: (mod: Mod) => void
disabled?: boolean;
uninstallMod?: (mods: Mod) => void;
uninstallAllMods?: () => void;
};
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos }: Props) {
const pageState = useService(PageStateService);
const modsManager = useService(BsModsManagerService);
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos, disabled, uninstallMod, uninstallAllMods }: Props) {
const [filter, setFilter] = useState("");
const [filterEnabled, setFilterEnabled] = useState(false);
@@ -50,10 +54,6 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
setFilterEnabled(b => !b);
};
const handleUninstallAll = () => {
modsManager.uninstallAllMods(pageState.getState());
};
return (
modsMap && (
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_1fr_min-content] bg-light-main-color-2 dark:bg-main-color-2 text-main-color-1 dark:text-light-main-color-1">
@@ -66,7 +66,7 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.latest")}</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.description")}</span>
<span className="z-10 sticky top-0 bg-inherit border-b-2 border-main-color-1 h-8 flex justify-start items-center py-1 pl-[3px] min-w-[50px]">
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: handleUninstallAll }]} />
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: () => uninstallAllMods?.() }]} />
</span>
{Array.from(modsMap.keys()).map(
@@ -74,7 +74,19 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && (
<ul key={key} className="contents">
<h2 className="col-span-full py-1 font-bold pl-3">{key}</h2>
{modsMap.get(key).map(mod => mod.name?.toLowerCase().includes(filter) && <ModItem key={mod.name} className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1 hover:cursor-pointer" mod={mod} installedVersion={installedModVersion(key, mod)} isDependency={isDependency(mod)} isSelected={isSelected(mod)} onChange={val => onModChange(val, mod)} onWantInfo={onWantInfos} wantInfo={mod.name === moreInfoMod?.name} />)}
{modsMap.get(key).map(mod => mod.name?.toLowerCase().includes(filter) && (
<ModItem
key={mod.name}
className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1 hover:cursor-pointer"
mod={mod} installedVersion={installedModVersion(key, mod)}
isDependency={isDependency(mod)}
isSelected={isSelected(mod)}
onChange={val => onModChange(val, mod)}
onWantInfo={onWantInfos}
wantInfo={mod.name === moreInfoMod?.name}
disabled={disabled}
onUninstall={() => uninstallMod?.(mod)} />
))}
</ul>
)
)}
@@ -9,8 +9,7 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component";
import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png";
import BeatConflictImg from "../../../../../../assets/images/apngs/beat-conflict.png";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { skip, filter } from "rxjs/operators";
import { Subscription, lastValueFrom, noop } from "rxjs";
import { lastValueFrom } from "rxjs";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { LinkOpenerService } from "renderer/services/link-opener.service";
import { useInView } from "framer-motion";
@@ -20,6 +19,8 @@ import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
import { lt } from "semver";
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";
export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; onDisclamerDecline: () => void }) {
const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer";
@@ -39,7 +40,8 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
const [moreInfoMod, setMoreInfoMod] = useState(null as Mod);
const [reinstallAllMods, setReinstallAllMods] = useState(false);
const isOnline = useObservable(() => os.isOnline$);
const installing = useObservable(() => modsManager.isInstalling$);
const [installing, setInstalling] = useState(false);
const [uninstalling, setUninstalling] = useState(false);
const downloadRef = useRef(null);
const [downloadWith, setDownloadWidth] = useState(0);
@@ -103,11 +105,34 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
return;
}
modsManager.installMods(modsToInstall, version).then(() => {
setInstalling(() => true)
lastValueFrom(modsManager.installMods(modsToInstall, version)).then(() => {
loadMods();
}).catch(noop).finally(() => setInstalling(() => false));
};
const uninstallMod = (mod: Mod): void => {
setUninstalling(() => true);
lastValueFrom(modsManager.uninstallMod(mod, version)).catch(noop).finally(() => {
loadMods();
setUninstalling(() => false);
});
};
const uninstallAllMods = async () => {
const res = await modals.openModal(UninstallAllModsModal, {data: version});
if (res.exitCode !== ModalExitCode.COMPLETED) {
return;
}
setUninstalling(() => true);
lastValueFrom(modsManager.uninstallAllMods(version)).catch(noop).finally(() => {
loadMods();
setUninstalling(() => false);
})
};
const loadMods = (): Promise<void> => {
if (os.isOffline) {
return Promise.resolve();
@@ -128,7 +153,6 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
};
useEffect(() => {
const subs: Subscription[] = [];
if(!isVisible || !isOnline){
return noop();
@@ -153,22 +177,12 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
}
loadMods();
subs.push(
modsManager.isUninstalling$.pipe(
skip(1),
filter(uninstalling => !uninstalling)
).subscribe(() => {
loadMods();
})
);
});
return () => {
setMoreInfoMod(null);
setModsAvailable(null);
setModsInstalled(null);
subs.forEach(s => s.unsubscribe());
};
}, [isVisible, isOnline, version]);
@@ -195,7 +209,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
return (
<>
<div className="grow overflow-y-scroll w-full min-h-0 scrollbar-default p-0 m-0">
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo} />
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo} disabled={uninstalling || installing} uninstallMod={uninstallMod} uninstallAllMods={uninstallAllMods}/>
</div>
<div className="shrink-0 flex items-center justify-between px-3 py-2">
<BsmButton className="flex items-center justify-center rounded-md px-1 h-8" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{ width: downloadWith }}/>
+15 -11
View File
@@ -1,10 +1,10 @@
import { map } from "rxjs/operators";
import { IpcService } from "./ipc.service";
import { ProgressBarService } from "./progress-bar.service";
import { ModalService } from "renderer/services/modale.service";
import { ChangelogModal } from "renderer/components/modal/modal-types/chabgelog-modal/changelog-modal.component";
import { ConfigurationService } from "./configuration.service";
import { Observable, lastValueFrom } from "rxjs";
import { Progression } from "main/helpers/fs.helpers";
export interface Changelog {
@@ -22,8 +22,6 @@ export class AutoUpdaterService {
private progressService: ProgressBarService;
private ipcService: IpcService;
private downloadProgress$: Observable<number>;
private modal: ModalService;
private configurationService: ConfigurationService;
@@ -42,22 +40,28 @@ export class AutoUpdaterService {
this.ipcService = IpcService.getInstance();
this.modal = ModalService.getInstance();
this.configurationService = ConfigurationService.getInstance();
this.downloadProgress$ = this.ipcService.watch<number>("update-download-progress").pipe(map(res => (res.success ? res.data : 0)));
}
public isUpdateAvailable(): Promise<boolean> {
return lastValueFrom(this.ipcService.sendV2("check-update")).catch(() => false);
}
public downloadUpdate(): Promise<boolean> {
const promise = lastValueFrom(this.ipcService.sendV2("download-update")).then(() => true).catch(() => false);
this.progressService.show(this.downloadProgress$, true);
return promise;
public downloadUpdate(): Observable<Progression> {
return new Observable<Progression>(obs => {
const download$ = this.ipcService.sendV2("download-update");
this.progressService.show(download$, true);
const sub = download$.subscribe(obs);
return () => {
sub.unsubscribe();
this.progressService.hide(true);
}
});
}
public quitAndInstall() {
lastValueFrom(this.ipcService.sendV2("install-update"));
public quitAndInstall(): Promise<void> {
return lastValueFrom(this.ipcService.sendV2("install-update"));
}
public getLastAppVersion(): string {
@@ -1,17 +1,11 @@
import { UninstallAllModsModal } from "renderer/components/modal/modal-types/uninstall-all-mods-modal.component";
import { UninstallModModal } from "renderer/components/modal/modal-types/uninstall-mod-modal.component";
import { Observable, BehaviorSubject, lastValueFrom } from "rxjs";
import { map } from "rxjs/operators";
import { Observable, BehaviorSubject, throwError, of } from "rxjs";
import { catchError, tap } from "rxjs/operators";
import { BSVersion } from "shared/bs-version.interface";
import { Mod, ModInstallProgression } from "shared/models/mods";
import { ProgressionInterface } from "shared/models/progress-bar";
import { Mod } from "shared/models/mods";
import { IpcService } from "./ipc.service";
import { ModalExitCode, ModalService } from "./modale.service";
import { NotificationType } from "../../shared/models/notification/notification.model";
import { OsDiagnosticService } from "./os-diagnostic.service";
import { ProgressBarService } from "./progress-bar.service";
import { NotificationService } from "./notification.service";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { Progression } from "main/helpers/fs.helpers";
export class BsModsManagerService {
private static instance: BsModsManagerService;
@@ -20,11 +14,8 @@ export class BsModsManagerService {
private readonly ipcService: IpcService;
private readonly progressBar: ProgressBarService;
private readonly modals: ModalService;
private readonly notifications: NotificationService;
private readonly os: OsDiagnosticService;
public readonly isInstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
public readonly isUninstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
public static getInstance(): BsModsManagerService {
@@ -37,9 +28,7 @@ export class BsModsManagerService {
private constructor() {
this.ipcService = IpcService.getInstance();
this.progressBar = ProgressBarService.getInstance();
this.modals = ModalService.getInstance();
this.notifications = NotificationService.getInstance();
this.os = OsDiagnosticService.getInstance();
}
public getAvailableMods(version: BSVersion): Observable<Mod[]> {
@@ -50,91 +39,98 @@ export class BsModsManagerService {
return this.ipcService.sendV2("get-installed-mods", version);
}
public installMods(mods: Mod[], version: BSVersion): Promise<void> {
if (this.os.isOffline) {
this.notifications.notifyError({
title: "notifications.shared.errors.titles.no-internet",
desc: "notifications.shared.errors.msg.no-internet",
});
return Promise.resolve();
}
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
if (!this.progressBar.require()) {
return Promise.resolve();
return throwError(() => new Error("Action already in progress"));
}
const progress$: Observable<ProgressionInterface> = this.ipcService.watch<ModInstallProgression>("mod-installed").pipe(
map(res => {
return { progression: res.data.progression, label: res.data.name } as ProgressionInterface;
})
);
return new Observable<Progression>(obs => {
const install$ = this.ipcService.sendV2("install-mods", { mods, version });
this.progressBar.show(install$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
this.progressBar.show(progress$, true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
const sub = install$.pipe(
tap({
error: err => {
if(err?.code){
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.install-mods.msg.errors.${err?.code}`, duration: this.NOTIFICATION_DURATION });
} else {
this.notifications.notifyError({ title: "notifications.types.error", desc: err?.message ?? err, duration: this.NOTIFICATION_DURATION });
}
},
complete: () => {
this.notifications.notifySuccess({ title: "notifications.mods.install-mods.titles.success", desc: "notifications.mods.install-mods.msg.success", duration: this.NOTIFICATION_DURATION });
},
})
).subscribe(obs);
this.isInstalling$.next(true);
return lastValueFrom(this.ipcService.sendV2("install-mods", { mods, version })).then(res => {
const isFullyInstalled = res.nbInstalledMods === res.nbModsToInstall;
const title = `notifications.mods.install-mods.titles.${isFullyInstalled ? "success" : "warning"}`;
const desc = `notifications.mods.install-mods.msg.${isFullyInstalled ? "success" : "warning"}`;
this.notifications.notify({ type: isFullyInstalled ? NotificationType.SUCCESS : NotificationType.WARNING, title, desc, duration: this.NOTIFICATION_DURATION });
}).catch((e: CustomError) => {
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.install-mods.msg.errors.${e?.code}`, duration: this.NOTIFICATION_DURATION });
}).finally(() => {
this.isInstalling$.next(false);
this.progressBar.hide();
})
}
public async uninstallMod(mod: Mod, version: BSVersion): Promise<void> {
if (!this.progressBar.require()) {
return;
}
const modalRes = await this.modals.openModal(UninstallModModal, {data: mod});
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
return;
}
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
this.progressBar.show(progress$, true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
this.isUninstalling$.next(true);
return lastValueFrom(this.ipcService.sendV2("uninstall-mods", { mods: [mod], version })).then(() => {
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-mod.titles.success", duration: this.NOTIFICATION_DURATION });
}).catch((e: CustomError) => {
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-mod.msg.errors.${e?.code}`, duration: this.NOTIFICATION_DURATION });
}).finally(() => {
this.isUninstalling$.next(false);
this.progressBar.hide();
})
return () => {
sub.unsubscribe();
this.progressBar.hide(true);
}
});
}
public async uninstallAllMods(version: BSVersion) {
public uninstallMod(mod: Mod, version: BSVersion): Observable<Progression> {
if (!this.progressBar.require()) {
return;
return throwError(() => new Error("Action already in progress"));
}
const modalRes = await this.modals.openModal(UninstallAllModsModal, {data: version});
return new Observable<Progression>(obs => {
const uninstall$ = this.ipcService.sendV2("uninstall-mods", { mods: [mod], version });
this.progressBar.show(uninstall$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
return;
const sub = uninstall$.pipe(
tap({
error: err => {
if(err?.code){
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-mod.msg.errors.${err?.code}`, duration: this.NOTIFICATION_DURATION });
} else {
this.notifications.notifyError({ title: "notifications.types.error", desc: err?.message ?? err, duration: this.NOTIFICATION_DURATION });
}
},
complete: () => {
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-mod.titles.success", duration: this.NOTIFICATION_DURATION });
},
})
).subscribe(obs);
return () => {
sub.unsubscribe();
this.progressBar.hide(true);
}
});
}
public uninstallAllMods(version: BSVersion): Observable<Progression> {
if (!this.progressBar.require()) {
return throwError(() => new Error("Action already in progress"));
}
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
this.progressBar.show(progress$, true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
return new Observable<Progression>(obs => {
const uninstall$ = this.ipcService.sendV2("uninstall-all-mods", version);
this.progressBar.show(uninstall$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
this.isUninstalling$.next(true);
return lastValueFrom(this.ipcService.sendV2("uninstall-all-mods", version)).then(() => {
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-all-mods.titles.success", desc: "notifications.mods.uninstall-all-mods.msg.success", duration: this.NOTIFICATION_DURATION });
}).catch((e: CustomError) => {
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-all-mods.msg.errors.${e?.code}`, duration: this.NOTIFICATION_DURATION });
}).finally(() => {
this.isUninstalling$.next(false);
this.progressBar.hide();
})
const sub = uninstall$.pipe(
tap({
error: err => {
if(err?.code){
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-all-mods.msg.errors.${err?.code}`, duration: this.NOTIFICATION_DURATION });
} else {
this.notifications.notifyError({ title: "notifications.types.error", desc: err?.message ?? err, duration: this.NOTIFICATION_DURATION });
}
},
complete: () => {
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-all-mods.titles.success", desc: "notifications.mods.uninstall-all-mods.msg.success", duration: this.NOTIFICATION_DURATION });
},
})
).subscribe(obs);
return () => {
sub.unsubscribe();
this.progressBar.hide(true);
}
});
}
}
@@ -1,5 +1,5 @@
import { DownloadMapsModal } from "renderer/components/modal/modal-types/download-maps-modal.component";
import { map, filter } from "rxjs/operators";
import { map, filter, share } from "rxjs/operators";
import { BehaviorSubject, timer, Observable, lastValueFrom } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
import { ModalResponse, ModalService } from "./modale.service";
@@ -30,9 +30,10 @@ export class MapsDownloaderService {
private readonly mapsQueue$: BehaviorSubject<MapDownload[]> = new BehaviorSubject([]);
private readonly currentDownload$: BehaviorSubject<MapDownload> = new BehaviorSubject(null);
private queueMaxLenght = 0;
private downloadedListerners: ((map: BsmLocalMap, version: BSVersion) => void)[] = [];
public readonly progressBarStyle: CSSProperties = { zIndex: 100000, position: "fixed", bottom: "10px", right: 0 };
private _lastDownloadedMap$: Observable<{map: BsmLocalMap, version?: BSVersion}>;
private constructor() {
this.modals = ModalService.getInstance();
this.progressBar = ProgressBarService.getInstance();
@@ -43,10 +44,6 @@ export class MapsDownloaderService {
this.mapsQueue$.pipe(filter(queue => queue.length === 0)).subscribe(() => {
this.queueMaxLenght = 0;
});
this.ipc.watch<{map: BsmLocalMap, version?: BSVersion}>("map-downloaded").subscribe((data) => {
this.downloadedListerners.forEach(func => func(data.map, data.version));
});
}
private async startDownloadMaps() {
@@ -119,16 +116,19 @@ export class MapsDownloaderService {
return this.mapsQueue$.asObservable();
}
public addOnMapDownloadedListener(func: (map: BsmLocalMap, version: BSVersion) => void) {
this.downloadedListerners.push(func);
}
public get lastDownloadedMap$(): Observable<{map: BsmLocalMap, version?: BSVersion}> {
if(!this._lastDownloadedMap$){
this._lastDownloadedMap$ = new Observable<{map: BsmLocalMap, version?: BSVersion}>(observer => {
const sub = this.ipc.sendV2("last-downloaded-map").subscribe(observer);
public removeOnMapDownloadedListener(func: (map: BsmLocalMap, version: BSVersion) => void) {
const funcIndex = this.downloadedListerners.indexOf(func);
if (funcIndex < 0) {
return;
return () => {
this._lastDownloadedMap$ = null;
sub.unsubscribe();
}
}).pipe(share());
}
this.downloadedListerners.splice(funcIndex, 1);
return this._lastDownloadedMap$;
}
public async oneClickInstallMap(map: BsvMapDetail): Promise<void> {
@@ -145,3 +145,5 @@ export interface MapDownload {
map: BsvMapDetail;
version: BSVersion;
}
export type MapsDownloadedListener = (map: BsmLocalMap, version: BSVersion) => void;
+7 -7
View File
@@ -7,6 +7,8 @@ import { useTranslation } from "../hooks/use-translation.hook";
import { AutoUpdaterService } from "../services/auto-updater.service";
import { WindowManagerService } from "../services/window-manager.service";
import { useService } from "renderer/hooks/use-service.hook";
import { lastValueFrom } from "rxjs";
import { logRenderError } from "renderer";
export default function Launcher() {
const updaterService = useService(AutoUpdaterService);
@@ -24,13 +26,11 @@ export default function Launcher() {
return windowService.openThenCloseAll("index.html");
}
setText("auto-update.downloading");
updaterService.downloadUpdate().then(installed => {
if (!installed) {
return windowService.openThenCloseAll("index.html");
}
updaterService.quitAndInstall();
});
});
return lastValueFrom(updaterService.downloadUpdate())
.then(() => updaterService.quitAndInstall())
.catch((err) => {logRenderError("omg", err); windowService.openThenCloseAll("index.html")});
}).catch(() => windowService.openThenCloseAll("index.html"));
}, []);
return (
+6 -5
View File
@@ -11,7 +11,7 @@ import { DepotDownloaderEvent } from "../bs-version-download/depot-downloader.mo
import { MSGetQuery, MSModel, MSModelType } from "../models/model-saber.model";
import { ModelDownload } from "renderer/services/models-management/models-downloader.service";
import { BsmLocalModel } from "../models/bsm-local-model.interface";
import { InstallModsResult, Mod, UninstallModsResult } from "../mods";
import { Mod } from "../mods";
import { BPList, DownloadPlaylistProgressionData } from "../playlists/playlist.interface";
import { VersionLinkerAction } from "renderer/services/version-folder-linker.service";
import { FileFilter, OpenDialogReturnValue } from "electron";
@@ -59,6 +59,7 @@ export interface IpcChannelMapping {
"delete-maps": { request: BsmLocalMap[], response: DeleteMapsProgress };
"export-maps": { request: { version: BSVersion; maps: BsmLocalMap[]; outPath: string }, response: Progression };
"download-map": { request: { map: BsvMapDetail; version: BSVersion }, response: BsmLocalMap };
"last-downloaded-map": { request: void, response: { version?: BSVersion, map: BsmLocalMap } };
"one-click-install-map": { request: BsvMapDetail, response: void };
"register-maps-deep-link": { request: void, response: boolean };
"unregister-maps-deep-link": { request: void, response: boolean };
@@ -78,9 +79,9 @@ export interface IpcChannelMapping {
/* ** 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: InstallModsResult };
"uninstall-mods": { request: { mods: Mod[]; version: BSVersion }, response: UninstallModsResult };
"uninstall-all-mods": { request: BSVersion, response: UninstallModsResult };
"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-playlist-ipcs ** */
"one-click-install-playlist": { request: string, response: Progression<DownloadPlaylistProgressionData> };
@@ -110,7 +111,7 @@ export interface IpcChannelMapping {
"relink-all-versions-folders": { request: void, response: void };
/* ** launcher-ipcs ** */
"download-update": { request: void, response: boolean };
"download-update": { request: void, response: Progression };
"check-update": { request: void, response: boolean };
"install-update": { request: void, response: void };