mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge branch 'master' into feat/702-respect-system-proxy-windows
This commit is contained in:
@@ -1,30 +1,48 @@
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import { ChildProcessWithoutNullStreams, SpawnOptionsWithoutStdio, spawn } from "child_process";
|
||||
import { Observable, ReplaySubject, Subscriber, filter, map, share } from "rxjs";
|
||||
import { DepotDownloaderArgsOptions, DepotDownloaderErrorEvent, DepotDownloaderEvent, DepotDownloaderEventType, DepotDownloaderEventTypes, DepotDownloaderInfoEvent, DepotDownloaderWarningEvent } from "../../shared/models/bs-version-download/depot-downloader.model";
|
||||
import { UtilsService } from 'main/services/utils.service';
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class DepotDownloader {
|
||||
|
||||
private static readonly EXE_PATH = path.join(
|
||||
UtilsService.getInstance().getAssetsScriptsPath(),
|
||||
process.platform === "win32" ? "DepotDownloader.exe" : "DepotDownloader"
|
||||
);
|
||||
|
||||
private process: ChildProcessWithoutNullStreams;
|
||||
private processOut$: Observable<string>;
|
||||
private subscriber: Subscriber<string>;
|
||||
|
||||
|
||||
public constructor(
|
||||
options: {
|
||||
command: string, args?: string[], options?: SpawnOptionsWithoutStdio, echoStartData?: unknown
|
||||
},
|
||||
args?: string[], options?: SpawnOptionsWithoutStdio, echoStartData?: unknown
|
||||
},
|
||||
logger?: Logger
|
||||
){
|
||||
this.processOut$ = new Observable<string>(subscriber => {
|
||||
) {
|
||||
if (!fs.existsSync(DepotDownloader.EXE_PATH)) {
|
||||
throw new CustomError(
|
||||
"DepotDownloader executable not found",
|
||||
process.platform === "win32"
|
||||
? DepotDownloaderErrorEvent.ExeNotFoundWindows
|
||||
: DepotDownloaderErrorEvent.ExeNotFoundLinux
|
||||
);
|
||||
}
|
||||
|
||||
this.processOut$ = new Observable<string>(subscriber => {
|
||||
this.subscriber = subscriber;
|
||||
|
||||
this.process = spawn(options.command, options.args ?? [], options.options);
|
||||
this.process = spawn(DepotDownloader.EXE_PATH, options.args ?? [], options.options);
|
||||
|
||||
subscriber.next(`[Info]|[Start]|${JSON.stringify(options.echoStartData) ?? ""}`);
|
||||
|
||||
|
||||
this.process.stdout.on("data", data => {
|
||||
const stringData: string = data.toString();
|
||||
|
||||
|
||||
if(!stringData.includes(DepotDownloaderInfoEvent.Progress) && !stringData.includes(DepotDownloaderInfoEvent.Validated)){
|
||||
logger?.info("DepotDownloader stdout:", stringData);
|
||||
}
|
||||
@@ -32,7 +50,7 @@ export class DepotDownloader {
|
||||
const lines: string[] = stringData.split("\n");
|
||||
lines.forEach(line => subscriber.next(line));
|
||||
});
|
||||
|
||||
|
||||
this.process.on("error", error => subscriber.error(error));
|
||||
this.process.stderr.on("error", error => subscriber.error(error));
|
||||
this.process.on("exit", () => subscriber.complete());
|
||||
@@ -74,7 +92,7 @@ export class DepotDownloader {
|
||||
data: splitedLine[2],
|
||||
}
|
||||
|
||||
}),
|
||||
}),
|
||||
filter(Boolean));
|
||||
}
|
||||
|
||||
@@ -93,7 +111,7 @@ export class DepotDownloader {
|
||||
const args: string[] = [];
|
||||
|
||||
for(const [key, value] of Object.entries(depotDownloaderArgs)){
|
||||
|
||||
|
||||
if(value === true){
|
||||
args.push(`-${key}`);
|
||||
}
|
||||
@@ -102,7 +120,7 @@ export class DepotDownloader {
|
||||
args.push(`${value}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
@@ -112,4 +130,4 @@ interface Logger {
|
||||
info: (...args: unknown[]) => void,
|
||||
warn: (...args: unknown[]) => void,
|
||||
error: (...args: unknown[]) => void,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,6 +131,7 @@ export class BSLauncherService {
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.DEBUG)){ res.debug = "true"; }
|
||||
if(launchOptions.additionalArgs){ res.additionalArgs = launchOptions.additionalArgs; }
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.SKIP_STEAM)){ res.skipSteam = "true"; }
|
||||
if(launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)){ res.protonLogs = "true"; }
|
||||
|
||||
return res;
|
||||
}
|
||||
@@ -248,6 +249,7 @@ type ShortcutParams = {
|
||||
debug?: string;
|
||||
additionalArgs?: string[];
|
||||
skipSteam?: string;
|
||||
protonLogs?: string;
|
||||
version: string;
|
||||
versionName?: string;
|
||||
versionIno?: string;
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { BS_APP_ID, BS_DEPOT } from "../../constants";
|
||||
import path from "path";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import log from "electron-log";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
@@ -17,14 +16,12 @@ import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
export class BsSteamDownloaderService {
|
||||
private static instance: BsSteamDownloaderService;
|
||||
|
||||
private readonly utils: UtilsService;
|
||||
private readonly installLocationService: InstallationLocationService;
|
||||
private readonly localVersionService: BSLocalVersionService;
|
||||
|
||||
private depotDownloader: DepotDownloader;
|
||||
|
||||
private constructor() {
|
||||
this.utils = UtilsService.getInstance();
|
||||
this.installLocationService = InstallationLocationService.getInstance();
|
||||
this.localVersionService = BSLocalVersionService.getInstance();
|
||||
|
||||
@@ -40,10 +37,6 @@ export class BsSteamDownloaderService {
|
||||
return BsSteamDownloaderService.instance;
|
||||
}
|
||||
|
||||
private getDepotDownloaderExePath(): string {
|
||||
return path.join(this.utils.getAssetsScriptsPath(), process.platform === 'linux' ? "DepotDownloader" : "DepotDownloader.exe");
|
||||
}
|
||||
|
||||
private async buildDepotDownloaderInstance(downloadInfos: DownloadSteamInfo, qr?: boolean): Promise<{depotDownloader: DepotDownloader, depotDownloaderOptions: DepotDownloaderArgsOptions, version: BSVersion}> {
|
||||
|
||||
const versionPath = await this.localVersionService.getVersionPath(downloadInfos.bsVersion);
|
||||
@@ -68,11 +61,9 @@ export class BsSteamDownloaderService {
|
||||
|
||||
await ensureDir(this.installLocationService.versionsDirectory());
|
||||
|
||||
const exePath = this.getDepotDownloaderExePath();
|
||||
const args = DepotDownloader.buildArgs(depotDownloaderOptions);
|
||||
|
||||
const depotDownloader = new DepotDownloader({
|
||||
command: exePath,
|
||||
args,
|
||||
options: { cwd: this.installLocationService.versionsDirectory() },
|
||||
echoStartData: downloadVersion
|
||||
@@ -104,11 +95,21 @@ export class BsSteamDownloaderService {
|
||||
finalize(() => this.localVersionService.initVersionMetadata(version, { store: BsStore.STEAM }))
|
||||
).subscribe(sub);
|
||||
|
||||
}).catch(err => sub.error({
|
||||
type: DepotDownloaderEventType.Error,
|
||||
subType: DepotDownloaderErrorEvent.Unknown,
|
||||
data: err
|
||||
} as DepotDownloaderEvent));
|
||||
}).catch(err => {
|
||||
if (err instanceof CustomError
|
||||
&& Object.values(DepotDownloaderErrorEvent).includes(
|
||||
err.code as DepotDownloaderErrorEvent
|
||||
)
|
||||
) {
|
||||
return sub.error(err);
|
||||
}
|
||||
|
||||
return sub.error({
|
||||
type: DepotDownloaderEventType.Error,
|
||||
subType: DepotDownloaderErrorEvent.Unknown,
|
||||
data: err
|
||||
} as DepotDownloaderEvent)
|
||||
});
|
||||
|
||||
return () => {
|
||||
depotDownloaderBuildPromise.then(({ depotDownloader }) => depotDownloader.stop());
|
||||
|
||||
@@ -6,6 +6,7 @@ import { StaticConfigurationService } from "./static-configuration.service";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { BSLaunchError, LaunchOption } from "shared/models/bs-launch";
|
||||
import { bsmExec } from "main/helpers/os.helpers";
|
||||
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
|
||||
|
||||
export class LinuxService {
|
||||
private static instance: LinuxService;
|
||||
@@ -78,11 +79,15 @@ export class LinuxService {
|
||||
"STEAM_COMPAT_CLIENT_INSTALL_PATH": steamPath,
|
||||
"STEAM_COMPAT_APP_ID": BS_APP_ID,
|
||||
// Run game in steam environment; fixes #585 for unicode song titles
|
||||
"SteamEnv": "1",
|
||||
// Uncomment these to create a proton log file in the Beat Saber install directory.
|
||||
// "PROTON_LOG": 1,
|
||||
// "PROTON_LOG_DIR": bsFolderPath,
|
||||
"SteamEnv": 1,
|
||||
});
|
||||
|
||||
if (launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)) {
|
||||
Object.assign(env, {
|
||||
"PROTON_LOG": 1,
|
||||
"PROTON_LOG_DIR": path.join(bsFolderPath, "Logs"),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public verifyProtonPath(protonFolder: string = ""): boolean {
|
||||
|
||||
+3
-3
@@ -2,7 +2,7 @@ import Tippy from "@tippyjs/react";
|
||||
import { GlowEffect } from "renderer/components/shared/glow-effect.component";
|
||||
import { BsmIcon } from "renderer/components/svgs/bsm-icon.component";
|
||||
import { SvgIcon } from "renderer/components/svgs/svg-icon.type";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
|
||||
|
||||
type Props = {
|
||||
onClick: (active: boolean) => void;
|
||||
@@ -13,7 +13,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export function LaunchModToogle({ onClick, active, text, icon: Icon, infoText }: Props) {
|
||||
const t = useTranslation();
|
||||
const { text: t } = useTranslationV2();
|
||||
|
||||
return (
|
||||
<button className={`shrink-0 relative rounded-full cursor-pointer group active:scale-95 transition-transform ${!active && "shadow-md shadow-black"}`} onClick={() => onClick(!active)}>
|
||||
@@ -22,7 +22,7 @@ export function LaunchModToogle({ onClick, active, text, icon: Icon, infoText }:
|
||||
{Icon && <Icon className="h-7 shrink-0 text-gray-800 dark:text-white" />}
|
||||
<span className="w-fit min-w-fit text-lg font-bold uppercase tracking-wide italic ">{t(text)}</span>
|
||||
{infoText && (
|
||||
<Tippy content={t(infoText)} placement="top" theme="default" delay={[400, 0]}>
|
||||
<Tippy content={t(infoText)} className="break-words" placement="top" theme="default" delay={[200, 0]}>
|
||||
<div className="h-[25px] w-[25px] shrink-0 p-1.5 rounded-full cursor-help bg-light-main-color-1 dark:bg-main-color-3 hover:brightness-110">
|
||||
<BsmIcon className="w-full h-full" icon="info" />
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -52,7 +52,7 @@ export function LaunchModItem({ id, icon: Icon, label, description, active, visi
|
||||
const { text: t } = useTranslationV2();
|
||||
|
||||
return (
|
||||
<Tippy theme="default" placement="top" content={description}>
|
||||
<Tippy theme="default" className="break-words" placement="top" content={description}>
|
||||
<button id={id} className={cn("grow rounded-md bg-theme-1 relative flex justify-center items-center h-10 py-1 px-3", visible === false && "hidden")} onClick={e => { e.preventDefault(); e.stopPropagation(); onChange?.(!active) }}>
|
||||
<BsmCheckbox className="h-4 aspect-square z-[1] relative mr-1.5" checked={active} onChange={onChange}/>
|
||||
{Icon && <Icon className="h-full w-fit py-0.5 mr-1.5"/>}
|
||||
|
||||
@@ -62,7 +62,22 @@ export function LaunchSlide({ version }: Props) {
|
||||
}
|
||||
}, [activeLaunchMods]);
|
||||
|
||||
const toggleActiveLaunchMod = (checked: boolean, launchMod: LaunchMod) => checked
|
||||
? setActiveLaunchMods(prev => [...prev, launchMod])
|
||||
: setActiveLaunchMods(prev => prev.filter(mod => mod !== launchMod));
|
||||
|
||||
const togglePinnedLaunchMod = (pinned: boolean, launchMod: LaunchMod) => pinned
|
||||
? setPinnedLaunchMods(prev => [...prev, launchMod])
|
||||
: setPinnedLaunchMods(prev => prev.filter(mod => mod !== launchMod));
|
||||
|
||||
useEffect(() => {
|
||||
let protonLogsPath: string[] = [];
|
||||
if (window.electron.platform === "linux") {
|
||||
protonLogsPath = version.steam
|
||||
? [version.path, "Logs"]
|
||||
: ["BSInstances", version.name, "Logs"];
|
||||
}
|
||||
|
||||
setLaunchModItems(() => [
|
||||
{
|
||||
id: LaunchMods.OCULUS,
|
||||
@@ -72,8 +87,8 @@ export function LaunchSlide({ version }: Props) {
|
||||
active: activeLaunchMods.includes(LaunchMods.OCULUS),
|
||||
visible: !(version.metadata?.store === BsStore.OCULUS),
|
||||
pinned: pinnedLaunchMods.includes(LaunchMods.OCULUS),
|
||||
onChange: checked => checked ? setActiveLaunchMods(prev => [...prev, LaunchMods.OCULUS]) : setActiveLaunchMods(prev => prev.filter(mod => mod !== LaunchMods.OCULUS)),
|
||||
onPinChange: pinned => pinned ? setPinnedLaunchMods(prev => [...prev, LaunchMods.OCULUS]) : setPinnedLaunchMods(prev => prev.filter(mod => mod !== LaunchMods.OCULUS)),
|
||||
onChange: (checked) => toggleActiveLaunchMod(checked, LaunchMods.OCULUS),
|
||||
onPinChange: (pinned) => togglePinnedLaunchMod(pinned, LaunchMods.OCULUS),
|
||||
},
|
||||
{
|
||||
id: LaunchMods.FPFC,
|
||||
@@ -82,8 +97,8 @@ export function LaunchSlide({ version }: Props) {
|
||||
description: t("pages.version-viewer.launch-mods.desktop-description"),
|
||||
active: activeLaunchMods.includes(LaunchMods.FPFC),
|
||||
pinned: pinnedLaunchMods.includes(LaunchMods.FPFC),
|
||||
onChange: checked => checked ? setActiveLaunchMods(prev => [...prev, LaunchMods.FPFC]) : setActiveLaunchMods(prev => prev.filter(mod => mod !== LaunchMods.FPFC)),
|
||||
onPinChange: pinned => pinned ? setPinnedLaunchMods(prev => [...prev, LaunchMods.FPFC]) : setPinnedLaunchMods(prev => prev.filter(mod => mod !== LaunchMods.FPFC)),
|
||||
onChange: (checked) => toggleActiveLaunchMod(checked, LaunchMods.FPFC),
|
||||
onPinChange: (pinned) => togglePinnedLaunchMod(pinned, LaunchMods.FPFC),
|
||||
},
|
||||
{
|
||||
id: LaunchMods.DEBUG,
|
||||
@@ -92,8 +107,8 @@ export function LaunchSlide({ version }: Props) {
|
||||
description: t("pages.version-viewer.launch-mods.debug-description"),
|
||||
active: activeLaunchMods.includes(LaunchMods.DEBUG),
|
||||
pinned: pinnedLaunchMods.includes(LaunchMods.DEBUG),
|
||||
onChange: checked => checked ? setActiveLaunchMods(prev => [...prev, LaunchMods.DEBUG]) : setActiveLaunchMods(prev => prev.filter(mod => mod !== LaunchMods.DEBUG)),
|
||||
onPinChange: pinned => pinned ? setPinnedLaunchMods(prev => [...prev, LaunchMods.DEBUG]) : setPinnedLaunchMods(prev => prev.filter(mod => mod !== LaunchMods.DEBUG)),
|
||||
onChange: (checked) => toggleActiveLaunchMod(checked, LaunchMods.DEBUG),
|
||||
onPinChange: (pinned) => togglePinnedLaunchMod(pinned, LaunchMods.DEBUG),
|
||||
},
|
||||
{
|
||||
id: LaunchMods.SKIP_STEAM,
|
||||
@@ -101,8 +116,8 @@ export function LaunchSlide({ version }: Props) {
|
||||
description: t("pages.version-viewer.launch-mods.skipsteam-description"),
|
||||
active: activeLaunchMods.includes(LaunchMods.SKIP_STEAM),
|
||||
pinned: pinnedLaunchMods.includes(LaunchMods.SKIP_STEAM),
|
||||
onChange: checked => checked ? setActiveLaunchMods(prev => [...prev, LaunchMods.SKIP_STEAM]) : setActiveLaunchMods(prev => prev.filter(mod => mod !== LaunchMods.SKIP_STEAM)),
|
||||
onPinChange: pinned => pinned ? setPinnedLaunchMods(prev => [...prev, LaunchMods.SKIP_STEAM]) : setPinnedLaunchMods(prev => prev.filter(mod => mod !== LaunchMods.SKIP_STEAM)),
|
||||
onChange: (checked) => toggleActiveLaunchMod(checked, LaunchMods.SKIP_STEAM),
|
||||
onPinChange: (pinned) => togglePinnedLaunchMod(pinned, LaunchMods.SKIP_STEAM),
|
||||
},
|
||||
{
|
||||
id: LaunchMods.EDITOR,
|
||||
@@ -112,14 +127,25 @@ export function LaunchSlide({ version }: Props) {
|
||||
active: activeLaunchMods.includes(LaunchMods.EDITOR),
|
||||
pinned: pinnedLaunchMods.includes(LaunchMods.EDITOR),
|
||||
visible: !safeLt(version.BSVersion, "1.23.0"),
|
||||
onChange: checked => checked ? setActiveLaunchMods(prev => [...prev, LaunchMods.EDITOR]) : setActiveLaunchMods(prev => prev.filter(mod => mod !== LaunchMods.EDITOR)),
|
||||
onPinChange: pinned => pinned ? setPinnedLaunchMods(prev => [...prev, LaunchMods.EDITOR]) : setPinnedLaunchMods(prev => prev.filter(mod => mod !== LaunchMods.EDITOR)),
|
||||
|
||||
}
|
||||
onChange: (checked) => toggleActiveLaunchMod(checked, LaunchMods.EDITOR),
|
||||
onPinChange: (pinned) => togglePinnedLaunchMod(pinned, LaunchMods.EDITOR),
|
||||
},
|
||||
{
|
||||
id: LaunchMods.PROTON_LOGS,
|
||||
label: t("pages.version-viewer.launch-mods.proton-logs"),
|
||||
description: t("pages.version-viewer.launch-mods.proton-logs-description", {
|
||||
versionPath: `${window.electron.path.join(...protonLogsPath)}/`
|
||||
}),
|
||||
active: activeLaunchMods.includes(LaunchMods.PROTON_LOGS),
|
||||
pinned: pinnedLaunchMods.includes(LaunchMods.PROTON_LOGS),
|
||||
visible: window.electron.platform === "linux",
|
||||
onChange: (checked) => toggleActiveLaunchMod(checked, LaunchMods.PROTON_LOGS),
|
||||
onPinChange: (pinned) => togglePinnedLaunchMod(pinned, LaunchMods.PROTON_LOGS),
|
||||
},
|
||||
]);
|
||||
}, [activeLaunchMods, pinnedLaunchMods, version]);
|
||||
|
||||
const launch = () => {
|
||||
const launch = async () => {
|
||||
const additionalArgs = additionalArgsString?.split(";").map(arg => arg.trim()).filter(arg => arg.length > 0);
|
||||
|
||||
const launch$ = bsLauncherService.launch({
|
||||
@@ -193,5 +219,3 @@ export function LaunchSlide({ version }: Props) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { ConfigurationService } from "./configuration.service";
|
||||
import { getProperty } from "dot-prop";
|
||||
import { i18n } from "dateformat";
|
||||
import { createElement, Fragment } from "react";
|
||||
import { logRenderError } from "renderer";
|
||||
|
||||
export class I18nService {
|
||||
private static instance: I18nService;
|
||||
@@ -79,7 +80,13 @@ export class I18nService {
|
||||
public translate(translationKey: string, args?: Record<string, string>): string {
|
||||
let translated = this.cache.get(translationKey);
|
||||
if (!translated) {
|
||||
translated = getProperty(this.dictionary, translationKey) ?? translationKey;
|
||||
try {
|
||||
translated = getProperty(this.dictionary, translationKey) ?? translationKey;
|
||||
} catch (error) {
|
||||
// Invalid translationKey like strings containing "[]"
|
||||
logRenderError(`Could not get property of ${translationKey}`, error);
|
||||
translated = translationKey;
|
||||
}
|
||||
this.cache.set(translationKey, translated);
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ export const LaunchMods = {
|
||||
DEBUG: "debug",
|
||||
SKIP_STEAM: "skip_steam",
|
||||
EDITOR: "editor",
|
||||
PROTON_LOGS: "proton_logs",
|
||||
} as const;
|
||||
|
||||
export type LaunchMod = typeof LaunchMods[keyof typeof LaunchMods];
|
||||
|
||||
@@ -2,7 +2,6 @@ export enum DepotDownloaderEventType {
|
||||
Error = "Error",
|
||||
Warning = "Warning",
|
||||
Info = "Info",
|
||||
|
||||
}
|
||||
|
||||
export interface DepotDownloaderEvent<T = unknown> {
|
||||
@@ -27,6 +26,8 @@ export enum DepotDownloaderInfoEvent {
|
||||
}
|
||||
|
||||
export enum DepotDownloaderErrorEvent {
|
||||
ExeNotFoundWindows = "ExeNotFoundWindows",
|
||||
ExeNotFoundLinux = "ExeNotFoundLinux",
|
||||
Password = "Password",
|
||||
InvalidCredentials = "InvalidCredentials",
|
||||
NoManifest = "NoManifest",
|
||||
@@ -66,4 +67,4 @@ export interface DepotDownloaderArgsOptions {
|
||||
dir: string,
|
||||
validate?: boolean,
|
||||
qr?: boolean,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user