mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge branch 'v1.5.0' into feature/playlists/107
This commit is contained in:
@@ -36,10 +36,3 @@ ipc.on<void>("bs-launch.restore-steamvr", (_, reply) => {
|
||||
const steamLauncher = SteamLauncherService.getInstance();
|
||||
reply(from(steamLauncher.restoreSteamVR()));
|
||||
});
|
||||
|
||||
ipc.on<void>("restore-original-oculus-folder", (_, reply) => {
|
||||
const oculusLauncher = OculusLauncherService.getInstance();
|
||||
reply(from(
|
||||
oculusLauncher.deleteBsSymlinks().then(() => oculusLauncher.restoreOriginalBeatSaber())
|
||||
));
|
||||
});
|
||||
|
||||
@@ -5,9 +5,9 @@ import path from "path";
|
||||
import log from "electron-log";
|
||||
|
||||
export abstract class AbstractLauncherService {
|
||||
|
||||
|
||||
protected readonly localVersions = BSLocalVersionService.getInstance();
|
||||
|
||||
|
||||
constructor(){
|
||||
this.localVersions = BSLocalVersionService.getInstance();
|
||||
}
|
||||
@@ -46,18 +46,20 @@ export abstract class AbstractLauncherService {
|
||||
|
||||
}
|
||||
|
||||
protected launchBs(bsExePath: string, args: string[], options?: SpawnOptionsWithoutStdio): Promise<number> {
|
||||
return new Promise<number>((resolve, reject) => {
|
||||
const bsProcess = this.launchBSProcess(bsExePath, args, options);
|
||||
protected launchBs(bsExePath: string, args: string[], options?: SpawnOptionsWithoutStdio): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
|
||||
const process = this.launchBSProcess(bsExePath, args, options);
|
||||
|
||||
bsProcess.on("error", reject);
|
||||
bsProcess.on("exit", resolve);
|
||||
|
||||
setTimeout(() => {
|
||||
bsProcess.removeAllListeners("error");
|
||||
bsProcess.removeAllListeners("exit");
|
||||
resolve(-1);
|
||||
}, 30_000);
|
||||
const exit = new Promise<number>((resolve, reject) => {
|
||||
process.on("error", (err) => {
|
||||
log.error(`Error while launching BS`, err);
|
||||
reject(err);
|
||||
});
|
||||
process.on("exit", (code) => {
|
||||
log.info(`BS process exit with code ${code}`);
|
||||
resolve(code);
|
||||
});
|
||||
});
|
||||
|
||||
return { process, exit };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ import { taskRunning } from "../../helpers/os.helpers";
|
||||
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { ensurePathNotAlreadyExist } from "../../helpers/fs.helpers";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import { spawn } from "child_process";
|
||||
import { tryit } from "../../../shared/helpers/error.helpers";
|
||||
|
||||
export class OculusLauncherService extends AbstractLauncherService implements StoreLauncherInterface {
|
||||
|
||||
@@ -26,6 +29,7 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
|
||||
private readonly oculus: OculusService;
|
||||
private readonly pathsService: InstallationLocationService;
|
||||
private readonly util: UtilsService;
|
||||
|
||||
private readonly oculusLib$ = new ReplaySubject<string>();
|
||||
|
||||
@@ -33,6 +37,7 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
super();
|
||||
this.oculus = OculusService.getInstance();
|
||||
this.pathsService = InstallationLocationService.getInstance();
|
||||
this.util = UtilsService.getInstance();
|
||||
|
||||
this.oculus.tryGetGameFolder([OCULUS_BS_DIR, OCULUS_BS_BACKUP_DIR]).then(async dirPath => {
|
||||
if(dirPath){
|
||||
@@ -100,6 +105,15 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
return rename(bsFolderBackupPath, originalPath);
|
||||
}
|
||||
|
||||
private launchSymlinkCleaner(pid: number, symlinkPath: string){
|
||||
|
||||
log.info("Launch Symlink Cleaner", pid, symlinkPath);
|
||||
|
||||
const exeName = "oculus_symlink_cleaner.exe";
|
||||
const scriptPath = this.util.getAssetsScriptsPath();
|
||||
spawn(path.join(scriptPath, exeName), [`${pid}`, symlinkPath], { detached: true, stdio: "ignore" });
|
||||
}
|
||||
|
||||
public launch(launchOptions: LaunchOption): Observable<BSLaunchEventData> {
|
||||
|
||||
const prepareOriginalVersion: () => Promise<string> = async () => {
|
||||
@@ -157,7 +171,16 @@ export class OculusLauncherService extends AbstractLauncherService implements St
|
||||
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
||||
|
||||
// Launch Beat Saber
|
||||
return this.launchBs(exePath, this.buildBsLaunchArgs(launchOptions)).catch(err => {
|
||||
const process = this.launchBs(exePath, this.buildBsLaunchArgs(launchOptions));
|
||||
|
||||
if(launchOptions.version.oculus !== true && process?.process?.pid){
|
||||
const { error } = tryit(() => this.launchSymlinkCleaner(process.process.pid, bsPath));
|
||||
if(error){
|
||||
log.error("Error while launching symlink cleaner", error);
|
||||
}
|
||||
}
|
||||
|
||||
return process.exit.catch(err => {
|
||||
throw CustomError.fromError(err, BSLaunchError.BS_EXIT_ERROR);
|
||||
});
|
||||
|
||||
|
||||
@@ -107,7 +107,7 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
|
||||
|
||||
const launchPromise = !launchOptions.admin ? (
|
||||
this.launchBs(exePath, launchArgs, { env: {...process.env, "SteamAppId": BS_APP_ID} })
|
||||
this.launchBs(exePath, launchArgs, { env: {...process.env, "SteamAppId": BS_APP_ID} }).exit
|
||||
) : (
|
||||
new Promise<number>(resolve => {
|
||||
const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${exePath}" ${launchArgs.join(" ")}`, { env: {...process.env, "SteamAppId": BS_APP_ID} });
|
||||
@@ -123,13 +123,16 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
|
||||
})
|
||||
);
|
||||
|
||||
await launchPromise.then(exitCode => {
|
||||
try {
|
||||
const exitCode = await launchPromise;
|
||||
log.info("BS process exit code", exitCode);
|
||||
}).catch(err => {
|
||||
}
|
||||
catch(err: any) {
|
||||
throw CustomError.fromError(err, BSLaunchError.BS_EXIT_ERROR);
|
||||
}).finally(() => {
|
||||
this.restoreSteamVR().catch(log.error);
|
||||
});
|
||||
}
|
||||
finally {
|
||||
await this.restoreSteamVR().catch(log.error);
|
||||
}
|
||||
|
||||
})().then(() => {
|
||||
obs.complete();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import path from "path";
|
||||
import { app } from "electron";
|
||||
import ElectronStore from "electron-store";
|
||||
import { copyDirectoryWithJunctions, deleteFolder, ensureFolderExist } from "../helpers/fs.helpers";
|
||||
import { copyDirectoryWithJunctions, deleteFolder, ensureFolderExist, pathExist } from "../helpers/fs.helpers";
|
||||
import { tryit } from "../../shared/helpers/error.helpers";
|
||||
import { pathExistsSync } from "fs-extra";
|
||||
|
||||
export class InstallationLocationService {
|
||||
@@ -69,8 +70,8 @@ export class InstallationLocationService {
|
||||
return this.installPathConfig.get(this.STORE_INSTALLATION_PATH_KEY) as string;
|
||||
}
|
||||
|
||||
const oldPath = path.join(app.getPath("documents"), this.INSTALLATION_FOLDER);
|
||||
if(pathExistsSync(oldPath)){
|
||||
const { result: oldPath } = tryit(() => path.join(app.getPath("documents"), this.INSTALLATION_FOLDER));
|
||||
if(oldPath && pathExistsSync(oldPath)){
|
||||
return app.getPath("documents");
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { MAP_SPECIFICITIES } from "renderer/partials/maps/map-general/map-specificity";
|
||||
import { MAP_REQUIREMENTS } from "renderer/partials/maps/map-requirements/map-requirements";
|
||||
import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties/map-difficulties-colors";
|
||||
import { MapExclude, MAP_EXCLUDES } from "renderer/partials/maps/map-excludes/map-excludes";
|
||||
import { BsmButton } from "../../shared/bsm-button.component";
|
||||
import equal from "fast-deep-equal/es6";
|
||||
import clone from "rfdc";
|
||||
@@ -21,12 +22,13 @@ export type Props = {
|
||||
ref?: MutableRefObject<undefined>;
|
||||
playlist?: boolean;
|
||||
filter: MapFilter;
|
||||
localData?: boolean;
|
||||
onChange?: (filter: MapFilter) => void;
|
||||
onApply?: (filter: MapFilter) => void;
|
||||
onClose?: (filter: MapFilter) => void;
|
||||
};
|
||||
|
||||
export function FilterPanel({ className, ref, playlist = false, filter, onChange, onApply, onClose }: Props) {
|
||||
export function FilterPanel({ className, ref, playlist = false, filter, localData = true, onChange, onApply, onClose }: Props) {
|
||||
const t = useTranslation();
|
||||
|
||||
const [haveChanged, setHaveChanged] = useState(false);
|
||||
@@ -147,6 +149,10 @@ export function FilterPanel({ className, ref, playlist = false, filter, onChange
|
||||
return t(`maps.map-specificities.${specificity}`);
|
||||
};
|
||||
|
||||
const translateMapExclude = (exclude: MapExclude): string => {
|
||||
return t(`maps.map-excludes.${exclude}`);
|
||||
};
|
||||
|
||||
type BooleanKeys<T> = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T];
|
||||
|
||||
const handleCheckbox = (key: BooleanKeys<MapFilter>) => {
|
||||
@@ -189,6 +195,17 @@ export function FilterPanel({ className, ref, playlist = false, filter, onChange
|
||||
<span className="grow capitalize">{requirement}</span>
|
||||
</div>
|
||||
))}
|
||||
{ !localData && (
|
||||
<>
|
||||
<h2 className="my-1 uppercase text-sm">{t("maps.map-filter-panel.exclude")}</h2>
|
||||
{MAP_EXCLUDES.map(exclude => (
|
||||
<div key={exclude} className="flex justify-start items-center h-[22px] z-20 relative py-0.5 cursor-pointer" onClick={() => handleCheckbox(exclude)}>
|
||||
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.[exclude]} onChange={() => handleCheckbox(exclude)} />
|
||||
<span className="grow capitalize">{translateMapExclude(exclude)}</span>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
<section className="grow capitalize">
|
||||
<h2 className="uppercase text-sm mb-1">{t("maps.map-filter-panel.tags")}</h2>
|
||||
|
||||
@@ -77,12 +77,48 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadMaps = (params: SearchParams) => {
|
||||
const applyInstalledFilter = (maps: BsvMapDetail[]): BsvMapDetail[] => {
|
||||
return maps.filter(map => {
|
||||
if (!filter.installed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !map.versions.some(version => ownedMapHashs.includes(version.hash));
|
||||
});
|
||||
}
|
||||
|
||||
const loadMaps = (params: SearchParams, tryToLoad = 5) => {
|
||||
setLoading(() => true);
|
||||
beatSaver
|
||||
.searchMaps(params)
|
||||
.then(maps => setMaps(prev => [...prev, ...maps]))
|
||||
.finally(() => setLoading(() => false));
|
||||
|
||||
const searchResult = beatSaver.searchMaps(params);
|
||||
|
||||
if (!filter.installed) {
|
||||
searchResult
|
||||
.then(maps => setMaps(prev => [...prev, ...maps]))
|
||||
.finally(() => setLoading(() => false));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
searchResult
|
||||
.then(maps => {
|
||||
if (!maps.length) {
|
||||
setLoading(() => false);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
maps = applyInstalledFilter(maps);
|
||||
setMaps(prev => [...prev, ...maps])
|
||||
|
||||
if (maps.length < tryToLoad) {
|
||||
handleLoadMore();
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(() => false);
|
||||
})
|
||||
};
|
||||
|
||||
const renderMap = (map: BsvMapDetail) => {
|
||||
@@ -155,7 +191,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
|
||||
>
|
||||
<div className="flex h-9 gap-2 shrink-0">
|
||||
<BsmDropdownButton ref={filterContainerRef} className="shrink-0 h-full relative z-[1] flex justify-start" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1 !bg-light-main-color-1 dark:!bg-main-color-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" withBar={false}>
|
||||
<FilterPanel className="absolute top-[calc(100%+3px)] origin-top w-[450px] h-fit p-2 rounded-md shadow-md shadow-black" filter={filter} onChange={setFilter} onApply={handleSearch} onClose={() => filterContainerRef.current.close()} />
|
||||
<FilterPanel className="absolute top-[calc(100%+3px)] origin-top w-[450px] h-fit p-2 rounded-md shadow-md shadow-black" localData={false} filter={filter} onChange={setFilter} onApply={handleSearch} onClose={() => filterContainerRef.current.close()} />
|
||||
</BsmDropdownButton>
|
||||
<input className="h-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2 grow pb-0.5" type="text" name="" id="" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={query} onChange={e => setQuery(e.target.value)} />
|
||||
<BsmButton
|
||||
|
||||
@@ -19,11 +19,9 @@ export const OriginalOculusVersionBackupModal: ModalComponent<boolean, void> = (
|
||||
<form className="max-w-[450px] text-gray-800 dark:text-gray-200">
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.original-version-backup-oculus.title")}</h1>
|
||||
<BsmImage className="mx-auto h-20" image={BeatConflict} />
|
||||
|
||||
|
||||
<p className="text-sm italic mb-4 font-bold">{t("modals.original-version-backup-oculus.body.must-be-installed-once")}</p>
|
||||
<p className="mb-4">{t("modals.original-version-backup-oculus.body.need-backup")}</p>
|
||||
<p className="mb-4">{t("modals.original-version-backup-oculus.body.can-restore-later")}</p>
|
||||
<p className="text-sm italic mb-4">{t("modals.original-version-backup-oculus.body.tips-launch-oculus")}</p>
|
||||
<p className="mb-4">{t("modals.original-version-backup-oculus.body.will-backup")}</p>
|
||||
|
||||
<div className="flex flex-row justify-start items-center gap-1.5 my-4" >
|
||||
<BsmCheckbox className="relative z-[1] w-6 aspect-square" checked={dontShowAgain} onChange={setDontShowAgain} />
|
||||
|
||||
@@ -99,20 +99,6 @@ export function VersionViewer() {
|
||||
});
|
||||
}
|
||||
|
||||
const restoreOriginalOculusFolder = () => {
|
||||
lastValueFrom(ipcService.sendV2("restore-original-oculus-folder")).then(() => {
|
||||
notification.notifySuccess({
|
||||
title: "Restauration réussie",
|
||||
desc: "Le dossier a été restauré avec succès"
|
||||
});
|
||||
}).catch(() => {
|
||||
notification.notifyError({
|
||||
title: "Erreur lors de la restauration",
|
||||
desc: "Le dossier n'a pas pu être restauré"
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<BsmImage className="absolute w-full h-full top-0 left-0 object-cover" image={state.ReleaseImg || DefautVersionImage} errorImage={DefautVersionImage} />
|
||||
@@ -131,7 +117,6 @@ export function VersionViewer() {
|
||||
</div>
|
||||
<BsmDropdownButton className="absolute top-3 right-4 h-9 w-9 bg-light-main-color-2 dark:bg-main-color-2 rounded-md" items={[
|
||||
{ text: "pages.version-viewer.dropdown.open-folder", icon: "folder", onClick: openFolder },
|
||||
state.oculus && { text: "pages.version-viewer.dropdown.restore-oculus-folder", icon: "backup-restore", onClick: restoreOriginalOculusFolder},
|
||||
{ text: "pages.version-viewer.dropdown.verify-files", icon: "task", onClick: verifyFiles },
|
||||
!state.steam && !state.oculus && { text: "pages.version-viewer.dropdown.edit", icon: "edit", onClick: edit },
|
||||
!state.oculus && { text: "pages.version-viewer.dropdown.clone", icon: "copy", onClick: clone },
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
export type MapExclude = "installed";
|
||||
|
||||
export const MAP_EXCLUDES: MapExclude[] = ["installed"];
|
||||
@@ -164,6 +164,7 @@ export interface MapFilter {
|
||||
to?: number;
|
||||
fullSpread?: boolean;
|
||||
ranked?: boolean;
|
||||
installed?: boolean;
|
||||
minDuration?: number;
|
||||
maxDuration?: number;
|
||||
minNps?: number;
|
||||
|
||||
Reference in New Issue
Block a user