From 96fb2ed8c7c955b6bbef3b2d692b10155fd176fb Mon Sep 17 00:00:00 2001 From: silentrald Date: Wed, 15 Jan 2025 19:58:03 +0800 Subject: [PATCH 01/29] [bugfix-738] fixed issue when creating Beat Saber shortcuts --- electron-builder.config.js | 1 + 1 file changed, 1 insertion(+) diff --git a/electron-builder.config.js b/electron-builder.config.js index c692c4dc..94858c8f 100644 --- a/electron-builder.config.js +++ b/electron-builder.config.js @@ -65,6 +65,7 @@ const config = { // Audio output "--socket=pulseaudio", // Read/write home directory access + "--filesystem=~/Desktop:rw", // allow writing shortcuts to desktop "--filesystem=~/BSManager:create", // Default BSManager installation folder "--filesystem=~/.steam/steam/steamapps:ro", // for the libraryfolders.vdf "--filesystem=~/.steam/steam/steamapps/common:create", // Steam game folder From a47616086ca2a40491105e32273ea19705caa224 Mon Sep 17 00:00:00 2001 From: silentrald Date: Wed, 15 Jan 2025 19:58:36 +0800 Subject: [PATCH 02/29] [bugfix] fixed issue when proton logs with flatpak --- src/main/services/bs-launcher/abstract-launcher.service.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/services/bs-launcher/abstract-launcher.service.ts b/src/main/services/bs-launcher/abstract-launcher.service.ts index cf4ee365..6bd98495 100644 --- a/src/main/services/bs-launcher/abstract-launcher.service.ts +++ b/src/main/services/bs-launcher/abstract-launcher.service.ts @@ -70,6 +70,8 @@ export abstract class AbstractLauncherService { "STEAM_COMPAT_CLIENT_INSTALL_PATH", "STEAM_COMPAT_APP_ID", "SteamEnv", + "PROTON_LOG", + "PROTON_LOG_DIR", ], }, }); From 350dff37cf37acac71faf7fe6abba690b7bd0e89 Mon Sep 17 00:00:00 2001 From: silentrald Date: Wed, 15 Jan 2025 23:25:40 +0800 Subject: [PATCH 03/29] [bugfix] refactored bs launch + launch bs version directly with shortcut --- .../bs-launcher/abstract-launcher.service.ts | 11 +- .../bs-launcher/bs-launcher.service.ts | 39 ++---- .../bs-launcher/steam-launcher.service.ts | 12 +- src/main/services/linux.service.ts | 111 +++++++++++++----- src/main/services/steam.service.ts | 2 +- 5 files changed, 108 insertions(+), 67 deletions(-) diff --git a/src/main/services/bs-launcher/abstract-launcher.service.ts b/src/main/services/bs-launcher/abstract-launcher.service.ts index 6bd98495..ba48b163 100644 --- a/src/main/services/bs-launcher/abstract-launcher.service.ts +++ b/src/main/services/bs-launcher/abstract-launcher.service.ts @@ -46,7 +46,7 @@ export abstract class AbstractLauncherService { return Array.from(new Set(launchArgs).values()); } - protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams { + protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): ChildProcessWithoutNullStreams { const spawnOptions: SpawnOptionsWithoutStdio = { detached: true, cwd: path.dirname(bsExePath), ...(options || {}) }; @@ -57,7 +57,7 @@ export abstract class AbstractLauncherService { spawnOptions.shell = true; // For windows to spawn properly return bsmSpawn(`"${bsExePath}"`, { args, options: spawnOptions, log: BsmShellLog.Command, - linux: { prefix: this.linux.getProtonPrefix() }, + linux: { prefix: options?.protonPrefix || "" }, flatpak: { host: IS_FLATPAK, env: [ @@ -80,7 +80,7 @@ export abstract class AbstractLauncherService { protected launchBs(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): {process: ChildProcessWithoutNullStreams, exit: Promise} { const process = this.launchBSProcess(bsExePath, args, options); - let timoutId: NodeJS.Timeout; + let timeoutId: NodeJS.Timeout; const exit = new Promise((resolve, reject) => { // Don't remove, useful for debugging! @@ -103,7 +103,7 @@ export abstract class AbstractLauncherService { const unrefAfter = options?.unrefAfter ?? sToMs(10); - timoutId = setTimeout(() => { + timeoutId = setTimeout(() => { log.error("BS process unref after timeout", unrefAfter); process.unref(); process.removeAllListeners(); @@ -111,7 +111,7 @@ export abstract class AbstractLauncherService { }, unrefAfter); }).finally(() => { - clearTimeout(timoutId); + clearTimeout(timeoutId); }); return { process, exit }; @@ -119,5 +119,6 @@ export abstract class AbstractLauncherService { } export type SpawnBsProcessOptions = { + protonPrefix?: string; unrefAfter?: number; } & SpawnOptionsWithoutStdio; diff --git a/src/main/services/bs-launcher/bs-launcher.service.ts b/src/main/services/bs-launcher/bs-launcher.service.ts index 6ebe7ac7..ac8cd546 100644 --- a/src/main/services/bs-launcher/bs-launcher.service.ts +++ b/src/main/services/bs-launcher/bs-launcher.service.ts @@ -24,6 +24,7 @@ import { LaunchMod, LaunchMods } from "shared/models/bs-launch/launch-option.int import { StaticConfigurationService } from "../static-configuration.service"; import { SteamService } from "../steam.service"; import { tryit } from "shared/helpers/error.helpers"; +import { LinuxService } from "../linux.service"; export class BSLauncherService { private static instance: BSLauncherService; @@ -37,6 +38,7 @@ export class BSLauncherService { private readonly steam: SteamService; private readonly oculusLauncher: OculusLauncherService; private readonly staticConfig: StaticConfigurationService; + private readonly linux: LinuxService; public static getInstance(): BSLauncherService { if (!BSLauncherService.instance) { @@ -55,6 +57,7 @@ export class BSLauncherService { this.oculusLauncher = OculusLauncherService.getInstance(); this.staticConfig = StaticConfigurationService.getInstance(); this.steam = SteamService.getInstance(); + this.linux = LinuxService.getInstance(); this.bsmProtocolService.on("launch", link => { log.info("Launch from bsm protocol", link.toString()); @@ -230,12 +233,13 @@ export class BSLauncherService { description: [shortcutName, launchOptions.version.color].join(" "), // <= Need color in description to help windows know that the shortcut is different }) ), - linux: async () => ( - createDesktopUrlShortcut(path.join(app.getPath("desktop"), `${shortcutName}.desktop`), { - name: shortcutName, - url: shortcutUrl, - icon: await this.createShortcutPng(shortcutIconColor), - }) + linux: async () => this.linux.createDesktopShortcut( + path.join(app.getPath("desktop"), `${shortcutName}.desktop`), + shortcutName, + await this.createShortcutPng(shortcutIconColor), + launchOptions, + await this.steam.getSteamPath(), + await this.localVersionService.getVersionPath(launchOptions.version) ) }) @@ -282,26 +286,3 @@ type ShortcutParams = { versionOculus?: string; } -/** - * Create .desktop file for url shortcut (only for linux) - * @param {string} shortcutPath - * @param options - * @returns - */ -function createDesktopUrlShortcut(shortcutPath: string, options?: { - url: string - name: string, - icon: string -}): Promise { - const { url, name, icon } = options || {}; - - const data = [ - "[Desktop Entry]", - "Type=Link", - `Name=${name}`, - `Icon=${icon}`, - `URL=${url}` - ].join("\n"); - - return writeFile(shortcutPath, data).then(() => true); -} diff --git a/src/main/services/bs-launcher/steam-launcher.service.ts b/src/main/services/bs-launcher/steam-launcher.service.ts index d3d25b31..96eb3822 100644 --- a/src/main/services/bs-launcher/steam-launcher.service.ts +++ b/src/main/services/bs-launcher/steam-launcher.service.ts @@ -112,9 +112,14 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto "SteamGameId": BS_APP_ID, }; + let protonPrefix = ""; // Linux setup if (process.platform === "linux") { - await this.linux.setupLaunch(launchOptions, steamPath, bsFolderPath, env); + const linuxSetup = await this.linux.setupLaunch( + launchOptions, steamPath, bsFolderPath + ); + protonPrefix = linuxSetup.protonPrefix; + Object.assign(env, linuxSetup.env); } obs.next({type: BSLaunchEvent.BS_LAUNCHING}); @@ -122,7 +127,10 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto const spawnOpts = { env, cwd: bsFolderPath }; const launchPromise = !launchOptions.admin ? ( - this.launchBs(bsExePath, launchArgs, spawnOpts).exit + this.launchBs(bsExePath, launchArgs, { + ...spawnOpts, + protonPrefix + }).exit ) : ( new Promise(resolve => { const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${bsExePath}" ${launchArgs.join(" ")} --log-path "${path.join(app.getPath("logs"), "bs-admin-start.log")}"`, spawnOpts); diff --git a/src/main/services/linux.service.ts b/src/main/services/linux.service.ts index 8db15e8d..b3ef82af 100644 --- a/src/main/services/linux.service.ts +++ b/src/main/services/linux.service.ts @@ -1,7 +1,7 @@ import fs from "fs-extra"; import log from "electron-log"; import path from "path"; -import { BS_APP_ID, IS_FLATPAK, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants"; +import { BS_APP_ID, BS_EXECUTABLE, IS_FLATPAK, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants"; import { InstallationLocationService } from "./installation-location.service"; import { StaticConfigurationService } from "./static-configuration.service"; import { CustomError } from "shared/models/exceptions/custom-error.class"; @@ -21,7 +21,6 @@ export class LinuxService { private readonly installLocationService: InstallationLocationService; private readonly staticConfig: StaticConfigurationService; - private protonPrefix = ""; private nixOS: boolean | undefined; @@ -40,24 +39,16 @@ export class LinuxService { public async setupLaunch( launchOptions: LaunchOption, steamPath: string, - bsFolderPath: string, - env: Record - ) { + bsFolderPath: string + ): Promise<{ + protonPrefix: string; + env: Record; + }> { if (launchOptions.admin) { log.warn("Launching as admin is not supported on Linux! Starting the game as a normal user."); launchOptions.admin = false; } - // Create the compat data path if it doesn't exist. - // If the user never ran Beat Saber through steam before - // using bsmanager, it won't exist, and proton will fail - // to launch the game. - const compatDataPath = this.getCompatDataPath(); - if (!fs.existsSync(compatDataPath)) { - log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`); - fs.mkdirSync(compatDataPath); - } - if (!this.staticConfig.has("proton-folder")) { throw CustomError.fromError( new Error("Proton folder not set"), @@ -75,27 +66,46 @@ export class LinuxService { ); } - this.protonPrefix = await this.isNixOS() - ? `steam-run "${protonPath}" run` - : `"${protonPath}" run`; + return { + protonPrefix: await this.isNixOS() + ? `steam-run "${protonPath}" run` + : `"${protonPath}" run`, + env: await this.prepareEnvVariables(launchOptions, steamPath, bsFolderPath) + }; + } + + private async prepareEnvVariables( + launchOptions: LaunchOption, + steamPath: string, + bsFolderPath: string + ): Promise> { + // Create the compat data path if it doesn't exist. + // If the user never ran Beat Saber through steam before + // using bsmanager, it won't exist, and proton will fail + // to launch the game. + const compatDataPath = this.getCompatDataPath(); + if (!fs.existsSync(compatDataPath)) { + log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`); + fs.mkdirSync(compatDataPath); + } // Setup Proton environment variables - Object.assign(env, { + const envVars: Record = { "WINEDLLOVERRIDES": "winhttp=n,b", // Required for mods to work "STEAM_COMPAT_DATA_PATH": compatDataPath, "STEAM_COMPAT_INSTALL_PATH": bsFolderPath, "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, - }); + "SteamEnv": "1", + }; if (launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)) { - Object.assign(env, { - "PROTON_LOG": 1, - "PROTON_LOG_DIR": path.join(bsFolderPath, "Logs"), - }); + envVars.PROTON_LOG = "1"; + envVars.PROTON_LOG_DIR = path.join(bsFolderPath, "Logs"); } + + return envVars; } public verifyProtonPath(protonFolder: string = ""): boolean { @@ -134,11 +144,6 @@ export class LinuxService { ? path.join(compatDataPath, "pfx") : ""; } - public getProtonPrefix(): string { - // Set in setupLaunch - return this.protonPrefix; - } - // === NixOS Specific === // public async isNixOS(): Promise { @@ -159,4 +164,50 @@ export class LinuxService { return this.nixOS; } + + // === Shortcuts === // + + public async createDesktopShortcut( + shortcutPath: string, + name: string, + icon: string, + launchOptions: LaunchOption, + steamPath: string, + bsFolderPath: string + ): Promise { + try { + const { + protonPrefix, env + } = await this.setupLaunch(launchOptions, steamPath, bsFolderPath); + + Object.assign(env, { + "SteamAppId": BS_APP_ID, + "SteamOverlayGameId": BS_APP_ID, + "SteamGameId": BS_APP_ID, + }); + + const envString = Object.entries(env) + .map(([ key, value ]) => `${key}="${value}"`) + .join(" "); + const command = `${envString} ${protonPrefix} "${ + path.join(bsFolderPath, BS_EXECUTABLE) + }"`; + + const desktopEntry = [ + "[Desktop Entry]", + "Type=Application", + `Name=${name}`, + `Icon=${icon}`, + `Path=${bsFolderPath}`, + `Exec=${command}` + ].join("\n"); + + await fs.writeFile(shortcutPath, desktopEntry); + log.info("Created shorcut at ", `"${shortcutPath}/${name}"`); + return true; + } catch (error) { + log.error("Could not create shortcut", error); + return false; + } + } } diff --git a/src/main/services/steam.service.ts b/src/main/services/steam.service.ts index 9298f8c8..54414ff5 100644 --- a/src/main/services/steam.service.ts +++ b/src/main/services/steam.service.ts @@ -211,7 +211,7 @@ export class SteamService { for (const userId of userIds) { const shortcuts = await this.getShortcuts(userId).catch(e => { log.warn("Error while reading shortcuts", e); - return []; + return [] as SteamShortcut[]; }); shortcuts.push(new SteamShortcut(shortcutData)); From dc3ae1971d38b5d1e66c60219b8d5937d39f37fb Mon Sep 17 00:00:00 2001 From: silentrald Date: Thu, 16 Jan 2025 00:24:59 +0800 Subject: [PATCH 04/29] [bugfix-742] launch bs version directly with steam shortcut --- electron-builder.config.js | 3 +- .../bs-launcher/abstract-launcher.service.ts | 54 ++++++------- .../bs-launcher/bs-launcher.service.ts | 35 +++++--- .../bs-launcher/oculus-launcher.service.ts | 4 +- .../bs-launcher/steam-launcher.service.ts | 4 +- src/main/services/linux.service.ts | 79 ++++++++++++++++--- 6 files changed, 122 insertions(+), 57 deletions(-) diff --git a/electron-builder.config.js b/electron-builder.config.js index 94858c8f..8c76afb9 100644 --- a/electron-builder.config.js +++ b/electron-builder.config.js @@ -65,11 +65,12 @@ const config = { // Audio output "--socket=pulseaudio", // Read/write home directory access - "--filesystem=~/Desktop:rw", // allow writing shortcuts to desktop "--filesystem=~/BSManager:create", // Default BSManager installation folder "--filesystem=~/.steam/steam/steamapps:ro", // for the libraryfolders.vdf "--filesystem=~/.steam/steam/steamapps/common:create", // Steam game folder "--filesystem=~/.steam/steam/steamapps/common/Beat Saber:create", // For installing mods/maps to original Beat Saber version + "--filesystem=~/Desktop", // allow writing shortcuts to desktop + "--filesystem=~/.steam/steam/userdata", // allow writing steam shortcuts // Allow BSManager to create the compat folder if it does not exist "--filesystem=~/.steam/steam/steamapps/compatdata/620980:create", // Allow communication with network diff --git a/src/main/services/bs-launcher/abstract-launcher.service.ts b/src/main/services/bs-launcher/abstract-launcher.service.ts index ba48b163..b61d23b9 100644 --- a/src/main/services/bs-launcher/abstract-launcher.service.ts +++ b/src/main/services/bs-launcher/abstract-launcher.service.ts @@ -9,6 +9,33 @@ import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers"; import { IS_FLATPAK } from "main/constants"; import { LaunchMods } from "shared/models/bs-launch/launch-option.interface"; +export function buildBsLaunchArgs(launchOptions: LaunchOption): string[] { + const launchArgs = []; + + if(!launchOptions.version.steam && !launchOptions.version.oculus){ + launchArgs.push("--no-yeet") + } + if(launchOptions.launchMods?.includes(LaunchMods.OCULUS)) { + launchArgs.push("-vrmode"); + launchArgs.push("oculus"); + } + if(launchOptions.launchMods?.includes(LaunchMods.FPFC)) { + launchArgs.push("fpfc"); + } + if(launchOptions.launchMods?.includes(LaunchMods.DEBUG)) { + launchArgs.push("--verbose"); + } + if(launchOptions.launchMods?.includes(LaunchMods.EDITOR)) { + launchArgs.push("editor"); + } + + if (launchOptions.additionalArgs) { + launchArgs.push(...launchOptions.additionalArgs); + } + + return Array.from(new Set(launchArgs).values()); +} + export abstract class AbstractLauncherService { protected readonly linux = LinuxService.getInstance(); @@ -19,33 +46,6 @@ export abstract class AbstractLauncherService { this.localVersions = BSLocalVersionService.getInstance(); } - protected buildBsLaunchArgs(launchOptions: LaunchOption): string[]{ - const launchArgs = []; - - if(!launchOptions.version.steam && !launchOptions.version.oculus){ - launchArgs.push("--no-yeet") - } - if(launchOptions.launchMods?.includes(LaunchMods.OCULUS)) { - launchArgs.push("-vrmode"); - launchArgs.push("oculus"); - } - if(launchOptions.launchMods?.includes(LaunchMods.FPFC)) { - launchArgs.push("fpfc"); - } - if(launchOptions.launchMods?.includes(LaunchMods.DEBUG)) { - launchArgs.push("--verbose"); - } - if(launchOptions.launchMods?.includes(LaunchMods.EDITOR)) { - launchArgs.push("editor"); - } - - if (launchOptions.additionalArgs) { - launchArgs.push(...launchOptions.additionalArgs); - } - - return Array.from(new Set(launchArgs).values()); - } - protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): ChildProcessWithoutNullStreams { const spawnOptions: SpawnOptionsWithoutStdio = { detached: true, cwd: path.dirname(bsExePath), ...(options || {}) }; diff --git a/src/main/services/bs-launcher/bs-launcher.service.ts b/src/main/services/bs-launcher/bs-launcher.service.ts index ac8cd546..b03dd0ef 100644 --- a/src/main/services/bs-launcher/bs-launcher.service.ts +++ b/src/main/services/bs-launcher/bs-launcher.service.ts @@ -185,7 +185,7 @@ export class BSLauncherService { * @returns {Promise} Path of the icon */ private async createShortcutIco(color: Color): Promise{ - const pngBuffer = await this.createShortcutPngBuffer(color); + const pngBuffer = this.createShortcutPngBuffer(color); const icoBuffer = await toIco([pngBuffer]); await ensureDir(IMAGE_CACHE_PATH); @@ -211,17 +211,28 @@ export class BSLauncherService { if(steamShortcut){ const userId = await tryit(() => this.steam.getActiveUser()); const exePath = app.getPath("exe"); - return this.steam.createShortcut({ - AppName: shortcutName, - Exe: exePath, - StartDir: path.dirname(exePath), - LaunchOptions: this.createLaunchLink(launchOptions), - icon: await this.createShortcutPng(shortcutIconColor), - OpenVR: "\u0001" - }, userId.result).then(() => true).catch(e => { - log.error(e); - return false; - }); + const icon = await this.createShortcutPng(shortcutIconColor) + return this.steam.createShortcut( + process.platform === "win32" + ? { + AppName: shortcutName, + Exe: exePath, + StartDir: path.dirname(exePath), + LaunchOptions: this.createLaunchLink(launchOptions), + OpenVR: "\u0001", icon, + } + : await this.linux.getSteamShortcutData( + shortcutName, icon, launchOptions, + await this.steam.getSteamPath(), + await this.localVersionService.getVersionPath(launchOptions.version) + ), + userId.result + ) + .then(() => true) + .catch(e => { + log.error(e); + return false; + }); } return execOnOs({ diff --git a/src/main/services/bs-launcher/oculus-launcher.service.ts b/src/main/services/bs-launcher/oculus-launcher.service.ts index d60ffe3a..d92ccef9 100644 --- a/src/main/services/bs-launcher/oculus-launcher.service.ts +++ b/src/main/services/bs-launcher/oculus-launcher.service.ts @@ -6,7 +6,7 @@ import { BS_EXECUTABLE } from "../../constants"; import path from "path"; import log from "electron-log"; import { pathExists } from "fs-extra"; -import { AbstractLauncherService } from "./abstract-launcher.service"; +import { AbstractLauncherService, buildBsLaunchArgs } from "./abstract-launcher.service"; import { isProcessRunning } from "../../helpers/os.helpers"; import { CustomError } from "../../../shared/models/exceptions/custom-error.class"; import { UtilsService } from "../utils.service"; @@ -57,7 +57,7 @@ export class OculusLauncherService extends AbstractLauncherService implements St obs.next({type: BSLaunchEvent.BS_LAUNCHING}); // Launch Beat Saber - const process = this.launchBs(exePath, this.buildBsLaunchArgs(launchOptions)); + const process = this.launchBs(exePath, buildBsLaunchArgs(launchOptions)); return process.exit.catch(err => { throw CustomError.fromError(err, BSLaunchError.BS_EXIT_ERROR); diff --git a/src/main/services/bs-launcher/steam-launcher.service.ts b/src/main/services/bs-launcher/steam-launcher.service.ts index 96eb3822..ab8277d6 100644 --- a/src/main/services/bs-launcher/steam-launcher.service.ts +++ b/src/main/services/bs-launcher/steam-launcher.service.ts @@ -6,7 +6,7 @@ import { SteamService } from "../steam.service"; import path from "path"; import { BS_APP_ID, BS_EXECUTABLE, STEAMVR_APP_ID } from "../../constants"; import log from "electron-log"; -import { AbstractLauncherService } from "./abstract-launcher.service"; +import { AbstractLauncherService, buildBsLaunchArgs } from "./abstract-launcher.service"; import { CustomError } from "../../../shared/models/exceptions/custom-error.class"; import { UtilsService } from "../utils.service"; import { exec } from "child_process"; @@ -102,7 +102,7 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto await this.restoreSteamVR().catch(log.error); } - const launchArgs = this.buildBsLaunchArgs(launchOptions); + const launchArgs = buildBsLaunchArgs(launchOptions); const steamPath = await this.steam.getSteamPath(); const env = { diff --git a/src/main/services/linux.service.ts b/src/main/services/linux.service.ts index b3ef82af..b3ead173 100644 --- a/src/main/services/linux.service.ts +++ b/src/main/services/linux.service.ts @@ -8,6 +8,8 @@ import { CustomError } from "shared/models/exceptions/custom-error.class"; import { BSLaunchError, LaunchOption } from "shared/models/bs-launch"; import { BsmShellLog, bsmExec } from "main/helpers/os.helpers"; import { LaunchMods } from "shared/models/bs-launch/launch-option.interface"; +import { SteamShortcutData } from "shared/models/steam/shortcut.model"; +import { buildBsLaunchArgs } from "./bs-launcher/abstract-launcher.service"; export class LinuxService { private static instance: LinuxService; @@ -49,6 +51,16 @@ export class LinuxService { launchOptions.admin = false; } + const protonPath = await this.getProtonPath(); + return { + protonPrefix: await this.isNixOS() + ? `steam-run "${protonPath}" run` + : `"${protonPath}" run`, + env: await this.buildEnvVariables(launchOptions, steamPath, bsFolderPath) + }; + } + + private async getProtonPath(): Promise { if (!this.staticConfig.has("proton-folder")) { throw CustomError.fromError( new Error("Proton folder not set"), @@ -66,15 +78,10 @@ export class LinuxService { ); } - return { - protonPrefix: await this.isNixOS() - ? `steam-run "${protonPath}" run` - : `"${protonPath}" run`, - env: await this.prepareEnvVariables(launchOptions, steamPath, bsFolderPath) - }; + return protonPath; } - private async prepareEnvVariables( + private async buildEnvVariables( launchOptions: LaunchOption, steamPath: string, bsFolderPath: string @@ -167,6 +174,20 @@ export class LinuxService { // === Shortcuts === // + private getCommand( + protonPrefix: string, + bsFolderPath: string, + env: Record, + launchOptions: LaunchOption + ): string { + const envString = Object.entries(env) + .map(([ key, value ]) => `${key}="${value}"`) + .join(" "); + const bsExe = path.join(bsFolderPath, BS_EXECUTABLE); + const args = buildBsLaunchArgs(launchOptions).join(" "); + return `${envString} ${protonPrefix} "${bsExe}" ${args}`; + } + public async createDesktopShortcut( shortcutPath: string, name: string, @@ -186,12 +207,10 @@ export class LinuxService { "SteamGameId": BS_APP_ID, }); - const envString = Object.entries(env) - .map(([ key, value ]) => `${key}="${value}"`) - .join(" "); - const command = `${envString} ${protonPrefix} "${ - path.join(bsFolderPath, BS_EXECUTABLE) - }"`; + const command = this.getCommand( + protonPrefix, bsFolderPath, + env, launchOptions + ); const desktopEntry = [ "[Desktop Entry]", @@ -210,4 +229,38 @@ export class LinuxService { return false; } } + + public async getSteamShortcutData( + shortcutName: string, + icon: string, + launchOptions: LaunchOption, + steamPath: string, + bsFolderPath: string + ): Promise { + const env = await this.buildEnvVariables( + launchOptions, steamPath, bsFolderPath + ); + Object.assign(env, { + "SteamAppId": BS_APP_ID, + "SteamOverlayGameId": BS_APP_ID, + "SteamGameId": BS_APP_ID, + }); + + const protonPrefix = await this.isNixOS() + ? "steam-run %command% run" + : "%command% run"; + + return { + AppName: shortcutName, + Exe: await this.getProtonPath(), + StartDir: bsFolderPath, + icon, + OpenVR: "\x01", + LaunchOptions: this.getCommand( + protonPrefix, bsFolderPath, + env, launchOptions + ) + }; + } + } From 0c3a5b9e75cd5f79978155844ef72de758c9a0f3 Mon Sep 17 00:00:00 2001 From: silentrald Date: Thu, 16 Jan 2025 23:42:10 +0800 Subject: [PATCH 05/29] [bugfix] use static config for bs-version writing/reading for linux --- build/after-install.sh | 4 ---- docs/wiki/Home.md | 1 - .../[Linux]-Permission-Denied-on-bs-version.json.md | 13 ------------- docs/wiki/_Sidebar.md | 1 - src/main/services/bs-version-lib.service.ts | 13 ++----------- 5 files changed, 2 insertions(+), 30 deletions(-) delete mode 100644 docs/wiki/Troubleshoots/Installation-Problems/Linux/[Linux]-Permission-Denied-on-bs-version.json/[Linux]-Permission-Denied-on-bs-version.json.md diff --git a/build/after-install.sh b/build/after-install.sh index a9e1aedd..f2ec23df 100644 --- a/build/after-install.sh +++ b/build/after-install.sh @@ -1,6 +1,2 @@ -# Add write permissions to anybody so that this can be sync with the -# github's bs-versions.json when starting bsmanager -/usr/bin/chmod +002 /opt/BSManager/resources/assets/jsons/bs-versions.json - # https://github.com/electron/electron/issues/42510 /usr/bin/chmod 4755 /opt/BSManager/chrome-sandbox diff --git a/docs/wiki/Home.md b/docs/wiki/Home.md index a0fbf9a7..737d8fce 100644 --- a/docs/wiki/Home.md +++ b/docs/wiki/Home.md @@ -49,7 +49,6 @@ Guidance on fixing errors during setup or version updates - __🐧Linux__: - [Missing Icons in Game]([Linux]-Missing-Icons-in-Game) - - [Permission Denied on "bs-versions.json"]([Linux]-Permission-Denied-on-bs-version.json) - [[Deb] The SUID Sandbox Helper Binary Was Found]([Linux]-[deb]-The-SUID-Sandbox-Helper-Binary-Was-Found) - [[Flatpak] Steam Beat Saber Version Not Showing / Proton Not Detected]([Linux]-[Flatpak]-Steam-Beat-Saber-Version-Not-Showing-Proton-Not-Detected) - [[Flatpak] Changing Installation Folder]([Linux]-[Flatpak]-Changing-Installation-Folder) diff --git a/docs/wiki/Troubleshoots/Installation-Problems/Linux/[Linux]-Permission-Denied-on-bs-version.json/[Linux]-Permission-Denied-on-bs-version.json.md b/docs/wiki/Troubleshoots/Installation-Problems/Linux/[Linux]-Permission-Denied-on-bs-version.json/[Linux]-Permission-Denied-on-bs-version.json.md deleted file mode 100644 index 81e9af5d..00000000 --- a/docs/wiki/Troubleshoots/Installation-Problems/Linux/[Linux]-Permission-Denied-on-bs-version.json/[Linux]-Permission-Denied-on-bs-version.json.md +++ /dev/null @@ -1,13 +0,0 @@ -
-Unhandled Exception UnhandledRejection Error: EACCES: permission denied, open '/opt/BSManager/resources/assets/jsons/bs-versions.json'
-
- -To fix this issue, the current user must have write permissions to the "bs-versions.json". To correct the permissions do command below: - -```bash -chmod +002 /opt/BSManager/resources/assets/jsons/bs-versions.json - -# or - -chown $(whoami) /opt/BSManager/resources/assets/jsons/bs-versions.json -``` diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index d114fbc6..1c5eddab 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -71,7 +71,6 @@ 🐧 Linux
  • Missing Icons in Game
  • -
  • Permission Denied on "bs-versions.json"
  • [Deb] The SUID Sandbox Helper Binary Was Found
  • [Flatpak] Steam Beat Saber Version Not Showing / Proton Not Detected
  • [Flatpak] Changing Installation Folder
  • diff --git a/src/main/services/bs-version-lib.service.ts b/src/main/services/bs-version-lib.service.ts index 70c16e9c..b90582e1 100644 --- a/src/main/services/bs-version-lib.service.ts +++ b/src/main/services/bs-version-lib.service.ts @@ -5,8 +5,6 @@ import { BSVersion } from "shared/bs-version.interface"; import { RequestService } from "./request.service"; import { readJSON } from "fs-extra"; import { allSettled } from "../../shared/helpers/promise.helpers"; -import { LinuxService } from "./linux.service"; -import { IS_FLATPAK } from "main/constants"; import { StaticConfigurationService } from "./static-configuration.service"; export class BSVersionLibService { @@ -15,7 +13,6 @@ export class BSVersionLibService { private static instance: BSVersionLibService; - private readonly linuxService: LinuxService; private readonly utilsService: UtilsService; private readonly requestService: RequestService; private readonly configService: StaticConfigurationService; @@ -23,7 +20,6 @@ export class BSVersionLibService { private bsVersions: BSVersion[]; private constructor() { - this.linuxService = LinuxService.getInstance(); this.utilsService = UtilsService.getInstance(); this.requestService = RequestService.getInstance(); this.configService = StaticConfigurationService.getInstance(); @@ -40,15 +36,10 @@ export class BSVersionLibService { return this.requestService.getJSON(this.REMOTE_BS_VERSIONS_URL).then(res => res.data); } - private async shouldLoadFromConfig(): Promise { - // Some special cases of readonly memory installations - return process.platform === "linux" && (IS_FLATPAK || this.linuxService.isNixOS()); - } - private async getLocalVersions(): Promise { const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE); - if (!(await this.shouldLoadFromConfig())) { + if (process.platform !== "linux") { return readJSON(localVersionsPath); } @@ -60,7 +51,7 @@ export class BSVersionLibService { } private async updateLocalVersions(versions: BSVersion[]): Promise { - if (await this.shouldLoadFromConfig()) { + if (process.platform === "linux") { this.configService.set("versions", versions); return; } From 773b54ca0866aa63b1352f250559eebada766fea Mon Sep 17 00:00:00 2001 From: silentrald Date: Fri, 17 Jan 2025 07:35:12 +0800 Subject: [PATCH 06/29] [bugfix] recursively create the compatdata folder --- src/main/services/linux.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/services/linux.service.ts b/src/main/services/linux.service.ts index b3ead173..5f12896a 100644 --- a/src/main/services/linux.service.ts +++ b/src/main/services/linux.service.ts @@ -93,7 +93,7 @@ export class LinuxService { const compatDataPath = this.getCompatDataPath(); if (!fs.existsSync(compatDataPath)) { log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`); - fs.mkdirSync(compatDataPath); + await fs.ensureDir(compatDataPath); } // Setup Proton environment variables From effe0608e1e03ddc1dcee723df0713f423bf744d Mon Sep 17 00:00:00 2001 From: silentrald Date: Sat, 18 Jan 2025 22:25:02 +0800 Subject: [PATCH 07/29] [bugfix] fix light theming pin icon color --- .../slides/launch/launch-options-panel.component.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/renderer/components/version-viewer/slides/launch/launch-options-panel.component.tsx b/src/renderer/components/version-viewer/slides/launch/launch-options-panel.component.tsx index bfea54ca..7ef2b418 100644 --- a/src/renderer/components/version-viewer/slides/launch/launch-options-panel.component.tsx +++ b/src/renderer/components/version-viewer/slides/launch/launch-options-panel.component.tsx @@ -60,7 +60,9 @@ export function LaunchModItem({ id, icon: Icon, label, description, active, visi {onPinChange && ( )} From b284b336697bebef34e907d9687cb7ac490846d7 Mon Sep 17 00:00:00 2001 From: silentrald Date: Sat, 18 Jan 2025 21:43:19 +0800 Subject: [PATCH 08/29] [chore] added rpm build for linux --- .github/workflows/build.yaml | 11 ++++++----- .github/workflows/release-linux.yaml | 7 +++++-- electron-builder.config.js | 1 + 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index fe246cc2..8a7bdc1a 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -24,14 +24,15 @@ jobs: with: node-version: 22.11.0 cache: "npm" + + # Install and setup rpm/Flatpak for Ubuntu + - name: Install rpm/flatpak packages (Ubuntu only) + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get install -y flatpak flatpak-builder rpm libarchive-tools + - run: npm ci - run: npm run package - # Install and setup Flatpak for Ubuntu - - name: Install flatpak packages (Ubuntu only) - if: matrix.os == 'ubuntu-latest' - run: sudo apt-get install -y flatpak flatpak-builder - - name: Setup flatpak repo (Ubuntu only) if: matrix.os == 'ubuntu-latest' run: | diff --git a/.github/workflows/release-linux.yaml b/.github/workflows/release-linux.yaml index 0996f1ae..7e361647 100644 --- a/.github/workflows/release-linux.yaml +++ b/.github/workflows/release-linux.yaml @@ -20,8 +20,8 @@ jobs: - name: Check out Git repository uses: actions/checkout@v4 - - name: Install flatpak packages - run: sudo apt-get install -y flatpak flatpak-builder + - name: Install rpm/flatpak packages + run: sudo apt-get install -y flatpak flatpak-builder rpm libarchive-tools - name: Setup flatpak repo run: | @@ -39,6 +39,9 @@ jobs: - name: Build deb run: npx electron-builder --config electron-builder.config.js --publish always --linux deb --x64 + - name: Build rpm + run: npx electron-builder --config electron-builder.config.js --publish always --linux rpm --x64 + - name: Build flatpak run: env DEBUG="@malept/flatpak-bundler" npx electron-builder --config electron-builder.config.js --publish always --linux flatpak diff --git a/electron-builder.config.js b/electron-builder.config.js index 8c76afb9..26c433a6 100644 --- a/electron-builder.config.js +++ b/electron-builder.config.js @@ -29,6 +29,7 @@ const config = { linux: { target: [ "deb", + "rpm", ], icon: "./build/icons/png", category: "Utility;Game;", From 8020084e77635aa7bcf27655853cf538f8da853b Mon Sep 17 00:00:00 2001 From: silentrald Date: Mon, 20 Jan 2025 09:00:44 +0800 Subject: [PATCH 09/29] [chore] updated contributor title to add rpm maintainer --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6b47a043..e887788b 100644 --- a/README.md +++ b/README.md @@ -559,7 +559,7 @@
  • GaetanGrd - Co-Developer & Co-Founder, Documentation Lead.
  • cheddZy - Icon Creator.
  • Insprill - Co-Developer, Linux Developer, AUR Maintainer.
  • -
  • silentrald - Co-Developer, Linux Developer, deb and flatpak Maintainer.
  • +
  • silentrald - Co-Developer, Linux Developer, deb/rpm/flatpak Maintainer.
From 13984f886fba0c68878d8c1d69d40e395f1a9916 Mon Sep 17 00:00:00 2001 From: silentrald Date: Tue, 21 Jan 2025 11:53:48 +0800 Subject: [PATCH 10/29] [feat] change how file log names are created --- src/main/main.ts | 78 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 53 insertions(+), 25 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index 371ca6bc..11f22581 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -23,7 +23,7 @@ import { LivShortcut } from "./services/liv/liv-shortcut.service"; import { SteamLauncherService } from "./services/bs-launcher/steam-launcher.service"; import { FileAssociationService } from "./services/file-association.service"; import { SongDetailsCacheService } from "./services/additional-content/maps/song-details-cache.service"; -import { readdirSync, statSync, unlinkSync } from "fs-extra"; +import { Dirent, readdirSync, rmSync, statSync, unlinkSync } from "fs-extra"; import { StaticConfigurationService } from "./services/static-configuration.service"; import { configureProxy } from './helpers/proxy.helpers'; @@ -37,7 +37,7 @@ export const filterPatterns = new Set(); filterPatterns.add(/(FRL|OC)\S{10,}/g); initLogger(); -deleteOlestLogs(); +deleteOldestLogs(); deleteOldLogs(); staticConfig.take("disable-hadware-acceleration", disabled => { @@ -156,11 +156,30 @@ if (!gotTheLock) { }).catch(log.error); } +function convertDateToDateString(date: Date): string { + const month = (date.getMonth() + 1).toString().padStart(2, "0"); + const day = date.getDate().toString().padStart(2, "0"); + return `${date.getFullYear()}-${month}-${day}`; +} + function initLogger(){ log.transports.file.level = "info"; + + let filepath = ""; + let currentDateString = convertDateToDateString(new Date()); log.transports.file.resolvePath = () => { const now = new Date(); - return path.join(app.getPath("logs"), `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}-v${app.getVersion()}.log`); + const nowString = convertDateToDateString(now); + if (filepath && nowString === currentDateString) { + return filepath; + } + + filepath = path.join( + app.getPath("logs"), nowString, + `${now.getTime()}-v${app.getVersion()}.log` + ); + currentDateString = nowString; + return filepath; }; log.hooks.push((message) => { @@ -206,18 +225,19 @@ function initLogger(){ function getLogFilesEntries() { try { const logsFolder = app.getPath("logs"); - let logs = readdirSync(logsFolder, { withFileTypes: true }); - + let logs = readdirSync(logsFolder, { + withFileTypes: true, + recursive: true + }); logs = logs.filter(file => file.isFile() && path.extname(file.name) === ".log"); logs.sort((a, b) => { - const aStat = statSync(path.join(logsFolder, a.name)); - const bStat = statSync(path.join(logsFolder, b.name)); - return bStat.mtime.getTime() - aStat.mtime.getTime(); + return path.basename(b.parentPath).localeCompare(path.basename(a.parentPath)) + || b.name.localeCompare(a.name); }); return logs.map(file => { - const filePath = path.join(logsFolder, file.name); + const filePath = path.join(file.parentPath, file.name); const stat = statSync(filePath); return { path: filePath, @@ -231,28 +251,36 @@ function getLogFilesEntries() { } } -// keep only the last 5 logs -function deleteOldLogs(): void{ +// Keep only the past week (7 days) of logs +function deleteOldLogs(): void { + const deleteLogFolders: Dirent[] = []; try { - let logs = getLogFilesEntries(); + const filterDate = convertDateToDateString( + new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // 7 days + ); + deleteLogFolders.push.apply([], + readdirSync(app.getPath("logs"), { withFileTypes: true }) + .filter(folder => folder.isDirectory() && folder.name <= filterDate) + ); + } catch (error) { + log.error("Error while deleting old logs:", error); + return; + } - logs = logs.slice(5); - - logs.forEach(file => { - try { - unlinkSync(file.path); - log.info(`Deleted log file: ${file.path}`); - } catch (err) { - log.error(`Error deleting file ${file.path}:`, err); - } - }); - } catch (err) { - log.error("Error while deleting old logs:", err); + for (const folder of deleteLogFolders) { + const folderPath = path.join(folder.parentPath, folder.name); + try { + rmSync(folderPath, { recursive: true, force: true }); + log.info("Deleted log folder:", folderPath); + } catch (error) { + log.error("Error deleting folder", folderPath, error); + } } } +// NOTE: Change this date to when the PR is merged // Temporary function to delete logs before 2024-07-31 -function deleteOlestLogs(): void{ +function deleteOldestLogs(): void { // delete all logs before 2024-07-31 const date = new Date(2024, 6, 31); // month is 0-based const logs = getLogFilesEntries().filter(file => file.stats.mtime.getTime() < date.getTime()); From ec0ae76d707e3ea07bfb7633045094ebfe112877 Mon Sep 17 00:00:00 2001 From: silentrald Date: Tue, 21 Jan 2025 12:19:05 +0800 Subject: [PATCH 11/29] [feat] delete obsolete log files from previous handling --- src/main/main.ts | 55 ++++++++++++------------------------------------ 1 file changed, 13 insertions(+), 42 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index 11f22581..e1c57273 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -23,7 +23,7 @@ import { LivShortcut } from "./services/liv/liv-shortcut.service"; import { SteamLauncherService } from "./services/bs-launcher/steam-launcher.service"; import { FileAssociationService } from "./services/file-association.service"; import { SongDetailsCacheService } from "./services/additional-content/maps/song-details-cache.service"; -import { Dirent, readdirSync, rmSync, statSync, unlinkSync } from "fs-extra"; +import { Dirent, readdirSync, rmSync, unlinkSync } from "fs-extra"; import { StaticConfigurationService } from "./services/static-configuration.service"; import { configureProxy } from './helpers/proxy.helpers'; @@ -222,35 +222,6 @@ function initLogger(){ log.catchErrors(); } -function getLogFilesEntries() { - try { - const logsFolder = app.getPath("logs"); - let logs = readdirSync(logsFolder, { - withFileTypes: true, - recursive: true - }); - logs = logs.filter(file => file.isFile() && path.extname(file.name) === ".log"); - - logs.sort((a, b) => { - return path.basename(b.parentPath).localeCompare(path.basename(a.parentPath)) - || b.name.localeCompare(a.name); - }); - - return logs.map(file => { - const filePath = path.join(file.parentPath, file.name); - const stat = statSync(filePath); - return { - path: filePath, - name: file.name, - stats: stat - }; - }); - } catch (err) { - log.error('Error while retrieving log files entries:', err); - return []; - } -} - // Keep only the past week (7 days) of logs function deleteOldLogs(): void { const deleteLogFolders: Dirent[] = []; @@ -273,26 +244,26 @@ function deleteOldLogs(): void { rmSync(folderPath, { recursive: true, force: true }); log.info("Deleted log folder:", folderPath); } catch (error) { - log.error("Error deleting folder", folderPath, error); + log.error("Error deleting folder:", folderPath, error); } } } -// NOTE: Change this date to when the PR is merged -// Temporary function to delete logs before 2024-07-31 +// Obsolete behavior, delete log files that are on the parent log folder function deleteOldestLogs(): void { - // delete all logs before 2024-07-31 - const date = new Date(2024, 6, 31); // month is 0-based - const logs = getLogFilesEntries().filter(file => file.stats.mtime.getTime() < date.getTime()); + const logsFolder = app.getPath("logs"); + const logs = readdirSync(logsFolder, { withFileTypes: true }) + .filter(file => file.isFile() && path.extname(file.name) === ".log"); - logs.forEach(file => { + for (const file of logs) { + const filepath = path.join(file.parentPath, file.name); try { - unlinkSync(file.path); - log.info(`Deleted log file: ${file.path}`); - } catch (err) { - log.error(`Error deleting file ${file.path}:`, err); + unlinkSync(filepath); + log.info("Deleted log file:", filepath); + } catch (error) { + log.error("Error deleting file:", filepath, error); } - }); + } } export function addFilterStringLog(filter: string): void { From 5f6cf77f53325f16e7c6eb82c47e796ee3a2a8e7 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Tue, 21 Jan 2025 20:51:24 +0100 Subject: [PATCH 12/29] Add patreon --- assets/jsons/patreons.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/jsons/patreons.json b/assets/jsons/patreons.json index 33bd4a11..9bd6acc1 100644 --- a/assets/jsons/patreons.json +++ b/assets/jsons/patreons.json @@ -138,5 +138,9 @@ { "username": "Alexander Herman", "type": "gold" + }, + { + "username": "Arts Rimuro Suraimu", + "type": "gold" } ] From 53c9f923446f078ccbccd45233e9cc23df7ce5cb Mon Sep 17 00:00:00 2001 From: arthurmluz Date: Tue, 21 Jan 2025 16:25:19 -0300 Subject: [PATCH 13/29] feat: adds brazilian portuguese language --- assets/jsons/translations/de.json | 2 + assets/jsons/translations/en.json | 2 + assets/jsons/translations/es.json | 2 + assets/jsons/translations/fr.json | 2 + assets/jsons/translations/it.json | 2 + assets/jsons/translations/ja.json | 2 + assets/jsons/translations/ko.json | 2 + assets/jsons/translations/pt-br.json | 1308 +++++++++++++++++ assets/jsons/translations/ru.json | 2 + assets/jsons/translations/zh-tw.json | 2 + assets/jsons/translations/zh.json | 1 + .../components/svgs/bsm-icon.component.tsx | 7 +- .../svgs/flags/brasil-icon.component.tsx | 11 + .../config/default-configuration.config.ts | 2 +- 14 files changed, 1345 insertions(+), 2 deletions(-) create mode 100644 assets/jsons/translations/pt-br.json create mode 100644 src/renderer/components/svgs/flags/brasil-icon.component.tsx diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index c7e7a533..0a2f49c3 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -227,6 +227,7 @@ "es-ES": "Español", "it-IT": "Italiano", "de-DE": "Deutsch", + "pt-BR": "Português (Brasil)", "ru-RU": "Русский", "zh-CN": "简体中文", "zh-TW": "正體中文", @@ -237,6 +238,7 @@ "en-US": "Englisch, USA", "fr-FR": "Französisch", "es-ES": "Spanisch", + "pt-BR": "Portugiesisch", "it-IT": "Italienisch", "de-DE": "Deutsch", "ru-RU": "Russisch", diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 8b769c7d..3ab022a9 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -227,6 +227,7 @@ "es-ES": "Español", "it-IT": "Italiano", "de-DE": "Deutsch", + "pt-BR": "Português (Brasil)", "ru-RU": "Русский", "zh-CN": "简体中文", "zh-TW": "正體中文", @@ -239,6 +240,7 @@ "es-ES": "Spanish", "it-IT": "Italian", "de-DE": "German", + "pt-BR": "Portuguese (Brazil)", "ru-RU": "Russian", "zh-CN": "Chinese (Simplified)", "zh-TW": "Chinese (Traditional)", diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index 61cba828..5b235990 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -227,6 +227,7 @@ "es-ES": "Español", "it-IT": "Italiano", "de-DE": "Deutsch", + "pt-BR": "Português (Brasil)", "ru-RU": "Русский", "zh-CN": "简体中文", "zh-TW": "正體中文", @@ -239,6 +240,7 @@ "es-ES": "Español", "it-IT": "Italiano", "de-DE": "Alemán", + "pt-BR": "Portugués (Brasil)", "ru-RU": "Ruso", "zh-CN": "Chino simplificado", "zh-TW": "Chino tradicional", diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index 09ff7267..6c15f34f 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -227,6 +227,7 @@ "es-ES": "Español", "it-IT": "Italiano", "de-DE": "Deutsch", + "pt-BR": "Português (Brasil)", "ru-RU": "Русский", "zh-CN": "简体中文", "zh-TW": "正體中文", @@ -237,6 +238,7 @@ "en-US": "Anglais, États-Unis", "fr-FR": "Français", "es-ES": "Espagnol", + "pt-BR": "Portugais (Brésil)", "it-IT": "Italien", "de-DE": "Allemand", "ru-RU": "Russe", diff --git a/assets/jsons/translations/it.json b/assets/jsons/translations/it.json index 6be54279..bb3bc41f 100644 --- a/assets/jsons/translations/it.json +++ b/assets/jsons/translations/it.json @@ -227,6 +227,7 @@ "es-ES": "Español", "it-IT": "Italiano", "de-DE": "Deutsch", + "pt-BR": "Português (Brasil)", "ru-RU": "Русский", "zh-CN": "简体中文", "zh-TW": "正體中文", @@ -239,6 +240,7 @@ "es-ES": "Spagnolo", "it-IT": "Italiano", "de-DE": "Tedesco", + "pt-BR": "Portoghese (Brasile)", "ru-RU": "Russo", "zh-CN": "Cinese (Semplificato)", "zh-TW": "Cinese (Tradizionale)", diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index 356fb81e..90323d5f 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -227,6 +227,7 @@ "es-ES": "Español", "it-IT": "Italiano", "de-DE": "Deutsch", + "pt-BR": "Português (Brasil)", "ru-RU": "Русский", "zh-CN": "简体中文", "zh-TW": "正體中文", @@ -239,6 +240,7 @@ "es-ES": "スペイン語", "it-IT": "イタリア語", "de-DE": "ドイツ語", + "pt-BR": "ポルトガル語 (ブラジル)", "ru-RU": "ロシア語", "zh-CN": "簡体字中国", "zh-TW": "繁体字中国", diff --git a/assets/jsons/translations/ko.json b/assets/jsons/translations/ko.json index d95476be..8f96b3d4 100644 --- a/assets/jsons/translations/ko.json +++ b/assets/jsons/translations/ko.json @@ -227,6 +227,7 @@ "es-ES": "Español", "it-IT": "Italiano", "de-DE": "Deutsch", + "pt-BR": "Português (Brasil)", "ru-RU": "Русский", "zh-CN": "简体中文", "zh-TW": "正體中文", @@ -239,6 +240,7 @@ "es-ES": "스페인어", "it-IT": "이탈리아어", "de-DE": "독일어", + "pt-BR": "포르투갈 인 (브라질)", "ru-RU": "러시아어", "zh-CN": "중국어 (간체)", "zh-TW": "중국어 (번체)", diff --git a/assets/jsons/translations/pt-br.json b/assets/jsons/translations/pt-br.json new file mode 100644 index 00000000..b8e783be --- /dev/null +++ b/assets/jsons/translations/pt-br.json @@ -0,0 +1,1308 @@ +{ + "misc": { + "download": "Baixar", + "add": "Adicionar", + "verify": "Verificar", + "launch": "Iniciar", + "mods": "Mods", + "maps": "Mapas", + "playlists": "Playlists", + "models": "Modelos", + "avatars": "Avatares", + "sabers": "Sabres", + "platforms": "Platformas", + "blocks": "Blocos", + "cancel": "Cancelar", + "delete": "Deletar", + "accept": "Aceitar", + "refuse": "Recusar", + "apply": "Aplicar", + "copy": "Copiar", + "copied": "Copiado!", + "confirm": "Confirmar", + "choose-folder": "Escolher pasta", + "unknown": "Um erro desconhecido aconteceu ¯\\_(ツ)_/¯", + "warning": "Aviso", + "continue": "Continuar", + "shared": "Compartilhados", + "pin": "Fixar", + "unpin": "Desafixar" + }, + "title-bar": { + "outdated": "Desatualizado" + }, + "nav-bar": { + "add-version": "Adicionar uma versão", + "settings": "Configurações", + "shared": { + "text": "Compartilhados", + "tooltip": "Conteúdos compartilhados" + } + }, + "pages": { + "version-viewer": { + "launch-mods": { + "oculus": "Modo Oculus", + "oculus-description": "Se você está utilizando Beat Saber pela Steam, isso permite que você use o compositor do Oculus VR sem passar pelo SteamVR, para possíveis ganhos de performance. Isso não é necessário para utilizar o Oculus.", + "desktop": "Modo FPFC", + "desktop-description": "O modo Controle Voador em Primeira Pessoa permite utilizar WASD e o mouse para navegar pelo menu do jogo. Isso facilita testes, pois não é necessário colocar o Oculus!", + "debug": "Modo Debug", + "debug-description": "Habilita a janela com logs de saída para IPA. Isso vai mostrar o console de debug que os mods usam.", + "outdated-tippy": "Essa versão está desatualizada, e alguns mods ou funções podem não funcionar mais como esperado. Prefira utilizar a versão recomendada ({recommendedVersion}) de Beat Saber para aproveitar todas as últimas novidades e correções de erros.", + "advanced-launch": { + "button": "Opções de Inicialização", + "placeholder": "Argumentos adicionais ex: --revert; --nowait" + }, + "skipsteam": "Pular Steam", + "skipsteam-description": "Faz com que a Steam pare de abrir automaticamente com Beat Saber, ative se você está utilizando algum outro executável de VR como WiVRn ou Monado, que pode interferir com o SteamVR.", + "map-editor": "Editor de Mapas", + "map-editor-description": "Iniciar o editor oficial de mapas do Beat Saber ao invés do jogo.", + "proton-logs": "Logs do Proton", + "proton-logs-description": "Habilita a gravação de logs do proton para essa instalação do Beat Saber Saber em \"{versionPath}\"." + }, + "maps": { + "search-bar": { + "search-placeholder": "Procurar um mapa", + "filters-btn": "Filtros", + "dropdown": { + "export-maps": "Exportar mapas", + "delete-maps": "Deletar mapas", + "delete-duplicate-maps": "Deletar duplicatas" + } + }, + "tabs": { + "maps": { + "actions": { + "drop-down": { + "browse-maps": "Explorar mapas", + "import-maps": "Importar mapas" + }, + "link-maps": { + "tooltips": { + "link": "Vincular mapas", + "unlink": "Desvincular mapas" + } + } + }, + "empty-maps": { + "text": "Sem mapas", + "button": "Baixar mapas" + }, + "drop-zone": { + "text": "Importar seus mapas", + "subtext": "Jogue seus arquivos zip aqui para importar seus mapas" + }, + "sort": { + "name": "Nome", + "song-author": "Autor da Música", + "map-author": "Autor do Mapa", + "bpm": "BPM", + "duration": "Duração", + "likes": "Gostei", + "date-uploaded": "Data de envio" + } + }, + "playlists": { + "drop-down": { + "browse-playlists": "Explorar playlists", + "create-a-playlist": "Criar uma playlist", + "import-playlists": "Importar playlists" + }, + "drop-zone": { + "text": "Importar suas playlists", + "subtext": "Jogue seu arquivos \".bplist\" ou \".json\" aqui para importá-los" + }, + "sort": { + "title": "Título", + "author": "Autor", + "number-of-maps": "Quantidade de Mapas", + "duration": "Duração", + "notes-per-second": "NPS" + } + } + } + }, + "mods": { + "loading-mods": "Carregando mods...", + "no-internet": "Sem internet", + "mods-not-available": "Nenhum mod está disponível para essa versão de Beat Saber ainda", + "buttons": { + "more-infos": "Mais informações", + "install-or-update": "Instalar ou Atualizar", + "reinstall-all": "Reinstalar todos" + }, + "mods-grid": { + "header-bar": { + "name": "Nome", + "installed": "Instalado", + "latest": "Mais recente", + "description": "Descrição", + "dropdown": { + "import-mods": "Importar mods", + "desinstalar-all": "Desinstalar todos", + "unselect-all": "Desmarcar todos" + } + } + }, + "notifications": { + "all-mods-already-installed": { + "title": "Mods já instalados", + "description": "Todos os mods selecionados já estão instalados" + } + }, + "drop-zone": { + "text": "Importar seus mods", + "subtext": "Jogue seus arquivos \"zip\" ou \"dll\" aqui para importá-los" + } + }, + "dropdown": { + "open-folder": "Abrir pasta", + "verify-files": "Verificar arquivos", + "clone": "Clonar", + "edit": "Editar", + "desinstalar": "Desintalar", + "create-shortcut": "Criar um atalho", + "shared-folders": "Pastas compartilhadas" + } + }, + "available-versions": { + "title": "Baixar uma versão", + "steam-release": "Página do lançamento", + "dropdown": { + "refresh": "Atualizar versões", + "import-version": "Importar uma versão" + }, + "recommended": "Recomendado", + "recommended-tooltip": "Versão mais modificável" + }, + "settings": { + "steam-and-oculus": { + "title": "Steam & Oculus", + "description": "Desconectar irá te permitir trocar de contas no próximo download de Beat Saber.", + "logout": "Deslogar", + "logout-success": "Deslogado com sucesso", + "download-platform": { + "title": "Plataforma padrão", + "desc": "Escolha a plataforma padrão que será utilizada para baixar as versões de Beat Saber.", + "always-ask": "Sempre perguntar" + } + }, + "appearance": { + "title": "Aparências", + "description": "Escolha entre as duas cores principais do BSManager.", + "reset": "Resetar", + "sub-title": "Tema", + "themes": { + "dark": "Escuro", + "light": "Claro", + "os": "Sincronizar com seu computador" + } + }, + "installation-folder": { + "title": "Pasta de instalação", + "description": "Alterar a pasta que contem todos os conteúdos baixados pelo BSManager." + }, + "proton-folder": { + "title": "Pasta do Proton ", + "description": "Alterar a pasta do Proton. (exemplo: Proton - Experimental)", + "errors": { + "title": "Alteração na pasta do Proton falhou", + "invalid-folder": "Caminho inválido para a pasta do Proton" + } + }, + "additional-content": { + "title": "Conteúdo Adicional", + "description": "Conteúdo Adicional que te permite customizar Beat Saber!", + "deep-links": { + "sub-title": "Instalações em apenas um clique" + } + }, + "language": { + "title": "Idiomas", + "description": "Selecione um idioma.", + "languages": { + "en-EN": "English, UK", + "en-US": "English, US", + "fr-FR": "Français", + "es-ES": "Español", + "it-IT": "Italiano", + "de-DE": "Deutsch", + "pt-BR": "Português (Brasil)", + "ru-RU": "Русский", + "zh-CN": "简体中文", + "zh-TW": "正體中文", + "ja-JP": "日本語", + "ko-KR": "한국어", + "translated": { + "en-EN": "Inglês, Reino Unido", + "en-US": "Inglês, Estados Unidos", + "fr-FR": "Francês", + "es-ES": "Espanhol", + "it-IT": "Italiano", + "de-DE": "Alemão", + "pt-BR": "Português (Brasil)", + "ru-RU": "Russo", + "zh-CN": "Chinês (Simplificado)", + "zh-TW": "Chinês (Tradicional)", + "ja-JP": "Japonês", + "ko-KR": "Coreano" + } + } + }, + "patreon": { + "title": "Apoie BSManager 💖", + "description": "Apoie o projeto e nos ajude a continuar trazendo melhorias para o BSManager.", + "buttons": { + "support": "Apoie BSManager 🥰", + "supporters": "Apoiadores 👀" + }, + "view": { + "no-supporters": "Nenhum apoiador no momento", + "sponsors": "Patrocinadores", + "supporters": "Apoiadores" + } + }, + "discord": { + "description": "Junte-se a comunidade do BSManager nos seguindo nas nossas redes sociais!" + }, + "contribution": { + "description": "Sugira uma nova função ou reporte um erro para nos ajudar a melhorar BSManager!", + "buttons": { + "request-features": "Solicitar nova função", + "report-bug": "Reportar um erro", + "open-logs": "Abrir logs" + } + }, + "advanced": { + "title": "Avançado", + "description": "Configurações avançadas para BSManager.", + "hardware-acceleration": { + "title": "Aceleração de Hardware", + "description": "Ativar aceleração de hardware para utilizar a sua GPU e melhorar a performance do BSManager. Desligue isso se você está enfrentando perda de quadros.", + "modal": { + "title": "Reinício necessário", + "body": "Alterar a configuração de aceleração de hardware irá fechar e reabrir BSManager. Você quer continuar com isso?", + "confirm-btn": "Sim, eu quero" + }, + "error-notification": { + "message": "Um erro aconteceu, não foi possível desabilitar a aceleração de hardware." + } + }, + "use-symlinks": { + "title": "Usar SymLinks", + "description": "Use SymLinks ao invés de atalhos para pastas. Só ative isso se você realmente precisar.", + "modal": { + "title": "Permissões de Symlink", + "body": "Quando criando symlinks, BSManager irá solicitar privilégios de administrador ou modo desenvolvedor habilitado no seu sistema. Você quer continuar com isso?", + "confirm-btn": "Sim, eu quero" + }, + "error-notification": { + "message": "Um erro aconteceu, não foi possível alterar a configuração de Symlinks." + } + }, + "use-system-proxy": { + "title": "Usar proxy de sistema", + "description": "BSManager irá enviar chamadas de rede pelo seu proxy de sistema.", + "error-notification": { + "message": "Um erro aconteceu, não foi possível alterar a configuração de proxy de sistema." + } + } + } + } + }, + "notifications": { + "types": { + "error": "🚨 Erro", + "warning": "⚠️ Perigo", + "success": "🎉 Succeso" + }, + "common": { + "msg": { + "error-occurred": "Um erro aconteceu" + } + }, + "shared": { + "errors": { + "titles": { + "operation-running": "Operação em andamento", + "no-internet": "Sem internet", + "file-not-supported": "Arquivo não suportado" + }, + "msg": { + "operation-running": "Espere a operação atual terminar, e então tente novamente.", + "no-internet": "Verifique sua conexão e tente novamente.", + "file-not-supported": "Apenas os arquivos {types} são suportados." + } + } + }, + "bs-download": { + "success": { + "titles": { + "download-success": "Download completo", + "verification-finished": "Verificação completa" + } + }, + "steam-download": { + "warnings": { + "msg": { + "ManifestChecksum": "O manifesto baixado anteriormente não é igual ao novo 🤔", + "ConnectionTimeout": "Sua conexão de internet parece instável 🥶", + "ConnectionLost": "A conexão foi perdida, tente novamente...", + "ConnectionError": "Não foi possível conectar a Steam, tente novamente...", + "Unknown": "Algo estranho aconteceu 🤔 sua conexão provavelmente está instável." + } + }, + "errors": { + "msg": { + "401": "Steam não está nos permitindo baixar Beat Saber 😢", + "404": "Não foi possível conectar com os servidores da Steam.", + "ExeNotFoundWindows": "\"DepotDownloader.exe\" está faltando. Por favor verifique se o executável está em quarentena pelo seu anti-virus.", + "ExeNotFoundLinux": "\"DepotDownloader\" executable está faltando.", + "Password": "Senha inválida.", + "InvalidCredentials": "Credenciais de login inválidas, conexão negada, ou muitas tentativas de login foram feitas.", + "NoManifest": "Nenhum manifesto foi encontrado", + "DirectoryCreate": "Não foi possível criar as pastas necessárias.", + "NotAvailableApp": "Você está tentando baixar BeatSaber sem ter comprado o jogo? 🤣", + "DepotNotFound": "Não foi possivel baixar Beat Saber 😥 tente novamente mais tarde 😕", + "NotCompleted": "Não foi possível completar o download ¯\\_(ツ)_/¯", + "InvalidManifest": "Não foi possível baixar Beat Saber 😥 tente novamente mais tarde 😕", + "NoValidKey": "Não foi possível baixar Beat Saber 😥 tente novamente mais tarde 😕", + "NoManifestCode": "Não foi possível baixar Beat Saber 😥 tente novamente mais tarde 😕", + "Unknown": "Um erro desconhecido aconteceu ¯\\_(ツ)_/¯", + "NoServer": "Não foi possível conectar aos servidores da Steam.", + "NotAllowed": "Aparentemente você não tem permissão para baixar Beat Saber 🥱", + "ConnectionTimeout": "Não foi possível conectar na Steam 😕", + "SteamLib": "Se você tiver esse erro, reporte no GitHub com os logs, por favor.", + "ConnectionError": "Não foi possível conectar na Steam depois de 10 tentativas 🤯", + "LicenceError": "Não foi possível pegar a lista de licenças.", + "RateLimitExceeded": "Você tentou muitas vezes, por favor espere um momento e tente novamente mais tarde", + "TokenRejected": "Seu token de login foi rejeitado 😕 Por favor tente novamente.", + "AccessDenied": "Seu acesso a Steam foi negado." + } + } + }, + "oculus-download": { + "errors": { + "msg": { + "DOWNLOAD_MANIFEST_FAILED": "Não foi possível baixar o manifesto para essa versão; seu token de login pode estár inválido.", + "MANIFEST_FILE_NOT_FOUND": "Não foi possível encontrar o manifesto para essa versão.", + "PARSE_MANIFEST_FILE_FAILED": "Um erro aconteceu enquanto o manifesto estava sendo lido.", + "ALREADY_DOWNLOADING": "Uma versão já está sendo baixada.", + "UNABLE_TO_GET_MANIFEST": "Não foi possível obter o manifesto necessário para baixar.", + "VERIFY_INTEGRITY_FAILED": "Um erro aconteceu durante a verificação de arquivos.", + "SOME_FILES_FAILED_TO_DOWNLOAD": "Alguns arquivos falharam na hora de serem baixados.", + "META_LOGIN_TIMED_OUT": "O token de login demorou tempo de mais para chegar.", + "META_LOGIN_WINDOW_CLOSED_BY_USER": "A tela de login da Meta foi fechada.", + "NO_META_AUTH_TOKEN": "Não foi possível recuperar o token de login da Meta necessário para baixar.", + "UNKNOWN_ERROR": "Um erro desconhecido aconteceu." + } + } + } + }, + "bs-import-version": { + "success": { + "start-import": { + "title": "Importação em progresso 👌", + "desc": "A importação pode levar muitos minutos dependendo das suas configurações." + }, + "imported": { + "title": "Versão importada 🎉" + } + }, + "errors": { + "import-error": { + "desc": "Verifique que a pasta selecionada contém uma instalação de Beat Saber." + } + } + }, + "settings": { + "move-folder": { + "success": { + "titles": { + "transfer-started": "Movendo arquivos", + "transfer-finished": "Mudança completa" + }, + "descs": { + "transfer-started": "A transferência de arquivos começou e pode levar muitos minutos dependendo das suas configurações." + } + }, + "errors": { + "titles": { + "transfer-failed": "A mudança de arquivos falhou 😕" + }, + "descs": { + "COPY_TO_SUBPATH": "A pasta destino não pode ser uma subpasta de uma pasta raiz.", + "restore-linked-folders": "Um erro aconteceu enquanto restaurando as pastas compartilhadas. Você ainda pode manualmente restaurara elas pelo menu 'Pastas Compartilhadas' na página de versões." + } + } + }, + "steam": { + "success": { + "titles": { + "logout": "Desconectado da Steam." + } + } + }, + "additional-content": { + "deep-link": { + "select-all": "Selecionar todos", + "activation": { + "success": { + "title": "UmClique ativado!", + "description": "Instalações em UmClique foram ativadas." + }, + "error": { + "description": "Não foi possível ativar instalações em UmClique." + } + }, + "deactivation": { + "success": { + "title": "UmClique desativado!", + "description": "Instalações em UmClique foram desativadas." + }, + "error": { + "description": "Um erro aconteceu." + } + }, + "check-all-enabled": { + "title": "UmClique desabilitado", + "description": "Uma ou mais instalações UmClique estão desabilitadas. Vá nas configurações para habilitá-las", + "actions": { + "settings": "Configurações", + "not-remind": "Não me lembrar novamente" + } + } + } + } + }, + "bs-launch": { + "success": { + "titles": { + "BS_LAUNCHING": "Iniciando...🚀", + "STEAM_LAUNCHING": "Iniciando Steam!", + "SKIPPING_STEAM_LAUNCH": "Pulando início da Steam" + }, + "msg": { + "BS_LAUNCHING": "Não se esqueça do aquecimento 😉", + "STEAM_LAUNCHING": "Beat Saber irá automaticamente iniciar após a Steam.", + "SKIPPING_STEAM_LAUNCH": "Eu espero que você saiba o que está fazendo :)" + } + }, + "errors": { + "titles": { + "UNKNOWN_ERROR": "Não foi possível iniciar", + "STEAM_NOT_RUNNING": "Steam não está iniciando", + "OCULUS_NOT_RUNNING": "Oculus não está iniciando", + "BS_ALREADY_RUNNING": "Beat Saber já está rodando", + "EXE_NOT_FINDED": "Arquivos faltando", + "PROTON_NOT_SET": "Pasta do Proton não está configurada", + "PROTON_NOT_FOUND": "Binário do Proton não encontrado", + "EXIT": "Interrupção Abrupta", + "OCULUS_LIB_NOT_FOUND": "Biblioteca Oculus não encontrada", + "ORIGINAL_OCULUS_NOT_INSTALLED": "Versão original do Oculus não encontrada" + }, + "msg": { + "UNKNOWN_ERROR": "Um erro desconhecido aconteceu.", + "STEAM_NOT_RUNNING": "Steam precisa estar rodando para iniciar BeatSaber.", + "OCULUS_NOT_RUNNING": "Oculus precisa estar rodando para iniciar.", + "BS_ALREADY_RUNNING": "Feche BeatSaber antes de iniciar novamente.", + "EXE_NOT_FINDED": "Alguns arquivos parecem estar faltando, tente verificar os arquivos.", + "EXIT": "BeatSaber parou abruptamente, tente verificar os arquivos.", + "OCULUS_LIB_NOT_FOUND": "Verifique que o aplicativo Oculus está instalado corretamente, e que as bibliotecas estão definidas no Oculus.", + "PROTON_NOT_SET": "Defina o Proton nas configurações.", + "PROTON_NOT_FOUND": "Defina o caminho da pasta do proton nas configurações.", + "ORIGINAL_OCULUS_NOT_INSTALLED": "Beat Saber precisa estar instalado no aplicativo do Oculus antes de iniciar." + }, + "actions": { + "STEAM_NOT_RUNNING": "Iniciar Steam" + } + } + }, + "steam": { + "steam-launching": { + "title": "Iniciando Steam!", + "description": "Beat Saber irá automaticamente iniciar depois da Steam." + } + }, + "custom-version": { + "errors": { + "titles": { + "CantEditSteam": "Não foi possível editar", + "CantRename": "Impossível renomear", + "VersionAlreadExist": "Essa versão já existe", + "CantClone": "Impossível clonar", + "UnknownError": "Um erro desconhecido aconteceu" + }, + "msg": { + "CantEditSteam": "Você não pode editar a versão da Steam, mas você pode cloná-la." + } + }, + "success": { + "titles": { + "CloningFinished": "Clonagem completa 🎉" + } + } + }, + "mods": { + "install-mods": { + "titles": { + "success": "Mods instalados 🎉", + "warning": "Mods instalados 🤔" + }, + "msg": { + "success": "Todos os mods foram instalados.", + "warning": "Um ou mais mods não foram instalados.", + "errors": { + "no-mods": "Nenhum mod para instalar.", + "cannot-install-bsipa": "Instalação BSIPA falhou 😨" + } + } + }, + "desinstalar-mod": { + "titles": { + "success": "Mod desinstalado 🎉" + }, + "msg": { + "errors": { + "no-mods": "Esse mod não está instalado 😑" + } + } + }, + "desinstalar-all-mods": { + "titles": { + "success": "Mods desinstalados 🎉" + }, + "msg": { + "success": "Todos os mods foram desinstalados.", + "errors": { + "no-mods": "Nenhum mod está instalado nessa versão 😑" + } + } + }, + "import-mod": { + "titles": { + "success": "Importação de mods completa", + "error": "Um erro aconteceu durante a importação de mods" + }, + "msgs": { + "success": "Importação de Mods foi concluída.", + "some-success": "Alguns mods foram importados com sucesso.", + "no-dlls": "Os arquivos não possuem nenhum arquivo \"dll\"." + } + } + }, + "maps": { + "one-click-install": { + "success": "Mapa instalado com sucesso", + "error": "Um erro aconteceu durante a instalação do mapa" + }, + "no-duplicates-maps": { + "title": "Sem duplicatas", + "msg": "Nenhum mapa foi deletado" + }, + "duplicates-maps-deleted": { + "title": "Duplicatas Deletadas", + "msg": "Duplicatas foram deletadas" + }, + "import-map": { + "titles": { + "success": "Importação de Maps concluída", + "error": "Um erro aconteceu durante a importação de mapas" + }, + "msgs": { + "success": "Mapas importados com sucesso.", + "some-success": "Alguns mapas foram importados com sucesso.", + "only-accept-zip": "Apenas arquivos zip são suportados.", + "invalid-zip": "O arquivo zip não possui nenhum mapa.", + "unknown": "Um erro desconhecido aconteceu" + } + } + }, + "playlists": { + "one-click-install": { + "success": "Instalação de playlist concluída", + "error": "Um erro aconteceu durante a importação da playlist" + } + }, + "models": { + "one-click-install": { + "success": "Instalação do Modelo concluída", + "error": "Um erro aconteceu durante a importação do modelo" + } + }, + "shared-folder": { + "info": { + "userdata-backup-created": { + "title": "Backup criado", + "msg": "Compartilhar a past 'UserData' pode gerar erros, em aso de problemas, remova o link da pasta e restaure o backup" + } + }, + "linking-error": { + "title": "Erro durante linkagem do arquivos", + "msg": { + "EPERM": "BSManager não tem as permissões necessárias para abrir a pasta.", + "EACCES": "BSManager não tem as permissões necessárias para linkagem da pasta.", + "ENOSPC": "O disco está cheio, abra algum espaço e tente novamente", + "LinkingNotSupported": "Linkagem de pastas não é suportada nesse sistema de arquivos.", + "UNKNOWN_ERROR": "Um erro desconhecido aconteceu durante a linkagem de arquivos." + } + }, + "adding-error": { + "title": "Adição de pasta compartilhada falhou", + "msg": "Você não pode adicionar \"{folder}\" às pastas compartilhadas." + } + }, + "create-launch-shortcut": { + "success": { + "title": "Atalho criado", + "msg": "O atalho foi criado na Área de Trabalho.", + "msg-steam": "O atalho foi criado na biblioteca da Steam." + }, + "error": { + "msg": "Um erro aconteceu enquanto o atalho era criado." + } + }, + "bs-version-oudated": { + "title": "Versão desatualizada", + "msg": "Essa versão do Beat Saber está desatualizada, use a versão recomendada para utilizar as funções mais recentes e correções de erros", + "actions": { + "do-not-remind": "Não me lembrar novamente", + "ok": "Ok" + } + } + }, + "modals": { + "misc": { + "remember-my-choice": "Lembrar minhas escolhas" + }, + "choose-store": { + "title": "Qual platforma?", + "body": "Selecione a plataforma na qual você quer baixar Beat Saber.", + "set-in-settings": "Configura a plataforma padrão nas configurações" + }, + "guard": { + "title": "Steam Guard", + "inputs": { + "guard-code": { + "label": "Código Guard", + "placeholder": "Digite seu Código do Guard" + } + }, + "buttons": { + "submit": "Login" + } + }, + "steam-login": { + "title": "Steam Login", + "inputs": { + "username": { + "label": "Entrar com seu usuário", + "placeholder": "Digite seu usuário" + }, + "password": { + "label": "Senha", + "placeholder": "Digite sua senha", + "max-length-warning": "Sua senha ultrapassa 64 caracteres! Se a senha é inválida, tente digitar apenas os primeiros 64 caracteres." + }, + "qr": { + "label": "Ou com um QR code", + "note": { + "use-the": "Use o ", + "steam-mobile-app": "Aplicativo móvel da Steam", + "to-connect-with-qr": "Para entrar com o QR code." + } + }, + "stay": "Lembre-se de mim" + }, + "why-credentials": "Por que minhas credenciais são necessárias?", + "need-help-to-connect": "Eu preciso de ajuda para acessar minha conta!", + "buttons": { + "submit": "Entrar" + } + }, + "steam-auth-approve": { + "title": "Esperando confirmação", + "protected-by-mobile-auth": "Conta progeida por autenticador móvel.", + "use-steam-app-to-approve": "Use o aplicativo móvel da steam para confirmar essa conexão...", + "not-access-to-steam-app": "Eu não tenho acesso ao aplicativo móvel da Steam" + }, + "steam-credentials": { + "title": "Credenciais Steam", + "p-1": "Suas credenciais da Steam são necessárias apenas para baixar as versões de Beat Saber, nós usamos DepotDownloader para fazer isso, e verificar que você possuí o jogo na sua biblioteca antes de baixar o jogo. Suas credenciais não são armazenadas ou salvas, e são passadas direto para o DepotDownloader. Entretanto, se você não quiser fazer isso, você pode seguir esse tutorial: ", + "p-2": "Após isso, você pode apertar no ícone de engrenagem no topo direito da tela e selecionar \"Importar uma versãp\", e então selecionar a pasta com a instalação do Beat Saber. (Se você seguiu o tutorial, você vai ter a pasta correta)" + }, + "bs-import-version": { + "title": "Importar uma versão", + "description": "Importa uma versão de Beat Saber para usar com o BSManager. Isso vai copiar a pasta de instalação selecionada para a pasta de versões do BSManager.", + "oculus-version": "Versão do Oculus", + "oculus-version-tooltip": "Verifique se é uma versão do Oculus", + "buttons": { + "submit": "Importar uma versão" + } + }, + "bs-desinstalar": { + "title": "Desinstalar", + "description": "Você tem certeza que quer desinstalar Beat Saber {version}? Você vai ter que baixar ela novamente se quiser jogá-la.", + "buttons": { + "submit": "Desinstalar" + } + }, + "install-folder": { + "title": "Pasta de instalação", + "description": "Alterar a pasta padrão de instalação vai resultar em mover todos os arquivos instalados para a nova pasta.", + "buttons": { + "submit": "Escolher pasta" + } + }, + "edit-version": { + "title": "Editar a versão", + "buttons": { + "submit": "Editar" + } + }, + "clone-version": { + "title": "Clonar a versão", + "description": "Clonar a versão te perimte separar conteúdos adicionais entre duas versões.", + "inputs": { + "name": { + "label": "Nome", + "placeholder": "Nome da versão" + }, + "color": { + "label": "Cor" + } + }, + "buttons": { + "submit": "Clonar" + } + }, + "desinstalar-mod": { + "title": "Desinstalar", + "description": "Você tem certeza que quer desinstalar Beat Saber {mod}? Pode fazer com que outros mods parem de funcionar.", + "description-bsipa": "Você tem certeza que quer desinstalar BSIPA? Depois, nenhum mod irá funcionar." + }, + "desinstalar-all-mods": { + "title": "Desinstalando mods", + "description": "Você tem certeza que quer desinstalar todos os mods da versão {version}? Essa operação não pode ser desfeita." + }, + "maps-actions": { + "delete-maps": { + "title": { + "single": "Deletar esse mapa?", + "multiple": "Deletar mapas?" + }, + "desc": { + "single": "Você tem certeza que quer desinstalar o mapa {name}?", + "multiple": "Você tem certeza que quer desinstalar {nb} mapas?" + }, + "info": { + "desc": { + "single": "Esse mapa faz parte de um mapa compartilhado", + "multiple": "Esses mapas fazem parte de mapas compartilhados" + }, + "title": { + "single": "Esse mapa também será removido de suas versões utilizando mapas compartilhados", + "multiple": "Esses mapas também serão removidos de suas versões utilizando mapas compartilhados" + } + } + } + }, + "link-contents":{ + "title": "Vincular {contentType}", + "p-1": "Vincular {contentType} permite compartilhar {contentType} entre todas as suas versões que possuam essa função habilitada. Uma vez vinculados, essa versão irá se beneficiar dos {contentType} compartilhados.", + "p-2": "Para que possa aproveitar a função de vinculação, todos os seus {itemsHtml} serão movidos para a pasta {sharedHtml}. Depois disso, um link simbólico apontando para a pasta compartilhada {contentType} irá substituir sua pasta original {contentType}.", + "warning": "Por favor, observe que qualquer mudança que afete {contentType} também irá afetar todas as versões com essa função habilitada.", + "what-is-a-symbolic-link": "O que é um link simbólico?", + "i-need-help": "Eu preciso de ajuda", + "valid-btn": "Link {contentType}" + }, + "unlink-contents":{ + "title": "Desvincular {contentType}", + "p-1": "Cuidado! Desvincular {contentType} irá desabilitar o compartilhamento de {contentType} para essa versão, até que seja habilitado novamente.", + "p-2": "Para que possa desvincular seus mapas, o link simbólico criado anteriomente para {sharedHtml} será removido. Depois disso, todos os mapas na pasta compartilhada serão copiados para a pasta {itemsHtml} da sua versão do Beat Saber.", + "do-not-copy-contents": "Não copie {contentType}", + "do-not-copy-contents-tip": "Se habilitado, {contentType} na pasta compartilhada não será copiado para sua versão de Beat Saber. Como consequência, nenhum {contentType} irá se manter na sua versão do Beat Saber depois da desvinculação.", + "valid-btn": "Desvincular {contentType}" + }, + "download-maps": { + "search-btn": "Procurar", + "loading-maps": "Carregando mapas...", + "no-maps-found": "Nenhum mapa encontrado", + "no-internet": "Sem internet" + }, + "mods-disclaimer": { + "title": "Aviso", + "p-1": "Ao escolher utilizar mods, você entende que:", + "li-1": "Você pode experienciar problemas que não existem no jogo convencional. 99.9% dos bugs, crashes, e lags acontecem por causa de mods.", + "li-2": "Mods estão sujeitos a serem quebrados por atualizações e isso é normal - seja paciente e respeitoso quando isso acontecer, visto que as pessoas são voluntárias com vidas reais.", + "li-3": "A empresa não está propositalmente tentando quebrar os mods. Eles apenas querem trabalhar no código base e as vezes isso quebra os mods, mas eles não estão tentando matá-los.", + "p-2": "Não ataque os desenvolvedores do jogo por problemas relacionados aos mods e vice-versa. Os desenvolvedores do jogo e dos mods são 2 grupos separados. Apenas não seja um idiota, ok?." + }, + "shared-folders": { + "title": "Pastas compartilhadas", + "description": "Vincule pastas do Beat Saber para sincronizar seus conteúdos com pastas compartilhadas entre as versões. Observe que deleções também são compartilhadas.", + "buttons": { + "add-folder": "Adicionar Pasta", + "link-folder": "Vincular Pasta", + "unlink-folder": "Desvincular Pasta", + "link-all": "Vincular todas", + "remove-from-the-list": "Remover da lista" + } + }, + "adding-shared-folder": { + "title": "Adicionando pasta compartilhada", + "description": "Você pode experienciar problemas vinculando a pasta \"{folder}\". Você tem certeza que quer adicioná-la?" + }, + "create-launch-shortcut": { + "title": "Criar um atalho", + "desc": "Criar um atalho irá permitir que você inicie o Beat Saber com as opções escolhidas sem ter que entrar no BSManager.", + "launch-options": "Iniciar opções", + "advanced-launch": "Início Avançado", + "valid-btn": "Criar o atalho", + "create-steam-shortcut": "Criar um atalho da Steam", + "steam-shortcut-tippy": "Se habilitado, ao invés de criar um atalho na área de trabalho, o atalho será criado na Steam." + }, + "connect-to-meta": { + "title": "Conectar a Meta", + "body": { + "token-needed": "Seu token de conexão com a Meta é necessário para baixar Beat Saber.", + "need-cookie-enabled": "Ao entrar na Meta, uma janela de login irá aparecer e então você pode iniciar o processo de login. Por favor, lembre de aceitar os cookies, caso contrário, podemos não conseguir recuperar seu token para começar o download.", + "enter-token-manually": "Digitar meu token de login manualmente", + "enter-token-manually-tooltip": "Isso irá lhe permitir digitar seu token de login sem ter que passar pelo login da Meta." + }, + "stay": "Lembre-se de mim", + "connect-to-meta": "Conectar a Meta" + }, + "enable-oculus-sideloaded-apps": { + "title": "Habilitar Aplicativos Externos", + "info-1": "Para que possamos iniciar Beat Saber, a possibilidade de executar aplicativos carregados externamente precisam estar habilitados. BSManager irá solicitar acesso de administrador para habilitar essa função automaticamente.", + "info-2": "A função de aplicativos externos irá permitir que jogos sejam iniciados por fora da sua pasta de biblioteca do Oculus.", + "info-3": "Após ativar os aplicativos externos, a função irá se manter ativa, e não pediremos mais para habilitá-la.", + "i-want-to-do-it-myself": "Eu quero fazer isso eu mesmo", + "understood": "Entendido" + }, + "enter-meta-token": { + "title": "Token Oculus", + "body": { + "info-enter-token": "Para baixar Beat Saber, seu token de login do Oculus é necessário.", + "how-obtain-token": "Como eu obtenho meu token Oculus?", + "oculus-token": "Token Oculus", + "token-is-invalid": "O token é inválido.", + "save-my-token": "Salvar meu token", + "have-token-saved": "Eu já tenho um token salvo", + "save-token-info": "Isso irá salvar seu token para facilitar o reuso. Você precisará criar uma senha para criptografar seu token a fim de armazená-lo com segurança. Se você esquecer sua senha, só precisará fornecer o token novamente.", + "password": "Senha", + "password-too-short": "Senha muito curta", + "info-enter-password": "Para baixar Beat Saber, seu token de login é necessário. Digite a senha anteriormente utilizada para salvar seu token.", + "info-disabled-btn-password": "Não foi possível descriptografar o token com essa senha. Tenha certeza que a senha é a mesma utilizada para salvar o token anteriormente.", + "enter-oculus-token": "Digite um token Oculus" + }, + "valid-btn": "Validar" + }, + "launch-as-admin": { + "title": "Permissões de Administrador", + "body": { + "info": "Steam está rodando com permissões de administrador. Para comunicar com a Steam, o Beat Saber também precisa ser iniciado como administrador. Caso contrário, Beat Saber pode experiênciar problemas e fechar após iniciar.", + "info-2": "Iniciar Beat Saber no modo administrador também irá dar permissões de administrador para os mods instalados. Dessa forma, é recomendado que você reinicie a Steam sem permissões de administrador.", + "info-3": "Observe que não é recomendado dar permissões de administrador para a Steam, visto que afeta os mods e os jogos instalados, podendo ser um risco de segurança." + }, + "launch-as-admin": "Iniciar como Administrador", + "not-remind-me": "Não me perguntar novamente" + }, + "ask-install-path": { + "title": "Pasta de instalação", + "choose-folder-description": "Escolha a pasta que irá conter todos os conteúdos baixados pelo BSManager. (versões, mods, mapas, playlists, etc.)", + "default": "Padrão", + "default-tooltip": "Utiliza sua pasta padrão" + }, + "choose-proton-folder": { + "title": "Pasta Proton", + "proton-folder-description": "No linux, BSManager precisa do Proton para funcionar. Escolha a pasta de instalação do Proton para continuar.", + "proton-folder-placeholder": "Pasta de instalação do Proton Proton", + "where-is-proton-installed": "Onde Proton é instalado?" + }, + "bs-version-outdated": { + "body": "Essa versão {outdatedVersion} está desatualizada, e alguns mods ou funções podem não funcionar como esperado. Por favor baixe a versão mais recente ({recommendedVersion}) de Beat Saber para aproveitar as últimas novidades e correções." + } + }, + "maps": { + "map-filter-panel": { + "duration": "Duração", + "nps": "Notas Por Segundo", + "njs": "Velocidade de Aparecimento das Notas", + "tags": "Rótulos", + "specificities": "Gerais", + "requirements": "Requerimentos", + "exclude": "Excluir", + "leaderboard": "leaderboard" + }, + "map-types": { + "accuracy": "Acurácia", + "balanced": "Balanceado", + "challenge": "Desafios", + "dance-style": "dança", + "fitness": "fitness", + "speed": "velocidade", + "tech": "técnica" + }, + "map-styles": { + "dance": "dança", + "swing": "swing", + "nightcore": "nightcore", + "folk-acoustic": "folk & acústica", + "kids-family": "crianças & familia", + "ambient": "ambiente", + "funk-disco": "funk & disco", + "jazz": "jazz", + "soul": "soul", + "speedcore": "speedcore", + "punk": "punk", + "rb": "r&b", + "holiday": "festivas", + "vocaloid": "vocaloid", + "j-rock": "j-rock", + "trance": "trance", + "drum-and-bass": "bateria & baixo", + "comedy-meme": "comedia & memes", + "instrumental": "instrumental", + "hardcore": "hardcore", + "k-pop": "k-pop", + "indie": "indie", + "techno": "techno", + "house": "house", + "video-game-soundtrack": "video game", + "tv-movie-soundtrack": "TV & film", + "alternative": "alternativos", + "dubstep": "dubstep", + "metal": "metal", + "anime": "anime", + "hip-hop-rap": "hip hop & rap", + "j-pop": "j-pop", + "rock": "rock", + "pop": "pop", + "electronic": "electrônica", + "classical-orchestral": "Classica & Orquestras" + }, + "map-specificities": { + "automapper": "AI", + "curated": "Curadas", + "verified": "verificadas", + "fullSpread": "full spread" + }, + "map-leaderboard": { + "All": "All", + "Ranked": "Ranqueada", + "BeatLeader": "BeatLeader", + "ScoreSaber": "ScoreSaber" + }, + "map-excludes": { + "installed": "instaladas" + }, + "difficulties": { + "Easy": "fácil", + "Normal": "normal", + "Hard": "difícil", + "Expert": "expert", + "ExpertPlus": "expert+" + }, + "map-item": { + "by": "Por {songAutor}", + "mapped-by": "Mapeado por", + "delete": "Deletar", + "preview": "Pré-visualização do mapa", + "bsr-code": "código BSR", + "download": "Baixar mapa", + "downloading": "Baixando mapa", + "cancel-download": "Cancelar download", + "hightlight-difficulty": "Destacar dificuldade" + } + }, + "models": { + "types": { + "singular": { + "avatar": "avatar", + "saber": "sabre", + "platform": "platforma", + "bloq": "bloco" + }, + "plural": { + "avatar": "avatares", + "saber": "sabres", + "platform": "platformas", + "bloq": "blocos" + } + }, + "sorts": { + "name": "Nome", + "date": "Data", + "author": "Autor" + }, + "panel": { + "actions": { + "search": "Procurar um modelo", + "drop-down": { + "delete": "Deletar modelos", + "export": "Exportar modelos" + }, + "link-models": "Vincular modelos", + "unlink-models": "Desvincular modelos" + }, + "grid": { + "loading": "Carregando modelos...", + "no-models": "Nenhum modelo", + "download-models": "Baixar modelos" + } + }, + "modals": { + "delete-model": { + "title": "Deletar o modelo", + "desc": "Tem certeza que quer deletar o modelo {modelName}?", + "linked-annotation": "Esse modelo será removido de todas as suas versões vinculadas." + }, + "delete-models": { + "title": "Deletar modelos", + "desc": "Tem certeza que quer deletar {nb} modelos?", + "linked-annotation": "Esses modelos serão removidos de todas as suas versões vinculadas." + }, + "download-models": { + "search-btn": "Procurar", + "search-placeholder": "Procurar um modelo", + "search-tips": { + "header": { + "tag": "Rótulo", + "desc": "Descrição" + }, + "author-desc": "Mostrar apenas modelos desse autor específico.", + "hash-desc": "Mostrar apenas modelos com essa hash específica.", + "tag-desc": "Mostrar apenas modelos com esse rótulo.", + "name-desc": "Mostrar apenas modelos com o nome especificado.", + "discordid-desc": "Mostrar apenas modelos desse usuário do discord.", + "status-desc": "Mostrar apenas modelos com esse status específico. (apenas perfil, e apenas do autor)" + }, + "no-models": "Nenhum modelo encontrado.", + "no-internet": "Sem conexão com a internet.", + "error-occurred": "Um erro aconteceu, tente novamente" + } + }, + "notifications": { + "prevent-for-mods": { + "title": "Mods necessários", + "desc": "Certifique-se de instalar os mods necessários par autilizar esse modelo em Beat Saber", + "go-to-mods": "Ir para mods", + "not-remind": "Não me lembrar novamente" + }, + "export-success": { + "title": "Exportação completa 🎉" + } + } + }, + "beat-saver": { + "maps-sorts": { + "Latest": "Mais recente", + "Relevance": "Relevância", + "Rating": "Avaliação", + "Curated": "Curados" + } + }, + "auto-update": { + "checking": "Verificando atualizações", + "downloading": "Baixando atualizações" + }, + "bs-shortcut-launch": { + "beat-saber-launching": "Iniciando Beat Saber", + "launching": "Iniciando", + "open-bsmanager": "Abra BSManager", + "status-text": { + "init": "Inicializando...", + "success": { + "BS_LAUNCHING": "Inicializando Beat Saber...", + "STEAM_LAUNCHING": "Iniciando Steam...", + "STEAM_LAUNCHED": "Steam iniciada com sucesso!", + "UNABLE_TO_LAUNCH_STEAM": "Não foi possível iniciar Steam, obrigando Beat Saber a iniciar..." + } + } + }, + "drop-zone": { + "or-browse-files": "Ou navegar pelos arquivos" + }, + "playlist": { + "error-playlist-creation-title": "Erro criando playlist", + "error-playlist-creation-desc": "Um erro aconteceu enquanto criávamos a playlist.", + "playlist-created-title": "Playlist criada", + "playlist-created-desc": "A playlist foi criada com sucesso, agora você pode sincronizar seus mapas!", + "download-playlist": "Baixar playlist", + "synchronize-playlist": "Sincronizar playlist", + "synchronize-maps": "Sincronizar maps", + "error-playlists-synchronization-title": "Erro ao sincronizar playlists", + "error-playlists-synchronization-desc": "Um erro aconteceu enquanto sincronizávamos as playlists.", + "playlists-synchronized-title": "Playlists sincronizadas!", + "playlists-synchronized-desc": "Playlists e seus mapas foram baixados.", + "playlists-export-error-title": "Erro ao exportar playlists", + "playlists-export-error-desc": "Um erro aconteceu enquanto exportávamos as playlists.", + "playlists-exported-title": "Playlists exportadas!", + "playlists-exported-desc": "Playlists foram exportadas com sucesso.", + "playlists-with-maps-exported-desc": "Playlists e seus mapas foram exportados com sucesso.", + "playlist-delete-error-title": "Erro ao deletar playlist", + "playlist-delete-error-desc": "Um erro aconteceu enquanto tentávamos deletar a playlist.", + "playlists-deleted-title": "Playlists deletadas!", + "playlists-deleted-desc": "Playlists foram deletadas com sucesso.", + "edit-playlist": "Editar playlist", + "playlist-edit-error-title": "Erro ao editar playlist", + "playlist-edit-error-desc": "Um erro aconteceu enquanto editavamos a playlist.", + "playlist-edited-title": "Playlist editada!", + "playlist-edited-desc": "A playlist foi modificada com sucesso. Agora você pode sincronizar seus mapas!", + "playlists-loading": "Carregando playlists...", + "no-playlists": "Nenhuma playlists", + "download-playlists": "Baixar playlists", + "created-by": "Criado por", + "stop-download": "Pausar download", + "cancel-download": "Cancelar download", + "open-file": "Abrir arquivo", + "delete-playlist-ask": "Deletar playlist?", + "delete-playlists-ask": "Deletar playlists?", + "delete-playlist-desc": "Tem certeza que quer deletar a playlist \"{playlistTitle}\"?", + "delete-playlists-desc": "Tem certeza que quer deletar {nb} playlists?", + "delete-maps": "Deletar mapas", + "delete-playlist-maps-tip": "Se habilitado, todos os mapas na playlist serão deletados", + "delete-playlists-maps-tip": "Se habilitado, todos os mapas na playlist serão deletados", + "export-playlist-ask": "Exportar playlist?", + "export-playlists-ask": "Exportar playlists?", + "export-playlist-desc": "Tem certeza que quer exportar a playlist \"{playlistTitle}\"?", + "export-playlists-desc": "Tem certeza que quer exportar {nb} playlists?", + "export-maps": "Exportar mapas", + "export-playlist-maps-tip": "Se habilitado, todos os mapas na playlist também serão exportados", + "export-playlists-maps-tip": "Se habilitado, todos os mapas na playlist também serão exportados", + "export": "Exportar", + "need-clone-title": "Aviso", + "need-clone-desc-1": "Essa playlist foi baixada de um site externo e contém um vínculo de sincronização.", + "need-clone-desc-2": "Para impedir que perca suas alterações durante a sincronização, essa playlist será duplicada e sua vinculação será removida.", + "need-clone-desc-3": "Você pode depois, se desejar, deletar a playlist original.", + "understood": "Eu entendi", + "synchronize-playlist-ask": "Sincronizar playlist?", + "synchronize-playlists-ask": "Sincronizar playlists?", + "synchronize-playlist-desc": "Tem certeza que quer sincronizar a playlist \"{playlistTitle}\"?", + "synchronize-playlists-desc": "Tem certeza que quer sincronizar {nb} playlists?", + "synchronize-playlist-tip": "Essa ação atualizará as playlists e baixará mapas que estão faltando; Pode levar vários minutos.", + "synchronize": "Sincronizar", + "curated": "Curadas", + "verified-mapper": "Criador de mapas Verificado", + "empty-playlists": "Playlists vazias", + "search-playlist": "Procurar por uma playlist", + "no-playlists-found": "Nenhuma playlists encontrada", + "error-occur-while-loading-playlists": "Um erro aconteceu enquanto carregávamos a playlist", + "error-occur-while-loading-playlist": "Um erro aconteceu enquanto carregávamos a playlist", + "loading-maps": "Carregando mapas...", + "no-maps-found-for-playlist": "Nenhum mapa encontrado para essa playlist", + "playlist-contain-no-maps": "Essa playlist não possui mapas", + "no-map-installed-for-playlist": "Nenhum mapa instalado para essa playlist", + "playlist-is-waiting-to-download": "A playlist está esperando para ser baixada", + "download-maps": "Baixar mapas", + "download-missing-maps": "Baixar mapas que estão faltando", + "playlist-is-downloading": "A playlist está sendo baixada", + "some-playlist-maps-are-missing": "Alguns mapas nessa playlist estão faltando", + "create-a-playlist": "Criar uma playlist", + "synchronize-playlists": "Sincronizar playlists", + "export-playlists": "Exportar playlists", + "delete-playlists": "Deletar playlists", + "choose-image": "Escolher uma imagem", + "title": "Título", + "playlist-title": "Título da Playlist", + "description": "Descrição", + "playlist-description": "Descrição da Playlist", + "author": "Autor", + "playlist-author": "Autor da Playlist", + "save": "Salvar", + "loading": "Carregando...", + "installed": "Instalado", + "no-map-found": "Nenhum mapa encontrado", + "edit-playlist-shortcuts": "Segure Shift ou Ctrl para selecionar múltiplos mapas", + "add-to-playlist": "Adicionar à playlist", + "remove-from-playlist": "Remover da playlist", + "playlist-is-empty": "A playlist está vazia", + "continue": "Continuar", + "nb-maps": "Quantidade de mapas", + "nb-mappers": "Quantidade de editores de mapa", + "duration": "Duração", + "nps": "Notas por segundo", + "date-picker": { + "start-date-end-date": "Date ínicio — Data Fim", + "all": "Todos", + "last-24h": "Últimas 24hrs", + "last-week": "Última semana", + "last-month": "Último mês", + "3-last-month": "Últimos 3 meses" + }, + "playlists-imported": "Playlists importadas", + "all-playlists-have-been-successfully-imported": "Todas as playlists foram importadas com sucesso", + "no-playlist-found": "Nenhuma playlist encontrada", + "no-playlist-found-in-selected-files": "Nenhuma playlist encontrada nos arquivos selecionados", + "some-playlists-not-imported": "Algumas playlists não foram importadas", + "some-playlists-have-been-imported": { + "INVALID_SOURCE": "Algumas playlists não puderam ser encontradas", + "INVALID_PLAYLIST_FILE": "Algumas playlists são inválidas", + "CANNOT_PARSE_PLAYLIST": "Algumas playlists não podem ser lidas", + "unknown": "Algumas playlists não puderam ser importadas" + }, + "no-playlists-imported": "Nenhuma playlists importada", + "no-playlists-imported-errors": { + "INVALID_SOURCE": "Playlists não foram encontradas", + "INVALID_PLAYLIST_FILE": "Playlists são invalidas", + "CANNOT_PARSE_PLAYLIST": "Playlists não podem ser lidas", + "unknown": "Nenhuma playlist pode ser importada" + } + }, + "dateformat": { + "dayNames": [ + "Dom", + "Seg", + "Ter", + "Qua", + "Qui", + "Sex", + "Sab", + "Domingo", + "Segunda", + "Terça", + "Quarta", + "Quinta", + "Sexta", + "Sábado" + ], + "monthNames": [ + "Jan", + "Fev", + "Mar", + "Abr", + "Maio", + "Jun", + "Jul", + "Ago", + "Set", + "Out", + "Nov", + "Dez", + "Janeiro", + "Fevereiro", + "Março", + "Abril", + "Maio", + "Junho", + "Julho", + "Agosto", + "Setembro", + "Outubro", + "Novembro", + "Dezembro" + ], + "timeNames": [ + "a", + "p", + "am", + "pm", + "A", + "P", + "AM", + "PM" + ] + } +} diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index d583e90e..bd6cca5a 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -227,6 +227,7 @@ "es-ES": "Español", "it-IT": "Italiano", "de-DE": "Deutsch", + "pt-BR": "Português (Brasil)", "ru-RU": "Русский", "zh-CN": "简体中文", "zh-TW": "正體中文", @@ -239,6 +240,7 @@ "es-ES": "Испанский", "it-IT": "Итальянец", "de-DE": "Немецкий", + "pt-BR": "португальский (Бразилия)", "ru-RU": "Русский", "zh-CN": "Китайский (упрощенный)", "zh-TW": "Китайский (традиционный)", diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index bec5592d..48a26ce6 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -227,6 +227,7 @@ "es-ES": "Español", "it-IT": "Italiano", "de-DE": "Deutsch", + "pt-BR": "Português (Brasil)", "ru-RU": "Русский", "zh-CN": "簡體中文", "zh-TW": "正體中文", @@ -239,6 +240,7 @@ "es-ES": "西班牙語", "it-IT": "義大利語", "de-DE": "德語", + "pt-BR": "葡萄牙語 (巴西)", "ru-RU": "俄語", "zh-CN": "簡體中文", "zh-TW": "正體中文", diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index 93434081..9b1b76b4 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -239,6 +239,7 @@ "es-ES": "西班牙语", "it-IT": "意大利语", "de-DE": "德语", + "pt-BR": "葡萄牙语 (巴西)", "ru-RU": "俄语", "zh-CN": "简体中文", "zh-TW": "正体中文", diff --git a/src/renderer/components/svgs/bsm-icon.component.tsx b/src/renderer/components/svgs/bsm-icon.component.tsx index 28a9f7f2..fde8d8f4 100644 --- a/src/renderer/components/svgs/bsm-icon.component.tsx +++ b/src/renderer/components/svgs/bsm-icon.component.tsx @@ -58,6 +58,7 @@ import { ChineseIcon } from "./flags/chinese-icon.component"; import { ChineseTraditionalIcon } from "./flags/chineseTraditional-icon.component"; import { JapanIcon } from "./flags/japan-icon.component"; import { KoreaIcon } from "./flags/korea-icon.component"; +import { BrazilIcon } from "./flags/brasil-icon.component"; import { ChevronTopIcon } from "./icons/chevron-top-icon.component"; import { EyeCrossIcon } from "./icons/eye-cross-icon.component"; import { ShortcutIcon } from "./icons/shortcut-icon.component"; @@ -72,7 +73,7 @@ import { ArrowUpwardIcon } from "./icons/arrow-upward-icon.component"; -export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "arrow-upward" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" ); +export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "terminal" | "desktop" | "oculus" | "add" | "cross" | "task" | "github" | "close" | "thumbUpFill" | "timerFill" | "pause" | "twitter" | "sync" | "chevron-top" | "copy" | "steam" | "edit" | "export" | "patreon" | "search" | "bsMapDifficulty" | "link" | "unlink" | "download" | "filter" | "mee6" | "volume-up" | "volume-off" | "volume-down" | "shortcut" | "backup-restore" | "web-site" | "clean" | "browse" | "add-file" | "arrow-upward" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "pt-BR-flag" | "null" ); export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => { // TODO : Very ugly very messy, need to find a better way to do this @@ -142,6 +143,10 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i if (icon === "ko-KR-flag"){ return ; } + if (icon === "pt-BR-flag"){ + return ; + } + if (icon === "task") { return ; } diff --git a/src/renderer/components/svgs/flags/brasil-icon.component.tsx b/src/renderer/components/svgs/flags/brasil-icon.component.tsx new file mode 100644 index 00000000..b6027db8 --- /dev/null +++ b/src/renderer/components/svgs/flags/brasil-icon.component.tsx @@ -0,0 +1,11 @@ +import { CSSProperties } from "react"; + +export function BrazilIcon(props: { className?: string; style?: CSSProperties }) { + return ( + + + + + + ); +}; diff --git a/src/renderer/config/default-configuration.config.ts b/src/renderer/config/default-configuration.config.ts index a9763d2d..e2c2bd90 100644 --- a/src/renderer/config/default-configuration.config.ts +++ b/src/renderer/config/default-configuration.config.ts @@ -25,7 +25,7 @@ export const defaultConfiguration: { "second-color": "#ff4444", theme: "os", language: window.navigator.language.length <= 2 ? `${window.navigator.language}-${window.navigator.language.toLocaleUpperCase()}` : window.navigator.language, - supported_languages: ["en-US", "en-EN", "fr-FR", "es-ES", "it-IT", "de-DE", "ru-RU", "zh-CN", "zh-TW", "ja-JP", "ko-KR"], + supported_languages: ["en-US", "en-EN", "fr-FR", "es-ES", "it-IT", "de-DE", "ru-RU", "zh-CN", "zh-TW", "ja-JP", "ko-KR", "pt-BR"], default_mods: ["SongCore", "WhyIsThereNoLeaderboard", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager"], "default-shared-folders": [ window.electron.path.join("Beat Saber_Data", "CustomLevels"), From 93bcd358cb43d5b19aedff3c791c6cf72ba7c349 Mon Sep 17 00:00:00 2001 From: silentrald Date: Wed, 22 Jan 2025 08:35:47 +0800 Subject: [PATCH 14/29] [feat] just set the deleteLogFolders than appending the new list --- src/main/main.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/main/main.ts b/src/main/main.ts index e1c57273..ab12b37c 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -224,15 +224,13 @@ function initLogger(){ // Keep only the past week (7 days) of logs function deleteOldLogs(): void { - const deleteLogFolders: Dirent[] = []; + let deleteLogFolders: Dirent[] = []; try { const filterDate = convertDateToDateString( new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // 7 days ); - deleteLogFolders.push.apply([], - readdirSync(app.getPath("logs"), { withFileTypes: true }) - .filter(folder => folder.isDirectory() && folder.name <= filterDate) - ); + deleteLogFolders = readdirSync(app.getPath("logs"), { withFileTypes: true }) + .filter(folder => folder.isDirectory() && folder.name <= filterDate); } catch (error) { log.error("Error while deleting old logs:", error); return; From cf0e092705162ec8d877be1768c5adb0d9e3ea8d Mon Sep 17 00:00:00 2001 From: Arne Keller Date: Wed, 22 Jan 2025 09:36:42 +0100 Subject: [PATCH 15/29] package.json: correct license value based on ./LICENSE --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 4e26697f..e9485e6e 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,7 @@ "url": "https://github.com/Zagrios" }, "contributors": [], - "license": "MIT", + "license": "GPL-3.0-only", "bugs": { "url": "https://github.com/Zagrios/bs-manager/issues" }, From 5d4b79c04101b574598d4934cb3dc8b160f634aa Mon Sep 17 00:00:00 2001 From: silentrald Date: Tue, 21 Jan 2025 01:04:12 +0800 Subject: [PATCH 16/29] [feat] added a way to parse env strings --- src/__tests__/unit/env.test.ts | 61 +++++++++++++++++ src/main/helpers/env.helpers.ts | 117 +++++++++++++++++++++++++++++++- 2 files changed, 177 insertions(+), 1 deletion(-) create mode 100644 src/__tests__/unit/env.test.ts diff --git a/src/__tests__/unit/env.test.ts b/src/__tests__/unit/env.test.ts new file mode 100644 index 00000000..fc6c80d2 --- /dev/null +++ b/src/__tests__/unit/env.test.ts @@ -0,0 +1,61 @@ +import { parseEnvString } from "main/helpers/env.helpers"; + +describe("Test parseEnvString", () => { + + it("Empty", () => { + const envVars = parseEnvString(""); + expect(envVars).toEqual({}); + }); + + it("Single test; no quotes", () => { + const envString = "HELLO=World!"; + const envVars = parseEnvString(envString); + expect(envVars).toEqual({ + HELLO: "World!", + }); + }); + + it("Single test; single quotes", () => { + const envString = "SINGLE_QOUTE='Single quote with spaces'"; + const envVars = parseEnvString(envString); + expect(envVars).toEqual({ + SINGLE_QOUTE: "Single quote with spaces", + }); + }); + + it("Single test; double quotes", () => { + const envString = 'DOUBLE_QOUTE="Some random quote."'; + const envVars = parseEnvString(envString); + expect(envVars).toEqual({ + DOUBLE_QOUTE: "Some random quote.", + }); + }); + + it("Single test; empty value", () => { + const envString = "EMPTY="; + const envVars = parseEnvString(envString); + expect(envVars).toEqual({ + EMPTY: "", + }); + }); + + it("Multiple test; combined", () => { + const envString = `HELLO=World! DOUBLE_QUOTE="Two Words" SINGLE_QUOTE='' EMPTY=` + const envVars = parseEnvString(envString); + expect(envVars).toEqual(expect.objectContaining({ + HELLO: "World!", + DOUBLE_QUOTE: "Two Words", + SINGLE_QUOTE: "", + EMPTY: "" + })); + }); + + it("Key with numbers and lower case", () => { + const envString = "H3ll0=world"; + const envVars = parseEnvString(envString); + expect(envVars).toEqual({ + H3ll0: "world", + }); + }); + +}); diff --git a/src/main/helpers/env.helpers.ts b/src/main/helpers/env.helpers.ts index 8eaffea7..677fa2eb 100644 --- a/src/main/helpers/env.helpers.ts +++ b/src/main/helpers/env.helpers.ts @@ -1,3 +1,4 @@ +import { CustomError } from "shared/models/exceptions/custom-error.class"; import { ProviderPlatform } from "shared/models/provider-platform.enum"; export function execOnOs(executions: { [key in ProviderPlatform]?: () => T }, noError = false): T { @@ -10,4 +11,118 @@ export function execOnOs(executions: { [key in ProviderPlatform]?: () => T }, } return undefined; -} \ No newline at end of file +} + +enum EnvParserState { + NAME_START, + NAME, + VALUE_START, + VALUE, + QUOTE_VALUE, + DQUOTE_VALUE, + SPACE, + ERROR, +}; + +const isAlphaCharacter = (c: string) => + (c >= "a" && c <= "z") || (c >= "A" && c <= "Z"); +const isNumber = (c: string) => c >= "0" && c <= "9"; + +export function parseEnvString(envString: string): Record { + const envVars: Record = {}; + + let state: EnvParserState = EnvParserState.NAME_START; + let index = 0; + let newName = ""; + for (let pos = 0; pos < envString.length; ++pos) { + const c = envString[pos]; + + switch (state) { + case EnvParserState.NAME_START: + if (isAlphaCharacter(c) || c === "_") { + state = EnvParserState.NAME; + index = pos; + } else if (c !== " ") { + state = EnvParserState.ERROR; + } + break; + + case EnvParserState.NAME: + if (c === "=") { + state = EnvParserState.VALUE_START; + newName = envString.substring(index, pos); + index = pos + 1; + } else if (!isAlphaCharacter(c) && !isNumber(c) && c !== "_") { + state = EnvParserState.ERROR; + } + break; + + case EnvParserState.VALUE_START: + if (c === "'") { + ++index; + state = EnvParserState.QUOTE_VALUE; + } else if (c === '"') { + ++index; + state = EnvParserState.DQUOTE_VALUE; + } else if (c === " ") { + state = EnvParserState.NAME_START; + envVars[newName] = ""; + } else { + state = EnvParserState.VALUE; + } + break; + + case EnvParserState.VALUE: + if (c === " ") { + state = EnvParserState.NAME_START; + envVars[newName] = envString.substring(index, pos); + } + break; + + case EnvParserState.QUOTE_VALUE: + if (c === "'") { + state = EnvParserState.SPACE; + envVars[newName] = envString.substring(index, pos); + } + break; + + case EnvParserState.DQUOTE_VALUE: + if (c === '"') { + state = EnvParserState.SPACE; + envVars[newName] = envString.substring(index, pos); + } + break; + + case EnvParserState.SPACE: + if (c === " ") { + state = EnvParserState.NAME_START; + } else { + state = EnvParserState.ERROR; + } + break; + + default: + } + + if (state === EnvParserState.ERROR) { + throw new CustomError( + `parseEnvString failed: invalid character at position ${pos}`, + "env.parse" + ); + } + } + + if (state === EnvParserState.VALUE_START || state === EnvParserState.VALUE) { + envVars[newName] = envString.substring(index); + return envVars; + } + + if (state === EnvParserState.NAME_START || state === EnvParserState.SPACE) { + return envVars; + } + + throw new CustomError( + "parseEnvString failed: invalid ending state", + "env.parse" + ); +} From c6ff848bfa6f347a87d3b094d891e0596b6eee2d Mon Sep 17 00:00:00 2001 From: silentrald Date: Tue, 21 Jan 2025 01:36:38 +0800 Subject: [PATCH 17/29] [feat] inject env vars to launch options with steam format %command% --- assets/jsons/translations/en.json | 5 +++++ src/main/helpers/env.helpers.ts | 4 ++-- src/main/services/linux.service.ts | 20 ++++++++++++++++++++ src/renderer/services/bs-launcher.service.ts | 7 ++++++- 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 8b769c7d..557137b8 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -28,6 +28,11 @@ "pin": "Pin", "unpin": "Unpin" }, + "generic": { + "env": { + "parse": "Could not properly parse the env var string." + } + }, "title-bar": { "outdated": "outdated" }, diff --git a/src/main/helpers/env.helpers.ts b/src/main/helpers/env.helpers.ts index 677fa2eb..bb4dd8c6 100644 --- a/src/main/helpers/env.helpers.ts +++ b/src/main/helpers/env.helpers.ts @@ -107,7 +107,7 @@ export function parseEnvString(envString: string): Record { if (state === EnvParserState.ERROR) { throw new CustomError( `parseEnvString failed: invalid character at position ${pos}`, - "env.parse" + "generic.env.parse" ); } } @@ -123,6 +123,6 @@ export function parseEnvString(envString: string): Record { throw new CustomError( "parseEnvString failed: invalid ending state", - "env.parse" + "generic.env.parse" ); } diff --git a/src/main/services/linux.service.ts b/src/main/services/linux.service.ts index 5f12896a..e956148c 100644 --- a/src/main/services/linux.service.ts +++ b/src/main/services/linux.service.ts @@ -10,6 +10,7 @@ import { BsmShellLog, bsmExec } from "main/helpers/os.helpers"; import { LaunchMods } from "shared/models/bs-launch/launch-option.interface"; import { SteamShortcutData } from "shared/models/steam/shortcut.model"; import { buildBsLaunchArgs } from "./bs-launcher/abstract-launcher.service"; +import { parseEnvString } from "main/helpers/env.helpers"; export class LinuxService { private static instance: LinuxService; @@ -24,6 +25,7 @@ export class LinuxService { private readonly installLocationService: InstallationLocationService; private readonly staticConfig: StaticConfigurationService; + private readonly COMMAND_FORMAT = "%command%"; private nixOS: boolean | undefined; private constructor() { @@ -112,6 +114,24 @@ export class LinuxService { envVars.PROTON_LOG_DIR = path.join(bsFolderPath, "Logs"); } + if (launchOptions.additionalArgs) { + const additionalArgs = launchOptions.additionalArgs.join(" "); + const index = additionalArgs.indexOf(this.COMMAND_FORMAT); + if (index > -1) { + const envString = additionalArgs.substring(0, index); + log.info("Parsing env string ", `"${envString}"`) + for (const [ key, value ] of Object.entries(parseEnvString(envString))) { + if (key in envVars) { + log.warn("Ignoring", `${key}=${value}`, "already set env launch command"); + } else { + log.info("Injecting", `${key}="${value}"`, "to the env launch command"); + } + } + } + + launchOptions.additionalArgs = [ additionalArgs.substring(index + this.COMMAND_FORMAT.length) ]; + } + return envVars; } diff --git a/src/renderer/services/bs-launcher.service.ts b/src/renderer/services/bs-launcher.service.ts index 86da6d59..3594d568 100644 --- a/src/renderer/services/bs-launcher.service.ts +++ b/src/renderer/services/bs-launcher.service.ts @@ -61,7 +61,12 @@ export class BSLauncherService { this.notificationService.notifySuccess({title: `notifications.bs-launch.success.titles.${event.type}`, desc: `notifications.bs-launch.success.msg.${event.type}`}); }, error: (err: CustomError) => { - if(!err?.code || !Object.values(BSLaunchError).includes(err.code as BSLaunchError)){ + if (err?.code?.startsWith("generic.")) { + this.notificationService.notifyError({ + title: "notifications.bs-launch.errors.titles.UNKNOWN_ERROR", + desc: err.code, + }); + } else if(!err?.code || !Object.values(BSLaunchError).includes(err.code as BSLaunchError)){ this.notificationService.notifyError({title: "notifications.bs-launch.errors.titles.UNKNOWN_ERROR", desc: "notifications.bs-launch.errors.msg.UNKNOWN_ERROR"}); } else { this.notificationService.notifyError({title: `notifications.bs-launch.errors.titles.${err.code}`, desc: `notifications.bs-launch.errors.msg.${err.code}`, duration: sToMs(9)}) From 889e792b3c20b58271ab1dacc4adfddcd7677b04 Mon Sep 17 00:00:00 2001 From: silentrald Date: Fri, 24 Jan 2025 20:37:46 +0800 Subject: [PATCH 18/29] [feat] added translations for env parser error --- assets/jsons/translations/de.json | 5 +++++ assets/jsons/translations/es.json | 5 +++++ assets/jsons/translations/fr.json | 5 +++++ assets/jsons/translations/it.json | 5 +++++ assets/jsons/translations/ja.json | 5 +++++ assets/jsons/translations/ko.json | 5 +++++ assets/jsons/translations/ru.json | 5 +++++ assets/jsons/translations/zh-tw.json | 5 +++++ assets/jsons/translations/zh.json | 5 +++++ 9 files changed, 45 insertions(+) diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index c7e7a533..3a27b870 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -28,6 +28,11 @@ "pin": "Anheften", "unpin": "Lösen" }, + "generic": { + "env": { + "parse": "Konnte den Umgebungsvariablen-String nicht richtig parsen." + } + }, "title-bar": { "outdated": "veraltet" }, diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index 61cba828..c5ee39ad 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -28,6 +28,11 @@ "pin": "Fijar", "unpin": "Desfijar" }, + "generic": { + "env": { + "parse": "No se pudo analizar correctamente la cadena de la variable de entorno." + } + }, "title-bar": { "outdated": "obsoleto" }, diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index 09ff7267..e4aeb2bf 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -28,6 +28,11 @@ "pin": "Épingler", "unpin": "Désépingler" }, + "generic": { + "env": { + "parse": "Impossible d'analyser correctement la chaîne de variable d'environnement." + } + }, "title-bar": { "outdated": "obsolète" }, diff --git a/assets/jsons/translations/it.json b/assets/jsons/translations/it.json index 6be54279..cec4cfbc 100644 --- a/assets/jsons/translations/it.json +++ b/assets/jsons/translations/it.json @@ -28,6 +28,11 @@ "pin": "Fissa", "unpin": "Rimuovi Fissaggio" }, + "generic": { + "env": { + "parse": "Impossibile analizzare correttamente la stringa della variabile d'ambiente." + } + }, "title-bar": { "outdated": "obsoleta" }, diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index 356fb81e..465c468a 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -28,6 +28,11 @@ "pin": "固定", "unpin": "固定解除" }, + "generic": { + "env": { + "parse": "環境変数の文字列を正しく解析できませんでした。" + } + }, "title-bar": { "outdated": "時代遅れ" }, diff --git a/assets/jsons/translations/ko.json b/assets/jsons/translations/ko.json index d95476be..beddd618 100644 --- a/assets/jsons/translations/ko.json +++ b/assets/jsons/translations/ko.json @@ -28,6 +28,11 @@ "pin": "고정", "unpin": "고정 해제" }, + "generic": { + "env": { + "parse": "환경 변수 문자열을 제대로 파싱할 수 없습니다." + } + }, "title-bar": { "outdated": "구식" }, diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index d583e90e..43038711 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -28,6 +28,11 @@ "pin": "Закрепить", "unpin": "Открепить" }, + "generic": { + "env": { + "parse": "Не удалось правильно разобрать строку переменной окружения." + } + }, "title-bar": { "outdated": "устаревший" }, diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index bec5592d..87c42b76 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -28,6 +28,11 @@ "pin": "固定", "unpin": "取消固定" }, + "generic": { + "env": { + "parse": "無法正確解析環境變數字串。" + } + }, "title-bar": { "outdated": "過時" }, diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index 93434081..9afbde0f 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -28,6 +28,11 @@ "pin": "固定", "unpin": "取消固定" }, + "generic": { + "env": { + "parse": "无法正确解析环境变量字符串。" + } + }, "title-bar": { "outdated": "过时" }, From 950ca71a0a1a21912d7868358aeb6f7218d04d33 Mon Sep 17 00:00:00 2001 From: MathieuG-P <40181755+Zagrios@users.noreply.github.com> Date: Mon, 20 Jan 2025 22:07:47 +0100 Subject: [PATCH 19/29] [feature] Outdated mod notification --- assets/jsons/translations/de.json | 8 +++ assets/jsons/translations/en.json | 8 +++ assets/jsons/translations/es.json | 8 +++ assets/jsons/translations/fr.json | 8 +++ assets/jsons/translations/it.json | 8 +++ assets/jsons/translations/ja.json | 8 +++ assets/jsons/translations/ko.json | 9 +++ assets/jsons/translations/ru.json | 8 +++ assets/jsons/translations/zh-tw.json | 8 +++ assets/jsons/translations/zh.json | 8 +++ .../services/mods/beat-mods-api.service.ts | 44 +++++++++--- .../services/mods/bs-mods-manager.service.ts | 62 ++++------------ .../slides/mods/mods-slide.component.tsx | 70 ++++++++++--------- .../pages/version-viewer.component.tsx | 50 ++++++++++++- .../services/bs-mods-manager.service.ts | 24 +++++++ 15 files changed, 239 insertions(+), 92 deletions(-) diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index c7e7a533..67881067 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -148,6 +148,14 @@ "all-mods-already-installed": { "title": "Mods bereits installiert", "description": "Alle ausgewählten Mods sind bereits installiert" + }, + "outdated-mods": { + "title": "Veraltetes Mod", + "title-plural": "Veraltete Mods", + "description": "Das Mod {name} ist veraltet. Möchten Sie es aktualisieren?", + "description-plural": "Diese Version enthält {nb} veraltete Mods. Möchten Sie sie aktualisieren?", + "dont-remind-me": "Nicht mehr daran erinnern", + "update": "Aktualisieren" } }, "drop-zone": { diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 8b769c7d..439d65c4 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -148,6 +148,14 @@ "all-mods-already-installed": { "title": "Mods already installed", "description": "All selected mods are already installed" + }, + "outdated-mods": { + "title": "Outdated Mod", + "title-plural": "Outdated Mods", + "description": "{name} mod is outdated. Do you want to update it?", + "description-plural": "This version has {nb} outdated mods. Do you want to update them?", + "dont-remind-me": "Don't remind me", + "update": "Update" } }, "drop-zone": { diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index 61cba828..aca42f3a 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -148,6 +148,14 @@ "all-mods-already-installed": { "title": "Mods ya instalados", "description": "Todos los mods seleccionados ya están instalados" + }, + "outdated-mods": { + "title": "Mod desactualizado", + "title-plural": "Mods desactualizados", + "description": "El mod {name} está desactualizado. ¿Quieres actualizarlo?", + "description-plural": "Esta versión tiene {nb} mods desactualizados. ¿Quieres actualizarlos?", + "dont-remind-me": "No me lo recuerdes", + "update": "Actualizar" } }, "drop-zone": { diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index 09ff7267..b652643a 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -148,6 +148,14 @@ "all-mods-already-installed": { "title": "Mods déjà installées", "description": "Tous les mods séléctionnées sont déjà installées" + }, + "outdated-mods": { + "title": "Mod obsolète", + "title-plural": "Mods obsolètes", + "description": "Le mod {name} est obsolète. Voulez-vous le mettre à jour ?", + "description-plural": "Cette version contient {nb} mods obsolètes. Voulez-vous les mettre à jour ?", + "dont-remind-me": "Ne plus me rappeler", + "update": "Mettre à jour" } }, "drop-zone": { diff --git a/assets/jsons/translations/it.json b/assets/jsons/translations/it.json index 6be54279..80b2ceba 100644 --- a/assets/jsons/translations/it.json +++ b/assets/jsons/translations/it.json @@ -148,6 +148,14 @@ "all-mods-already-installed": { "title": "Mod già installata", "description": "Tutte le mod selezionate sono già installate" + }, + "outdated-mods": { + "title": "Mod obsoleto", + "title-plural": "Mod obsoleti", + "description": "Il mod {name} è obsoleto. Vuoi aggiornarlo?", + "description-plural": "Questa versione ha {nb} mod obsoleti. Vuoi aggiornarli?", + "dont-remind-me": "Non ricordarmelo", + "update": "Aggiorna" } }, "drop-zone": { diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index 356fb81e..b3ef17ba 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -148,6 +148,14 @@ "all-mods-already-installed": { "title": "すでにインストール済みのMOD", "description": "選択したすべてのMODはすでにインストールされています" + }, + "outdated-mods": { + "title": "古いMod", + "title-plural": "古いMod", + "description": "{name} Modが古くなっています。更新しますか?", + "description-plural": "このバージョンには古いModが{nb}個あります。更新しますか?", + "dont-remind-me": "もう知らせない", + "update": "更新する" } }, "drop-zone": { diff --git a/assets/jsons/translations/ko.json b/assets/jsons/translations/ko.json index d95476be..5b84c9df 100644 --- a/assets/jsons/translations/ko.json +++ b/assets/jsons/translations/ko.json @@ -148,7 +148,16 @@ "all-mods-already-installed": { "title": "이미 설치된 모드", "description": "선택한 모든 모드가 이미 설치되었습니다." + }, + "outdated-mods": { + "title": "구식 모드", + "title-plural": "구식 모드", + "description": "{name} 모드가 오래되었습니다. 업데이트하시겠습니까?", + "description-plural": "이 버전에는 {nb}개의 구식 모드가 있습니다. 업데이트하시겠습니까?", + "dont-remind-me": "다시는 알리지 않기", + "update": "업데이트" } + }, "drop-zone": { "text": "모드를 가져오기", diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index d583e90e..0823c3a7 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -148,6 +148,14 @@ "all-mods-already-installed": { "title": "Моды уже установлены", "description": "Все выбранные моды уже установлены" + }, + "outdated-mods": { + "title": "Устаревший мод", + "title-plural": "Устаревшие моды", + "description": "Мод {name} устарел. Хотите его обновить?", + "description-plural": "В этой версии есть {nb} устаревших модов. Хотите их обновить?", + "dont-remind-me": "Не напоминать мне", + "update": "Обновить" } }, "drop-zone": { diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index bec5592d..adbc7502 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -148,6 +148,14 @@ "all-mods-already-installed": { "title": "模組已安裝", "description": "所有選中的模組已經安裝" + }, + "outdated-mods": { + "title": "過時的模組", + "title-plural": "過時的模組", + "description": "{name} 模組已過時。您想更新嗎?", + "description-plural": "此版本有 {nb} 個過時的模組。您想更新它們嗎?", + "dont-remind-me": "不要提醒我", + "update": "更新" } }, "drop-zone": { diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index 93434081..0dc0f347 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -148,6 +148,14 @@ "all-mods-already-installed": { "title": "模组已安装", "description": "所有选中的模组已经安装" + }, + "outdated-mods": { + "title": "过时的模组", + "title-plural": "过时的模组", + "description": "{name} 模组已过时。您想更新吗?", + "description-plural": "此版本有 {nb} 个过时的模组。您想更新它们吗?", + "dont-remind-me": "不要提醒我", + "update": "更新" } }, "drop-zone": { diff --git a/src/main/services/mods/beat-mods-api.service.ts b/src/main/services/mods/beat-mods-api.service.ts index ead1384b..f35ae07d 100644 --- a/src/main/services/mods/beat-mods-api.service.ts +++ b/src/main/services/mods/beat-mods-api.service.ts @@ -3,6 +3,7 @@ import { BbmFullMod, BbmMod, BbmModVersion, BbmPlatform } from "../../../shared/ import { RequestService } from "../request.service"; import { BsStore } from "../../../shared/models/bs-store.enum"; import log from "electron-log" +import { tryit } from "shared/helpers/error.helpers"; export class BeatModsApiService { private static instance: BeatModsApiService; @@ -59,17 +60,42 @@ export class BeatModsApiService { }); } - public getModByHash(hash: string): Promise { - if (this.modsHashCache.has(hash)) { - return Promise.resolve(this.modsHashCache.get(hash)); + public async getModByHash(hashs: T[]): Promise> { + + const getModsFromCache = (hashs: T[]): Record => { + const mods = {} as Record; + + for (const hash of hashs) { + const mod = this.modsHashCache.get(hash); + if(mod){ + mods[hash] = mod; + } + } + + return mods; } - return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`).then(({ data }) => { - this.updateModsHashCache(data?.modVersions ?? []); - return data?.modVersions?.at(0); - }).catch((e): undefined => { - log.error(`Failed to get mod by hash: ${hash}`, e); + const mods = getModsFromCache(hashs); + const missingHashs = hashs.filter(hash => !mods[hash]); + + if(!missingHashs.length){ + return mods; + } + + const url = new URL(`${this.MODS_REPO_API_URL}/hashlookup`); + missingHashs.forEach(hash => url.searchParams.append("hash", hash)); + + const res = await tryit(() => this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(url.toString())); + + if(res.error){ + log.error(`Failed to get mod by hashes`, res.error); return undefined; - }); + } + + this.updateModsHashCache(res.result?.data?.modVersions ?? []); + + const missingMods = getModsFromCache(missingHashs); + + return {...mods, ...missingMods}; } } diff --git a/src/main/services/mods/bs-mods-manager.service.ts b/src/main/services/mods/bs-mods-manager.service.ts index 4121cf31..993edb04 100644 --- a/src/main/services/mods/bs-mods-manager.service.ts +++ b/src/main/services/mods/bs-mods-manager.service.ts @@ -19,6 +19,7 @@ import crypto from "crypto"; import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class"; import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers"; import { BbmFullMod, BbmModVersion, ExternalMod } from "../../../shared/models/mods/mod.interface"; +import { Stats } from "fs"; export class BsModsManagerService { private static instance: BsModsManagerService; @@ -28,8 +29,6 @@ export class BsModsManagerService { private readonly linuxService: LinuxService; private readonly requestService: RequestService; - private manifestMatches: BbmModVersion[]; - public static getInstance(): BsModsManagerService { if (!BsModsManagerService.instance) { BsModsManagerService.instance = new BsModsManagerService(); @@ -44,14 +43,9 @@ export class BsModsManagerService { this.requestService = RequestService.getInstance(); } - private async getModFromHash(hash: string): Promise { - const mod = await this.beatModsApi.getModByHash(hash); - - if(mod?.contentHashes?.some(content => content.path.includes("IPA.exe"))){ - return undefined; - } - - return mod; + private async getModsFromHashes(hashes: string[]): Promise { + const mods = await this.beatModsApi.getModByHash(hashes) + return Object.values(mods).filter(mod => !mod.contentHashes.some(content => content.path.includes("IPA"))); } @@ -63,40 +57,16 @@ export class BsModsManagerService { return []; } - const files = await recursiveReadDir(modsPath); + const ignoreFunc = (file: string, stats: Stats): boolean => { + if(!stats.isFile()) { return true; } + const ext = path.extname(file); + return !(ext === ".dll" || ext === ".exe" || ext === ".manifest"); + } - const promises = files.map(async filePath => { - const ext = path.extname(filePath); + const files = await recursiveReadDir(modsPath, [ignoreFunc]); + const hashes = await Promise.all(files.map(file => md5File(file))); - if (ext !== ".dll" && ext !== ".exe" && ext !== ".manifest") { - return undefined; - } - const hash = await md5File(filePath); - const mod = await this.getModFromHash(hash); - - if (!mod) { - return undefined; - } - - if (ext === ".manifest") { - this.manifestMatches.push(mod); - return undefined; - } - - if (filePath.toLowerCase().includes("libs")) { - const manifestIndex = this.manifestMatches.findIndex(m => m.id === mod.id); - - if (manifestIndex < 0) { - return undefined; - } - - this.manifestMatches.splice(manifestIndex, 1); - } - - return mod; - }); - - const mods = await Promise.all(promises); + const mods = await this.getModsFromHashes(hashes); return mods.filter(Boolean); } @@ -107,7 +77,7 @@ export class BsModsManagerService { return undefined; } const injectorMd5 = await md5File(injectorPath); - return this.beatModsApi.getModByHash(injectorMd5); + return (await this.beatModsApi.getModByHash([injectorMd5]))[injectorMd5]; } private async downloadZip(zipUrl: string): Promise { @@ -338,12 +308,10 @@ export class BsModsManagerService { } public async getInstalledMods(version: BSVersion): Promise { - this.manifestMatches = []; - const bsipa = await this.getBsipaInstalled(version); - const pluginsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.PLUGINS), this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING)]); - const libsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.LIBS), this.getModsInDir(version, ModsInstallFolder.LIBS_PENDING)]); + const pluginsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING), this.getModsInDir(version, ModsInstallFolder.PLUGINS)]); + const libsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.LIBS_PENDING), this.getModsInDir(version, ModsInstallFolder.LIBS)]); const dirMods = pluginsMods.flat().concat(libsMods.flat()); diff --git a/src/renderer/components/version-viewer/slides/mods/mods-slide.component.tsx b/src/renderer/components/version-viewer/slides/mods/mods-slide.component.tsx index 97cfbf71..97854b4d 100644 --- a/src/renderer/components/version-viewer/slides/mods/mods-slide.component.tsx +++ b/src/renderer/components/version-viewer/slides/mods/mods-slide.component.tsx @@ -1,7 +1,7 @@ -import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react"; +import { forwardRef, ReactNode, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react"; import { BsModsManagerService } from "renderer/services/bs-mods-manager.service"; import { BSVersion } from "shared/bs-version.interface"; -import { BbmCategories, BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface"; +import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface"; import { ModsGrid } from "./mods-grid.component"; import { ConfigurationService } from "renderer/services/configuration.service"; import { BsmButton } from "renderer/components/shared/bsm-button.component"; @@ -19,11 +19,17 @@ 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"; import Tippy from "@tippyjs/react"; import { ProgressBarService } from "renderer/services/progress-bar.service"; +import { Dropzone } from "renderer/components/shared/dropzone.component"; -export function ModsSlide({ version, isActive, onDisclamerDecline }: { version: BSVersion; isActive?: boolean, onDisclamerDecline: () => void }) { +export type ModsSlideRef = { + loadMods: () => Promise; +} + +type Props = { version: BSVersion; isActive?: boolean, onDisclamerDecline: () => void }; + +export const ModsSlide = forwardRef(({ version, isActive, onDisclamerDecline }, forwaredRef) => { const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer"; const { text: t } = useTranslationV2(); @@ -100,14 +106,7 @@ export function ModsSlide({ version, isActive, onDisclamerDecline }: { version: return Array.from(collectedDependencies); }; - const installMods = (reinstallAll: boolean): void => { - - setReinstallAllMods(false); - - if (installing) { - return; - } - + const getModsToInstall = (reinstallAll?: boolean): BbmFullMod[] => { let modsToInstall = [ ...modsSelected, ...getAllDependencies(modsSelected, Array.from(modsAvailable.values()).flat()) @@ -127,18 +126,30 @@ export function ModsSlide({ version, isActive, onDisclamerDecline }: { version: set.delete(null); set.delete(undefined); - modsToInstall = Array.from(set); // Remove duplicates + return Array.from(set); // Remove duplicates + } + + const installMods = (reinstallAll: boolean): Promise => { + + setReinstallAllMods(false); + + if (installing) { + return Promise.resolve(); + } + + const modsToInstall = getModsToInstall(reinstallAll); if (!modsToInstall.length) { notification.notifyInfo({ title: "pages.version-viewer.mods.notifications.all-mods-already-installed.title", desc: "pages.version-viewer.mods.notifications.all-mods-already-installed.description" }); - loadMods(); - return; + return loadMods(); } setInstalling(() => true) - lastValueFrom(modsManager.installMods(modsToInstall, version)).then(() => { - loadMods(); - }).catch(noop).finally(() => setInstalling(() => false)); + return lastValueFrom(modsManager.installMods(modsToInstall, version)).then(() => ( + loadMods() + )).catch(noop).finally(() => ( + setInstalling(() => false) + )); }; const importMods = (files: string[]): void => { @@ -179,26 +190,19 @@ export function ModsSlide({ version, isActive, onDisclamerDecline }: { version: return Promise.resolve(); } - const promise = async (): Promise<[BbmFullMod[], BbmModVersion[]]> => { - const available = await lastValueFrom(modsManager.getAvailableMods(version)); - const installed = await lastValueFrom(modsManager.getInstalledMods(version)); - return [available, installed]; - } - - return promise().then(([available, installed]) => { + return modsManager.getVersionModsState(version).then(({ available, installed }) => { const defaultMods = installed?.length ? [] : available.filter(m => m.mod.category === BbmCategories.Core || m.mod.category === BbmCategories.Essential); setModsAvailable(() => modsToCategoryMap(available)); - const installedMods: BbmFullMod[] = installed.map(version => { - const mod = available.find(m => m.mod.id === version.modId); - return mod ? { ...mod, version } : null; - }).filter(mod => mod); - - setModsSelected(available.filter(m => m.mod.category === BbmCategories.Core || defaultMods.some(d => m.mod.name.toLowerCase() === d.mod.name.toLowerCase()) || installedMods.some(i => m.mod.id === i.mod.id))); - setModsInstalled(modsToCategoryMap(installedMods)); + setModsSelected(available.filter(m => m.mod.category === BbmCategories.Core || defaultMods.some(d => m.mod.name.toLowerCase() === d.mod.name.toLowerCase()) || installed.some(i => m.mod.id === i.mod.id))); + setModsInstalled(modsToCategoryMap(installed)); }); }; + useImperativeHandle(forwaredRef, () => ({ + loadMods + }), [version]); + useEffect(() => { if(!isActive || !isOnline){ @@ -325,7 +329,7 @@ export function ModsSlide({ version, isActive, onDisclamerDecline }: { version: ); -} +}); function ModStatus({ text, image, spin = false, children }: { text: string; image: string; spin?: boolean, children?: ReactNode}) { const t = useTranslation(); diff --git a/src/renderer/pages/version-viewer.component.tsx b/src/renderer/pages/version-viewer.component.tsx index d79f9d67..601e3063 100644 --- a/src/renderer/pages/version-viewer.component.tsx +++ b/src/renderer/pages/version-viewer.component.tsx @@ -1,5 +1,5 @@ import { BSVersion } from "shared/bs-version.interface"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { Navigate, useLocation, useNavigate } from "react-router-dom"; import { TabNavBar } from "renderer/components/shared/tab-nav-bar.component"; import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component"; @@ -10,7 +10,7 @@ import { ModalExitCode, ModalService } from "../services/modale.service"; import DefautVersionImage from "../../../assets/images/default-version-img.jpg"; import { IpcService } from "renderer/services/ipc.service"; import { LaunchSlide } from "renderer/components/version-viewer/slides/launch/launch-slide.component"; -import { ModsSlide } from "renderer/components/version-viewer/slides/mods/mods-slide.component"; +import { ModsSlide, ModsSlideRef } from "renderer/components/version-viewer/slides/mods/mods-slide.component"; import { UninstallModal } from "renderer/components/modal/modal-types/uninstall-modal.component"; import { MapsPlaylistsPanel } from "renderer/components/maps-playlists-panel/maps-playlists-panel.component"; import { ShareFoldersModal } from "renderer/components/modal/modal-types/share-folders-modal.component"; @@ -25,12 +25,18 @@ import { useOnUpdate } from "renderer/hooks/use-on-update.hook"; import { safeLt } from "shared/helpers/semver.helpers"; import { ConfigurationService } from "renderer/services/configuration.service"; import { logRenderError } from "renderer"; +import { BsModsManagerService } from "renderer/services/bs-mods-manager.service"; +import { noop } from "shared/helpers/function.helpers"; +import { useTranslationV2 } from "renderer/hooks/use-translation.hook"; export function VersionViewer() { + const { text: t } = useTranslationV2(); + const bsUninstallerService = useService(BSUninstallerService); const bsVersionManagerService = useService(BSVersionManagerService); const modalService = useService(ModalService); + const modsService = useService(BsModsManagerService); const bsDownloader = useService(BsDownloaderService); const ipcService = useService(IpcService); const bsLauncher = useService(BSLauncherService); @@ -40,10 +46,12 @@ export function VersionViewer() { const { state, pathname: url } = useLocation() as { state: BSVersion; pathname: string }; const navigate = useNavigate(); const [currentTabIndex, setCurrentTabIndex] = useState(0); + const modsSlideRef = useRef(null); useOnUpdate(() => { checkIsVersionOutaded(); + checkOutdatedMods(); }, [state]); @@ -70,6 +78,42 @@ export function VersionViewer() { } } + const checkOutdatedMods = async () => { + + if(config.get("not-show-outdated-mods-notification")){ + return; + } + + const { available: availableMods, installed: installedMods } = await modsService.getVersionModsState(state); + + const modsToUpdate = availableMods.filter(availableMod => { + const installedMod = installedMods.find(installedMod => installedMod.mod.id === availableMod.mod.id); + return installedMod && safeLt(installedMod.version.modVersion, availableMod.version.modVersion); + }); + + if(!modsToUpdate.length){ + return; + } + + const choice = await notification.notifyInfo({ + title: modsToUpdate.length > 1 ? t("pages.version-viewer.mods.notifications.outdated-mods.title-plural") : t("pages.version-viewer.mods.notifications.outdated-mods.title"), + desc: modsToUpdate.length > 1 ? t("pages.version-viewer.mods.notifications.outdated-mods.description-plural", { nb: `${modsToUpdate.length}` }) : t("pages.version-viewer.mods.notifications.outdated-mods.description", { name: `${modsToUpdate[0].mod.name}` }), + duration: 9000, + actions: [ + { id: "0", title: "pages.version-viewer.mods.notifications.outdated-mods.dont-remind-me", cancel: true }, + { id: "1", title: "pages.version-viewer.mods.notifications.outdated-mods.update" } + ] + }); + + if(choice === "1"){ + lastValueFrom(modsService.installMods(modsToUpdate, state)).then(() => { + modsSlideRef?.current?.loadMods?.(); + }).catch(noop); + } else if(choice === "0"){ + config.set("not-show-outdated-mods-notification", true); + } + } + const navigateToVersion = (version?: BSVersion) => { if (!version) { return navigate("/available-versions"); @@ -153,7 +197,7 @@ export function VersionViewer() {
setCurrentTabIndex(() => 3)} />
- + { + const [available, installed]: [BbmFullMod[], BbmModVersion[]] = await (async () => { + const available = await lastValueFrom(this.getAvailableMods(version)); + const installed = await lastValueFrom(this.getInstalledMods(version)); + return [available ?? [], installed ?? []] as [BbmFullMod[], BbmModVersion[]]; // Make TS happy + })().catch(e => { + logRenderError(e); + return [[], []] as [BbmFullMod[], BbmModVersion[]]; // Make TS happy + }) + + const installedMods: BbmFullMod[] = installed.reduce((acc, installedMod) => { + const mod = available.find(m => m.mod.id === installedMod.modId); + + if(mod){ + acc.push({ ...mod, version: installedMod } as BbmFullMod); + } + + return acc; + }, []); + + return { available: (available ?? []), installed: (installedMods ?? []) }; + } + } From 5466ad61c35fc0b634877c683beb67ca3edc0012 Mon Sep 17 00:00:00 2001 From: Zagrios <40181755+Zagrios@users.noreply.github.com> Date: Fri, 24 Jan 2025 22:42:24 +0100 Subject: [PATCH 20/29] Ensure file size consistency during folder content movement --- src/__tests__/unit/fs.helpers.test.ts | 92 +++++++++++++++++++++++++++ src/main/helpers/fs.helpers.ts | 44 +++++++++++++ src/shared/helpers/error.helpers.ts | 4 +- 3 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/__tests__/unit/fs.helpers.test.ts diff --git a/src/__tests__/unit/fs.helpers.test.ts b/src/__tests__/unit/fs.helpers.test.ts new file mode 100644 index 00000000..906e353a --- /dev/null +++ b/src/__tests__/unit/fs.helpers.test.ts @@ -0,0 +1,92 @@ +import { mkdir, pathExistsSync, rm, writeFile } from "fs-extra"; +import { getSize } from "main/helpers/fs.helpers"; +import path from "path"; + +const TEST_FOLDER = path.resolve(__dirname, "..", "assets", "fs"); + +describe("Test fs.helpers getSize", () => { + + beforeEach(async () => { + if (pathExistsSync(TEST_FOLDER)) { + await rm(TEST_FOLDER, { recursive: true, force: true }); + } + await mkdir(TEST_FOLDER); + }); + + afterEach(async () => { + await rm(TEST_FOLDER, { recursive: true, force: true }); + }); + + it("should return 0 for empty folder", async () => { + const size = await getSize(TEST_FOLDER); + expect(size).toBe(0); + }); + + it("should throw error for non-existing folder", async () => { + await expect(getSize(`${TEST_FOLDER}1`)).rejects.toThrow(); + }); + + it("should return the total size of files in the directory", async () => { + const filePath = path.join(TEST_FOLDER, "testFile.bin"); + const buffer = Buffer.alloc(10); + + await writeFile(filePath, buffer); + + const size = await getSize(TEST_FOLDER); + expect(size).toBe(10); + }); + + it("should include the sizes of all files in the directory", async () => { + const filePath1 = path.join(TEST_FOLDER, "testFile1.bin"); + const filePath2 = path.join(TEST_FOLDER, "testFile2.bin"); + const buffer = Buffer.alloc(10); + + await writeFile(filePath1, buffer); + await writeFile(filePath2, buffer); + + const size = await getSize(TEST_FOLDER); + expect(size).toBe(20); + }); + + it("should include the sizes of files in nested directories", async () => { + const subFolder = path.join(TEST_FOLDER, "subFolder"); + const filePath1 = path.join(TEST_FOLDER, "testFile1.bin"); + const filePath2 = path.join(subFolder, "testFile2.bin"); + const buffer = Buffer.alloc(10); + + await mkdir(subFolder); + + await writeFile(filePath1, buffer); + await writeFile(filePath2, buffer); + + const size = await getSize(TEST_FOLDER); + expect(size).toBe(20); + }); + + it("should not include files beyond the default depth limit", async () => { + const subFolder = path.join(TEST_FOLDER, "1", "2", "3", "4", "5"); + const filePath = path.join(subFolder, "testFile.bin"); + const buffer = Buffer.alloc(10); + + await mkdir(subFolder, { recursive: true }); + await writeFile(filePath, buffer); + + const size = await getSize(TEST_FOLDER); + expect(size).toBe(0); + }); + + it("should include files within the specified depth limit", async () => { + const subFolder = path.join(TEST_FOLDER, "1", "2", "3", "4", "5"); + const filePath = path.join(subFolder, "testFile.bin"); + const filePath2 = path.join(TEST_FOLDER, "1", "2", "testFile2.bin"); + const buffer = Buffer.alloc(10); + + await mkdir(subFolder, { recursive: true }); + await writeFile(filePath, buffer); + await writeFile(filePath2, buffer); + + const size = await getSize(TEST_FOLDER, 6); + expect(size).toBe(20); + }); + +}); diff --git a/src/main/helpers/fs.helpers.ts b/src/main/helpers/fs.helpers.ts index d8111de9..5692d32f 100644 --- a/src/main/helpers/fs.helpers.ts +++ b/src/main/helpers/fs.helpers.ts @@ -81,6 +81,7 @@ export async function getFilesInFolder(folderPath: string): Promise { } export function moveFolderContent(src: string, dest: string, option?: MoveOptions): Observable { + log.info(`(moveFolderContent) Moving ${src} to ${dest}`); const progress: Progression = { current: 0, total: 0 }; return new Observable(subscriber => { subscriber.next(progress); @@ -104,7 +105,17 @@ export function moveFolderContent(src: string, dest: string, option?: MoveOption const allChildsAlreadyExist = srcChilds.every(child => pathExistsSync(path.join(destFullPath, child))); if(file.isFile() || !allChildsAlreadyExist){ + const prevSize = await getSize(srcFullPath); await move(srcFullPath, destFullPath, option); + const afterSize = await getSize(destFullPath); + + // The size after moving should be the same or greater than the size before moving but never less + if(afterSize < prevSize){ + throw new CustomError(`File size mismath. before: ${prevSize}, after: ${afterSize} (${srcFullPath})`, "FILE_SIZE_MISMATCH"); + } + + } else { + log.info(`Skipping ${srcFullPath} to ${destFullPath}, all child already exist in destination`); } progress.current++; @@ -274,6 +285,39 @@ export function getUniqueFileNamePath(filePath: string): string { return path.join(dir, newFileName); } +/** + * @throws {Error} Can throw file system errors + */ +export async function getSize(targetPath: string, maxDepth = 5): Promise { + const visited = new Set(); + + const computeSize = async (currentPath: string, depth: number): Promise => { + if (visited.has(currentPath)){ + return 0; + } + + visited.add(currentPath); + + const stats = await stat(currentPath); + + if (stats.isFile()) { + return stats.size; + } + + if (!stats.isDirectory() || depth >= maxDepth) { + return 0; + } + + const entries = await readdir(currentPath); + const sizes = await Promise.all( + entries.map((entry) => computeSize(path.join(currentPath, entry), depth + 1)) + ); + return sizes.reduce((acc, cur) => acc + cur, 0); + }; + + return computeSize(targetPath, 0); +} + export interface Progression { total: number; current: number; diff --git a/src/shared/helpers/error.helpers.ts b/src/shared/helpers/error.helpers.ts index 33f71fce..a8fc2543 100644 --- a/src/shared/helpers/error.helpers.ts +++ b/src/shared/helpers/error.helpers.ts @@ -8,8 +8,8 @@ export function tryit(func: () => Return): TryitReturn { if(isPromise(result)){ return result - .then((value) => ({ error: null, result: value })) - .catch((err) => ({ error: err instanceof Error ? err : new Error(`${err}`), result: null })) as Return extends Promise + .then((value) => ({ error: null as null, result: value })) + .catch((err) => ({ error: err instanceof Error ? err : new Error(`${err}`), result: null as null })) as Return extends Promise ? Promise<{error: Error, result: undefined} | {error: undefined, result: Awaited}> : {error: Error, result: undefined} | {error: undefined, result: Return}; } From 1c2321b43820ca122b3c6329414c9f2f450d9fdd Mon Sep 17 00:00:00 2001 From: silentrald Date: Tue, 28 Jan 2025 13:41:16 +0800 Subject: [PATCH 21/29] [feat] support %command% env args to windows and oculus * refactored additionalArgs to command and change its type to string instead of string array --- assets/jsons/translations/de.json | 2 +- assets/jsons/translations/en.json | 2 +- assets/jsons/translations/es.json | 2 +- assets/jsons/translations/fr.json | 2 +- assets/jsons/translations/it.json | 2 +- assets/jsons/translations/ja.json | 2 +- assets/jsons/translations/ko.json | 2 +- assets/jsons/translations/ru.json | 2 +- assets/jsons/translations/zh-tw.json | 2 +- assets/jsons/translations/zh.json | 2 +- .../bs-launcher/abstract-launcher.service.ts | 35 +++++++++++++++++-- .../bs-launcher/bs-launcher.service.ts | 10 ++---- .../bs-launcher/oculus-launcher.service.ts | 16 +++++---- .../bs-launcher/steam-launcher.service.ts | 4 ++- src/main/services/linux.service.ts | 20 ----------- ...create-launch-shortcut-modal.component.tsx | 12 +++---- .../slides/launch/launch-slide.component.tsx | 16 ++++----- src/renderer/services/bs-launcher.service.ts | 12 ++----- .../bs-launch/launch-option.interface.ts | 2 +- 19 files changed, 74 insertions(+), 73 deletions(-) diff --git a/assets/jsons/translations/de.json b/assets/jsons/translations/de.json index 3a27b870..b5b70e28 100644 --- a/assets/jsons/translations/de.json +++ b/assets/jsons/translations/de.json @@ -56,7 +56,7 @@ "outdated-tippy": "Diese Version ist veraltet, und einige Mods oder Funktionen funktionieren möglicherweise nicht wie erwartet. Es wird empfohlen, die empfohlene Version ({recommendedVersion}) von Beat Saber zu verwenden, um die neuesten Funktionen und Fehlerbehebungen zu genießen.", "advanced-launch": { "button": "Startoptionen", - "placeholder": "Weitere Argumente, bspw: --revert; --nowait" + "placeholder": "Startoptionen, bspw: KEY=VALUE %command% fpfc" }, "skipsteam": "Steam überspringen", "skipsteam-description": "Verhindert, dass Steam automatisch mit Beat Saber geöffnet wird. Aktivieren Sie dies, wenn Sie eine andere VR-Laufzeit wie WiVRn oder Monado verwenden, mit der SteamVR interferieren könnte.", diff --git a/assets/jsons/translations/en.json b/assets/jsons/translations/en.json index 557137b8..f1f0daf4 100644 --- a/assets/jsons/translations/en.json +++ b/assets/jsons/translations/en.json @@ -56,7 +56,7 @@ "outdated-tippy": "This version is outdated, and some mods or features may no longer work as expected. Prefer using the recommended version ({recommendedVersion}) of Beat Saber to enjoy the latest features and bugfixes.", "advanced-launch": { "button": "Launch options", - "placeholder": "Additional arguments ex: --revert; --nowait" + "placeholder": "Launch options ex: KEY=VALUE %command% fpfc" }, "skipsteam": "Skip Steam", "skipsteam-description": "Stops Steam from opening automatically with Beat Saber, enable if you are using a different VR runtime like WiVRn or Monado that SteamVR may interfere with.", diff --git a/assets/jsons/translations/es.json b/assets/jsons/translations/es.json index c5ee39ad..3b902458 100644 --- a/assets/jsons/translations/es.json +++ b/assets/jsons/translations/es.json @@ -56,7 +56,7 @@ "outdated-tippy": "Esta versión está desactualizada, y algunos mods o funciones pueden no funcionar como se espera. Es mejor usar la versión recomendada ({recommendedVersion}) de Beat Saber para disfrutar de las últimas características y correcciones.", "advanced-launch": { "button": "Opciones de lanzamiento", - "placeholder": "Argumentos adicionales ej: --revert; --no-wait" + "placeholder": "Opciones de lanzamiento ej: KEY=VALUE %command% fpfc" }, "skipsteam": "Saltar Steam", "skipsteam-description": "Evita que Steam se abra automáticamente con Beat Saber, habilita esto si estás usando un runtime de VR diferente como WiVRn o Monado que SteamVR podría interferir.", diff --git a/assets/jsons/translations/fr.json b/assets/jsons/translations/fr.json index e4aeb2bf..2f1fe96b 100644 --- a/assets/jsons/translations/fr.json +++ b/assets/jsons/translations/fr.json @@ -56,7 +56,7 @@ "outdated-tippy": "Cette version est obsolète et certains mods ou fonctionnalités peuvent ne plus fonctionner comme prévu. Préférez utiliser la version recommandée ({recommendedVersion}) de Beat Saber pour profiter des dernières fonctionnalités et correctifs.", "advanced-launch": { "button": "Options de lancement", - "placeholder": "Arguments supplémentaires ex: --revert; --nowait" + "placeholder": "Options de lancement ex: KEY=VALUE %command% fpfc" }, "skipsteam": "Ignorer Steam", "skipsteam-description": "Empêche Steam de s'ouvrir automatiquement avec Beat Saber, activez-le si vous utilisez un autre runtime VR comme WiVRn ou Monado avec lequel SteamVR pourrait interférer.", diff --git a/assets/jsons/translations/it.json b/assets/jsons/translations/it.json index cec4cfbc..bc497b23 100644 --- a/assets/jsons/translations/it.json +++ b/assets/jsons/translations/it.json @@ -56,7 +56,7 @@ "outdated-tippy": "Questa versione è obsoleta, e alcune mod o funzioni non potrebbero più funzionare correttamente. Consigliamo di usare la versione raccomandata ({recommendedVersion}) di Beat Saber per godere delle ultime funzioni e bugfix.", "advanced-launch": { "button": "Opzioni di Lancio", - "placeholder": "Argomenti aggiuntivi es: --revert; --nowait" + "placeholder": "Opzioni di Lancio es: KEY=VALUE %command% fpfc" }, "skipsteam": "Salta Steam", "skipsteam-description": "Ferma Steam da aprirsi automaticamente con Beat Saber, abilitalo se stai usando un VR runtime differente come WiVRn o Monado con cui SteamVR potrebbe interferire.", diff --git a/assets/jsons/translations/ja.json b/assets/jsons/translations/ja.json index 465c468a..fed708d3 100644 --- a/assets/jsons/translations/ja.json +++ b/assets/jsons/translations/ja.json @@ -56,7 +56,7 @@ "outdated-tippy": "このバージョンは古いため、一部のMODや機能が期待通りに動作しない可能性があります。最新の機能やバグ修正を楽しむには、推奨バージョン ({recommendedVersion}) のBeat Saberを使用することをお勧めします。", "advanced-launch": { "button": "起動オプション", - "placeholder": "追加引数 例:--revert; --nowait" + "placeholder": "起動オプション 例:KEY=VALUE %command% fpfc" }, "skipsteam": "Steamをスキップ", "skipsteam-description": "Beat Saberと一緒にSteamが自動的に開くのを防ぎます。SteamVRが干渉する可能性のあるWiVRnやMonadoなど、別のVRランタイムを使用している場合は有効にしてください。", diff --git a/assets/jsons/translations/ko.json b/assets/jsons/translations/ko.json index beddd618..e44135c8 100644 --- a/assets/jsons/translations/ko.json +++ b/assets/jsons/translations/ko.json @@ -56,7 +56,7 @@ "outdated-tippy": "이 버전은 오래되어 일부 모드나 기능이 예상대로 작동하지 않을 수 있습니다. 최신 기능과 버그 수정을 사용하려면 권장 버전({recommendedVersion})의 Beat Saber를 사용하는 것이 좋습니다.", "advanced-launch": { "button": "실행 옵션", - "placeholder": "추가 인수 예: --revert; --nowait" + "placeholder": "실행 옵션 예: KEY=VALUE %command% fpfc" }, "skipsteam": "Steam 실행 건너뛰기", "skipsteam-description": "Beat Saber와 함께 Steam이 자동으로 열리는 것을 방지합니다. SteamVR이 간섭할 수 있는 WiVRn 또는 Monado와 같은 다른 VR 런타임을 사용하는 경우 활성화하세요.", diff --git a/assets/jsons/translations/ru.json b/assets/jsons/translations/ru.json index 43038711..03a88032 100644 --- a/assets/jsons/translations/ru.json +++ b/assets/jsons/translations/ru.json @@ -56,7 +56,7 @@ "outdated-tippy": "Эта версия устарела, и некоторые моды или функции могут работать не так, как ожидалось. Рекомендуется использовать рекомендуемую версию ({recommendedVersion}) Beat Saber, чтобы воспользоваться последними функциями и исправлениями ошибок.", "advanced-launch": { "button": "Параметры запуска", - "placeholder": "Параметры запуска, например: --revert; --nowait" + "placeholder": "Параметры запуска, например: KEY=VALUE %command% fpfc" }, "skipsteam": "Пропустить Steam", "skipsteam-description": "Предотвращает автоматическое открытие Steam с Beat Saber, включите, если вы используете другую VR-среду, такую как WiVRn или Monado, с которой SteamVR может мешать.", diff --git a/assets/jsons/translations/zh-tw.json b/assets/jsons/translations/zh-tw.json index 87c42b76..924889da 100644 --- a/assets/jsons/translations/zh-tw.json +++ b/assets/jsons/translations/zh-tw.json @@ -56,7 +56,7 @@ "outdated-tippy": "此版本已過時,某些模組或功能可能無法按預期運作。建議使用推薦版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修復。", "advanced-launch": { "button": "啟動選項", - "placeholder": "額外啟動參數,例如: --revert; --nowait" + "placeholder": "啟動選項,例如: KEY=VALUE %command% fpfc" }, "skipsteam": "跳過 Steam", "skipsteam-description": "防止 Steam 與 Beat Saber 自動打開,如果您使用的是其他 VR 執行時,如 WiVRn 或 Monado,SteamVR 可能會干擾,請啟用此選項。", diff --git a/assets/jsons/translations/zh.json b/assets/jsons/translations/zh.json index 9afbde0f..5ca0ef8e 100644 --- a/assets/jsons/translations/zh.json +++ b/assets/jsons/translations/zh.json @@ -56,7 +56,7 @@ "outdated-tippy": "此版本已过时,某些模组或功能可能无法按预期运行。建议使用推荐版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修复。", "advanced-launch": { "button": "启动选项", - "placeholder": "额外启动参数,例如: --revert; --nowait" + "placeholder": "启动选项,例如: KEY=VALUE %command% fpfc" }, "skipsteam": "跳过 Steam", "skipsteam-description": "防止 Steam 与 Beat Saber 自动打开,如果您使用的是其他 VR 运行时,如 WiVRn 或 Monado,SteamVR 可能会干扰,请启用此选项。", diff --git a/src/main/services/bs-launcher/abstract-launcher.service.ts b/src/main/services/bs-launcher/abstract-launcher.service.ts index b61d23b9..0399e3f1 100644 --- a/src/main/services/bs-launcher/abstract-launcher.service.ts +++ b/src/main/services/bs-launcher/abstract-launcher.service.ts @@ -8,6 +8,7 @@ import { LinuxService } from "../linux.service"; import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers"; import { IS_FLATPAK } from "main/constants"; import { LaunchMods } from "shared/models/bs-launch/launch-option.interface"; +import { parseEnvString } from "main/helpers/env.helpers"; export function buildBsLaunchArgs(launchOptions: LaunchOption): string[] { const launchArgs = []; @@ -29,8 +30,8 @@ export function buildBsLaunchArgs(launchOptions: LaunchOption): string[] { launchArgs.push("editor"); } - if (launchOptions.additionalArgs) { - launchArgs.push(...launchOptions.additionalArgs); + if (launchOptions.command) { + launchArgs.push(launchOptions.command); } return Array.from(new Set(launchArgs).values()); @@ -46,6 +47,8 @@ export abstract class AbstractLauncherService { this.localVersions = BSLocalVersionService.getInstance(); } + private readonly COMMAND_FORMAT = "%command%"; + protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): ChildProcessWithoutNullStreams { const spawnOptions: SpawnOptionsWithoutStdio = { detached: true, cwd: path.dirname(bsExePath), ...(options || {}) }; @@ -116,6 +119,34 @@ export abstract class AbstractLauncherService { return { process, exit }; } + + protected injectAdditionalArgsEnvs( + launchOptions: LaunchOption, + env: Record + ) { + if (!launchOptions.command) { + return; + } + + const { command } = launchOptions; + const index = command.indexOf(this.COMMAND_FORMAT); + if (index === -1) { + return; + } + + const envString = command.substring(0, index); + log.info("Parsing env string ", `"${envString}"`) + for (const [ key, value ] of Object.entries(parseEnvString(envString))) { + if (key in env) { + log.warn("Ignoring", `${key}=${value}`, "already set env launch command"); + } else { + log.info("Injecting", `${key}="${value}"`, "to the env launch command"); + } + } + + launchOptions.command = command.substring(index + this.COMMAND_FORMAT.length); + } + } export type SpawnBsProcessOptions = { diff --git a/src/main/services/bs-launcher/bs-launcher.service.ts b/src/main/services/bs-launcher/bs-launcher.service.ts index b03dd0ef..66e4e9e5 100644 --- a/src/main/services/bs-launcher/bs-launcher.service.ts +++ b/src/main/services/bs-launcher/bs-launcher.service.ts @@ -95,10 +95,6 @@ export class BSLauncherService { const params = objectFromEntries(shortcutLink.searchParams.entries()) as ShortcutParams; - if(typeof params.additionalArgs === "string"){ - params.additionalArgs = [params.additionalArgs]; - } - return params; } @@ -123,7 +119,7 @@ export class BSLauncherService { oculus: params.versionOculus === "true", ino: +params.versionIno }, - additionalArgs: params.additionalArgs, + command: params.command, launchMods, }; @@ -141,7 +137,7 @@ export class BSLauncherService { if(launchOptions.launchMods?.includes(LaunchMods.OCULUS)){ res.oculusMode = "true"; } if(launchOptions.launchMods?.includes(LaunchMods.FPFC)){ res.desktopMode = "true"; } if(launchOptions.launchMods?.includes(LaunchMods.DEBUG)){ res.debug = "true"; } - if(launchOptions.additionalArgs){ res.additionalArgs = launchOptions.additionalArgs; } + if(launchOptions.command){ res.command = launchOptions.command; } if(launchOptions.launchMods?.includes(LaunchMods.SKIP_STEAM)){ res.skipSteam = "true"; } if(launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)){ res.protonLogs = "true"; } @@ -287,7 +283,7 @@ type ShortcutParams = { oculusMode?: string; desktopMode?: string; debug?: string; - additionalArgs?: string[]; + command?: string; skipSteam?: string; protonLogs?: string; version: string; diff --git a/src/main/services/bs-launcher/oculus-launcher.service.ts b/src/main/services/bs-launcher/oculus-launcher.service.ts index d92ccef9..6a02c652 100644 --- a/src/main/services/bs-launcher/oculus-launcher.service.ts +++ b/src/main/services/bs-launcher/oculus-launcher.service.ts @@ -1,4 +1,4 @@ -import { Observable, ReplaySubject } from "rxjs"; +import { Observable } from "rxjs"; import { StoreLauncherInterface } from "./store-launcher.interface"; import { BSLaunchError, BSLaunchEvent, BSLaunchEventData, LaunchOption } from "../../../shared/models/bs-launch"; import { OculusService } from "../oculus.service"; @@ -9,7 +9,6 @@ import { pathExists } from "fs-extra"; import { AbstractLauncherService, buildBsLaunchArgs } from "./abstract-launcher.service"; import { isProcessRunning } from "../../helpers/os.helpers"; import { CustomError } from "../../../shared/models/exceptions/custom-error.class"; -import { UtilsService } from "../utils.service"; export class OculusLauncherService extends AbstractLauncherService implements StoreLauncherInterface { @@ -23,14 +22,10 @@ export class OculusLauncherService extends AbstractLauncherService implements St } private readonly oculus: OculusService; - private readonly util: UtilsService; - - private readonly oculusLib$ = new ReplaySubject(); private constructor() { super(); this.oculus = OculusService.getInstance(); - this.util = UtilsService.getInstance(); } public launch(launchOptions: LaunchOption): Observable { @@ -54,10 +49,17 @@ export class OculusLauncherService extends AbstractLauncherService implements St // Make sure Oculus is running await this.oculus.startOculus().catch(err => log.error("Error while starting Oculus", err)); + const env: Record = {}; + this.injectAdditionalArgsEnvs(launchOptions, env); + obs.next({type: BSLaunchEvent.BS_LAUNCHING}); // Launch Beat Saber - const process = this.launchBs(exePath, buildBsLaunchArgs(launchOptions)); + const process = this.launchBs( + exePath, + buildBsLaunchArgs(launchOptions), + { env } + ); return process.exit.catch(err => { throw CustomError.fromError(err, BSLaunchError.BS_EXIT_ERROR); diff --git a/src/main/services/bs-launcher/steam-launcher.service.ts b/src/main/services/bs-launcher/steam-launcher.service.ts index ab8277d6..8b46ff42 100644 --- a/src/main/services/bs-launcher/steam-launcher.service.ts +++ b/src/main/services/bs-launcher/steam-launcher.service.ts @@ -102,7 +102,6 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto await this.restoreSteamVR().catch(log.error); } - const launchArgs = buildBsLaunchArgs(launchOptions); const steamPath = await this.steam.getSteamPath(); const env = { @@ -122,6 +121,9 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto Object.assign(env, linuxSetup.env); } + this.injectAdditionalArgsEnvs(launchOptions, env); + const launchArgs = buildBsLaunchArgs(launchOptions); + obs.next({type: BSLaunchEvent.BS_LAUNCHING}); const spawnOpts = { env, cwd: bsFolderPath }; diff --git a/src/main/services/linux.service.ts b/src/main/services/linux.service.ts index e956148c..5f12896a 100644 --- a/src/main/services/linux.service.ts +++ b/src/main/services/linux.service.ts @@ -10,7 +10,6 @@ import { BsmShellLog, bsmExec } from "main/helpers/os.helpers"; import { LaunchMods } from "shared/models/bs-launch/launch-option.interface"; import { SteamShortcutData } from "shared/models/steam/shortcut.model"; import { buildBsLaunchArgs } from "./bs-launcher/abstract-launcher.service"; -import { parseEnvString } from "main/helpers/env.helpers"; export class LinuxService { private static instance: LinuxService; @@ -25,7 +24,6 @@ export class LinuxService { private readonly installLocationService: InstallationLocationService; private readonly staticConfig: StaticConfigurationService; - private readonly COMMAND_FORMAT = "%command%"; private nixOS: boolean | undefined; private constructor() { @@ -114,24 +112,6 @@ export class LinuxService { envVars.PROTON_LOG_DIR = path.join(bsFolderPath, "Logs"); } - if (launchOptions.additionalArgs) { - const additionalArgs = launchOptions.additionalArgs.join(" "); - const index = additionalArgs.indexOf(this.COMMAND_FORMAT); - if (index > -1) { - const envString = additionalArgs.substring(0, index); - log.info("Parsing env string ", `"${envString}"`) - for (const [ key, value ] of Object.entries(parseEnvString(envString))) { - if (key in envVars) { - log.warn("Ignoring", `${key}=${value}`, "already set env launch command"); - } else { - log.info("Injecting", `${key}="${value}"`, "to the env launch command"); - } - } - } - - launchOptions.additionalArgs = [ additionalArgs.substring(index + this.COMMAND_FORMAT.length) ]; - } - return envVars; } diff --git a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx index 0ec7966e..014c5142 100644 --- a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx @@ -22,8 +22,8 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, const color = useThemeColor("second-color"); const [launchOption, setLaunchOptions] = useState(bsLauncher.getLaunchOptions(data)); - const [advanced, setAdvanced] = useState(!!launchOption.additionalArgs?.length); - const [additionalArgsString, setAdditionalArgsString] = useState(launchOption.additionalArgs?.join("; ") ?? ""); + const [advanced, setAdvanced] = useState(!!launchOption.command?.length); + const [command, setCommand] = useState(launchOption.command || ""); const [steamShortcut, setSteamShortcut] = useState(false); const isSteamVersion = useMemo(() => { @@ -33,9 +33,9 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, const completeModal = () => { if(advanced) { - launchOption.additionalArgs = additionalArgsString.split(";").map(arg => arg.trim()).filter(arg => arg.length); + launchOption.command = command.trim(); } else { - launchOption.additionalArgs = undefined; + launchOption.command = ""; } resolver({exitCode: ModalExitCode.COMPLETED, data: { launchOption, steamShortcut }}); @@ -95,8 +95,8 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, type="text" className="w-full rounded-md text-center outline-none bg-light-main-color-3 dark:bg-main-color-3" placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")} - value={additionalArgsString} - onChange={e => setAdditionalArgsString(e.target.value)} + value={command} + onChange={e => setCommand(e.target.value)} /> diff --git a/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx b/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx index 3f933c89..1eb75078 100644 --- a/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx +++ b/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx @@ -38,7 +38,7 @@ export function LaunchSlide({ version }: Props) { const versions = useService(BSVersionManagerService); const [advancedLaunch, setAdvancedLaunch] = useState(false); - const [additionalArgsString, setAdditionalArgsString] = useState(configService.get("additionnal-args") || ""); + const [command, setCommand] = useState(configService.get("additionnal-args") || ""); const versionDownloading = useObservable(() => bsDownloader.downloadingVersion$); const [activeLaunchMods, setActiveLaunchMods] = useState(configService.get("launch-mods") ?? []); const [pinnedLaunchMods, setPinnedLaunchMods] = useState(configService.get("pinned-launch-mods" as DefaultConfigKey) ?? []); @@ -46,8 +46,8 @@ export function LaunchSlide({ version }: Props) { const versionRunning = useObservable(() => bsLauncherService.versionRunning$); useEffect(() => { - configService.set("additionnal-args", additionalArgsString); - }, [additionalArgsString]); + configService.set("additionnal-args", command); + }, [command]); useEffect(() => { configService.set("pinned-launch-mods", pinnedLaunchMods); @@ -146,12 +146,10 @@ export function LaunchSlide({ version }: Props) { }, [activeLaunchMods, pinnedLaunchMods, version]); const launch = async () => { - const additionalArgs = additionalArgsString?.split(";").map(arg => arg.trim()).filter(arg => arg.length > 0); - const launch$ = bsLauncherService.launch({ version, launchMods: activeLaunchMods, - additionalArgs: advancedLaunch ? additionalArgs : [], + command: advancedLaunch ? command : "", }); return lastValueFrom(launch$).catch(() => {}); @@ -195,9 +193,9 @@ export function LaunchSlide({ version }: Props) {
- + { @@ -207,7 +205,7 @@ export function LaunchSlide({ version }: Props) { />
- +
("not-rewind-backup-oculus"); - } - - private setNotRewindBackupOculus(value: boolean): void{ - this.config.set("not-rewind-backup-oculus", value); - } - - public getLaunchOptions(version: BSVersion): LaunchOption{ + public getLaunchOptions(version: BSVersion): LaunchOption { return { version, launchMods: this.config.get("launch-mods") ?? [], - additionalArgs: (this.config.get("additionnal-args") || "").split(";").map(arg => arg.trim()).filter(arg => arg.length > 0), + command: this.config.get("additionnal-args") || "", } } diff --git a/src/shared/models/bs-launch/launch-option.interface.ts b/src/shared/models/bs-launch/launch-option.interface.ts index 6199fdde..d255e67a 100644 --- a/src/shared/models/bs-launch/launch-option.interface.ts +++ b/src/shared/models/bs-launch/launch-option.interface.ts @@ -14,6 +14,6 @@ export type LaunchMod = typeof LaunchMods[keyof typeof LaunchMods]; export interface LaunchOption { version: BSVersion, launchMods?: LaunchMod[], - additionalArgs?: string[], + command?: string, admin?: boolean } From b94fe0a053118188d51c9c4d4dadceb7c93dcba3 Mon Sep 17 00:00:00 2001 From: silentrald Date: Tue, 28 Jan 2025 20:58:24 +0800 Subject: [PATCH 22/29] [feat] follow launch command string even if its hidden --- .../create-launch-shortcut-modal.component.tsx | 8 +------- .../slides/launch/launch-slide.component.tsx | 6 +++--- src/renderer/services/bs-launcher.service.ts | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx index 014c5142..5c44584d 100644 --- a/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx +++ b/src/renderer/components/modal/modal-types/create-launch-shortcut-modal.component.tsx @@ -31,13 +31,7 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean, }, [data]); const completeModal = () => { - - if(advanced) { - launchOption.command = command.trim(); - } else { - launchOption.command = ""; - } - + launchOption.command = command.trim(); resolver({exitCode: ModalExitCode.COMPLETED, data: { launchOption, steamShortcut }}); } diff --git a/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx b/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx index 1eb75078..472e2086 100644 --- a/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx +++ b/src/renderer/components/version-viewer/slides/launch/launch-slide.component.tsx @@ -38,7 +38,7 @@ export function LaunchSlide({ version }: Props) { const versions = useService(BSVersionManagerService); const [advancedLaunch, setAdvancedLaunch] = useState(false); - const [command, setCommand] = useState(configService.get("additionnal-args") || ""); + const [command, setCommand] = useState(configService.get("launch-command") || ""); const versionDownloading = useObservable(() => bsDownloader.downloadingVersion$); const [activeLaunchMods, setActiveLaunchMods] = useState(configService.get("launch-mods") ?? []); const [pinnedLaunchMods, setPinnedLaunchMods] = useState(configService.get("pinned-launch-mods" as DefaultConfigKey) ?? []); @@ -46,7 +46,7 @@ export function LaunchSlide({ version }: Props) { const versionRunning = useObservable(() => bsLauncherService.versionRunning$); useEffect(() => { - configService.set("additionnal-args", command); + configService.set("launch-command", command); }, [command]); useEffect(() => { @@ -149,7 +149,7 @@ export function LaunchSlide({ version }: Props) { const launch$ = bsLauncherService.launch({ version, launchMods: activeLaunchMods, - command: advancedLaunch ? command : "", + command, }); return lastValueFrom(launch$).catch(() => {}); diff --git a/src/renderer/services/bs-launcher.service.ts b/src/renderer/services/bs-launcher.service.ts index 357399b5..8c630c95 100644 --- a/src/renderer/services/bs-launcher.service.ts +++ b/src/renderer/services/bs-launcher.service.ts @@ -40,7 +40,7 @@ export class BSLauncherService { return { version, launchMods: this.config.get("launch-mods") ?? [], - command: this.config.get("additionnal-args") || "", + command: this.config.get("launch-command") || "", } } From 812600a3eca74cf73b59bf8f74c68346af4d081e Mon Sep 17 00:00:00 2001 From: Zagrios <40181755+Zagrios@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:23:21 +0100 Subject: [PATCH 23/29] Fix brazil flag icon --- .../svgs/flags/brasil-icon.component.tsx | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/renderer/components/svgs/flags/brasil-icon.component.tsx b/src/renderer/components/svgs/flags/brasil-icon.component.tsx index b6027db8..5537fbcf 100644 --- a/src/renderer/components/svgs/flags/brasil-icon.component.tsx +++ b/src/renderer/components/svgs/flags/brasil-icon.component.tsx @@ -1,11 +1,14 @@ -import { CSSProperties } from "react"; +import { createSvgIcon } from "../svg-icon.type"; -export function BrazilIcon(props: { className?: string; style?: CSSProperties }) { +export const BrazilIcon = createSvgIcon((props, ref) => { return ( - - - - + + + + + + + ); -}; +}); From 47158a264a466ecaeba2375c59c864b302a0b8a5 Mon Sep 17 00:00:00 2001 From: Zagrios <40181755+Zagrios@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:30:14 +0100 Subject: [PATCH 24/29] Add missing translations --- assets/jsons/translations/pt-br.json | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/assets/jsons/translations/pt-br.json b/assets/jsons/translations/pt-br.json index b8e783be..ba034dcd 100644 --- a/assets/jsons/translations/pt-br.json +++ b/assets/jsons/translations/pt-br.json @@ -28,6 +28,11 @@ "pin": "Fixar", "unpin": "Desafixar" }, + "generic": { + "env": { + "parse": "Não foi possível interpretar corretamente a string da variável de ambiente." + } + }, "title-bar": { "outdated": "Desatualizado" }, @@ -51,7 +56,7 @@ "outdated-tippy": "Essa versão está desatualizada, e alguns mods ou funções podem não funcionar mais como esperado. Prefira utilizar a versão recomendada ({recommendedVersion}) de Beat Saber para aproveitar todas as últimas novidades e correções de erros.", "advanced-launch": { "button": "Opções de Inicialização", - "placeholder": "Argumentos adicionais ex: --revert; --nowait" + "placeholder": "Opções de lançamento, ex: KEY=VALUE %command% fpfc" }, "skipsteam": "Pular Steam", "skipsteam-description": "Faz com que a Steam pare de abrir automaticamente com Beat Saber, ative se você está utilizando algum outro executável de VR como WiVRn ou Monado, que pode interferir com o SteamVR.", @@ -148,6 +153,14 @@ "all-mods-already-installed": { "title": "Mods já instalados", "description": "Todos os mods selecionados já estão instalados" + }, + "outdated-mods": { + "title": "Mod Desatualizado", + "title-plural": "Mods Desatualizados", + "description": "O mod {name} está desatualizado. Deseja atualizá-lo?", + "description-plural": "Esta versão possui {nb} mods desatualizados. Deseja atualizá-los?", + "dont-remind-me": "Não me lembre", + "update": "Atualizar" } }, "drop-zone": { From fd299e126778dcbdf42d65724afcbf3ad64e96a2 Mon Sep 17 00:00:00 2001 From: Zagrios <40181755+Zagrios@users.noreply.github.com> Date: Tue, 28 Jan 2025 14:54:42 +0100 Subject: [PATCH 25/29] fix z index --- .../playlist-details-template.component.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/playlist-details-template.component.tsx b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/playlist-details-template.component.tsx index cd8066dc..b9b31287 100644 --- a/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/playlist-details-template.component.tsx +++ b/src/renderer/components/modal/modal-types/playlist/playlist-details-modal/playlist-details-template.component.tsx @@ -48,7 +48,7 @@ export function PlaylistDetailsTemplate({title, imagebase64, imageUrl, author, d return (
- From ad87874e09680a799bda82cca9a66fd7c5a72436 Mon Sep 17 00:00:00 2001 From: Zagrios <40181755+Zagrios@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:10:07 +0100 Subject: [PATCH 26/29] update changelog --- CHANGELOG.md | 144 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 933d3a8a..0aca4d39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,148 @@ ## Not released yet ~ +[PR Merged since the last release](https://github.com/Zagrios/bs-manager/pulls?q=is%3Apr+is%3Amerged+merged%3A%3E2025-01-18)\ +[PR Merged since the last alpha](https://github.com/Zagrios/bs-manager/pulls?q=is%3Apr+is%3Amerged+merged%3A%3E2025-01-11+) + +## [1.4.18](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.18) (Jan 18, 2025) + +### Fixes +- Fixed an issue where enabling Oculus sideloading was not working if BSManager installation path contained spaces #735 +- The IPA folder is now cleared each time BSIPA is installed to prevent potential crashes caused by conflicts with old BSIPA files #746 +- Added a check to ensure the file system supports symlinks before moving contents to the shared folder #739 + +### Other changes +- Events occurring in `bs-admin-start.exe` and `oculus-allow-dev-sideloaded.exe` are now logged #735 + +## [1.4.17](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.17) (Jan 2, 2025) + +### Fixes +- Fixed an issue where original Steam installations of Beat Saber did not have any mods available +- Mod descriptions should no longer contain HTML tags + +## [1.4.16](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.16) (Jan 2, 2025) + +### Fixes +- Fixed infinite mod loading when a mod file could not be found on BeatMods #725 (Thanks to @silentrald) +- Fixed an issue where no mods were shown as installed when a mod dependency could not be found #726 (Thanks to @silentrald) +- Fixed minor UI issues + +## [1.4.15](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.15) (Jan 2, 2025) + +### Fixes +- Use the sideloading feature of Oculus to prevent errors when launching Oculus versions of Beat Saber #720 +- Oculus token starting with `OC` are now accepted + +### Other changes +- Mods now load faster #508 +- Switched to the new BeatMods API for loading mods + + +## [1.4.14](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.14) (Dec 2, 2024) + +### Fixes +- Fixed an issue where downloading private playlists through OneClick was always resulting in an error #679 (Thanks to @Top-Cat) +- Beat Saber file verification was broken due to an error in DepotDownloader #677 + + +## [1.4.13](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.13) (Nov 26, 2024) + +### Features +- Added the possibility to unselect all mods from the mods panel #666 + +### Fixes +- Fixed an issue where downloading maps with subfolders was not possible #649 (Thanks to @silentrald) +- Corrected the mods search bar color in the light theme #660 (Thanks to @chk1) + +### Other changes +- Added the BeatLeader icon to the OneClick playlist settings #653 +- A warning now appears before downloading an outdated BeatSaber version #654 +- Oculus BeatSaber versions no longer launch if the Oculus library is not found #657 (Thanks to @LiamillionSS) +- Maps from BeatSaver are now sorted by relevance by default #662 (Thanks to @Top-Cat) +- Removed the `--no-yeet` argument when launching original copies of BeatSaber #667 +- Added warnings for outdated versions in the versions view #673 + + +## [1.4.12](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.12) (Nov 6, 2024) + +### Fixes +- Fixed OneClick playlist download that has been broken in the previous release #645 +- Fixed an issue where downloading playlists could fail if the playlist's file contained special characters #645 + +## [1.4.11](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.11) (Nov 4, 2024) + +### Fixes +- BSManager was not working if the path to BSM's installation folder contained URL special characters #636 +- Linking folders was no longer working if the folders contained files +- Under certain conditions, loading maps could result in a black screen + +### Other changes +- The "Broken Models" notification has been removed #640 + + +## [1.4.10](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.10) (Oct 27, 2024) + +### Fixes +- Mods using a manifest file were sometimes not detected as installed #564 +- BSIPA installation was always considered as successfull even if an error occured #511 +- Some map filter tags were no longer working #619 (Thanks to @silentrald) +- Fixed an issue where, under certain conditions, linking a folder could result in the loss of its contents #568 + +### Other changes +- Default mods are no longer reselected when mods are already installed #535 +- The `DLC` folder has been removed from the default list of folders that can be linked #628 +- The naming scheme for downloaded maps and playlists has been updated to prevent duplicates when downloading maps or playlists from Beat Saber or other tools + + +## [1.4.9](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.9) (Oct 15, 2024) + +### Fixes +- Updated DepotDownloader dependency to fix connection errors with Steam + +### Other changes +- Added support for `info.dat` v4 + + +## [1.4.8](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.8) (Aug 1, 2024) + +### Other changes +- Added checks to avoid logging sensitive data in log files +- The oldest log files are deleted to retain a maximum of 5 log files + + +## [1.4.7](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.7) (Mar 13, 2024) + +### Fixes +- Fixed being unable to download models with BSManager #436 + +### Other changes +- Fixed a typo in german translation (Thanks to @fllppi) + +## [1.4.6](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.6) (Mar 6, 2024) + +### Features +- It's now possible to exclude already installed maps when downloading maps from BSManager #426 (Thanks to @Liborsaf) + +### Fixes +- Fixed an issue where BSManager couldn't get path to user's documents under certain conditions, preventing any action from the user #431 +- To prevent the last launched Beat Saber version getting wiped by Oculus auto-updates, the symlink created to launch the version is now deleted after the game stops #432 + +## [1.4.5](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.5) (Feb 12, 2024) + +### Fixes +- Fixed an issue preventing launching downgraded Steam versions under certain conditions #415 +- Fixed an issue preventing launching downgraded Oculus versions under certain conditions #421 +- Fixed an issue where SteamVR was not properly restored after launching Beat Saber from a shortcut with FPFC mode #417 (Thanks to @slinkstr) + +## [1.4.4](https://github.com/Zagrios/bs-manager/releases/edit/v1.4.4) (Feb 1, 2024) + +### Fixes +- When launching an Oculus Beat Saber version, the Oculus app is also started if needed to avoid crashes #405 +- If Steam is running as admin, Beat Saber is now also started as admin to avoid crashes #404 +- Fixed the issue where BSManager could fail to start due to a missing DLL #400 +- Fixed the issue where BSManager could not locate the Oculus library #398 + +### Other changes +- Added some tooltips to the maps UIs #397 +- Updated the maps panel UI for consistency with the rest of BSManager UIs #399 ## [1.4.3](https://github.com/Zagrios/bs-manager/releases/tag/v1.4.3) (Dec 27, 2023) From 8476f869b443e775c41ce631c0850533c44061fc Mon Sep 17 00:00:00 2001 From: Zagrios <40181755+Zagrios@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:16:44 +0100 Subject: [PATCH 27/29] Bumb to v1.5.0-alpha.9 --- package.json | 2 +- release/app/package-lock.json | 4 ++-- release/app/package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index e9485e6e..df3479c4 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "bs-manager", "description": "Manage maps, mods and more for Beat Saber", "main": "./.erb/dll/main.bundle.dev.js", - "version": "1.5.0-alpha.8", + "version": "1.5.0-alpha.9", "scripts": { "build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js", "build": "concurrently \"npm run build:main\" \"npm run build:renderer\"", diff --git a/release/app/package-lock.json b/release/app/package-lock.json index ff426014..9e80fa89 100644 --- a/release/app/package-lock.json +++ b/release/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "bs-manager", - "version": "1.5.0-alpha.8", + "version": "1.5.0-alpha.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bs-manager", - "version": "1.5.0-alpha.8", + "version": "1.5.0-alpha.9", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/release/app/package.json b/release/app/package.json index 16c77c61..14c7e276 100644 --- a/release/app/package.json +++ b/release/app/package.json @@ -1,6 +1,6 @@ { "name": "bs-manager", - "version": "1.5.0-alpha.8", + "version": "1.5.0-alpha.9", "description": "BSManager", "main": "./dist/main/main.js", "author": { From 2c1c94640fe9b017536f2f8b8f093246e23df120 Mon Sep 17 00:00:00 2001 From: Zagrios <40181755+Zagrios@users.noreply.github.com> Date: Tue, 28 Jan 2025 15:18:44 +0100 Subject: [PATCH 28/29] bump version to 1.5.0-alpha.9 and update license to GPL-3.0-only --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5c3c112..a5d14dd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,14 @@ { "name": "bs-manager", - "version": "1.5.0-alpha.7", + "version": "1.5.0-alpha.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bs-manager", - "version": "1.5.0-alpha.7", + "version": "1.5.0-alpha.9", "hasInstallScript": true, - "license": "MIT", + "license": "GPL-3.0-only", "dependencies": { "@internationalized/date": "^3.5.4", "@nextui-org/date-picker": "^2.0.7", From 4c4b783aa7a47ba737d84fae0b828384b7a29c4f Mon Sep 17 00:00:00 2001 From: Zagrios <40181755+Zagrios@users.noreply.github.com> Date: Tue, 28 Jan 2025 16:08:17 +0100 Subject: [PATCH 29/29] [bugfix] Min nps of playlists was always zero --- .../local-playlists-manager.service.ts | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/services/additional-content/local-playlists-manager.service.ts b/src/main/services/additional-content/local-playlists-manager.service.ts index d77bb85c..4de1b4a1 100644 --- a/src/main/services/additional-content/local-playlists-manager.service.ts +++ b/src/main/services/additional-content/local-playlists-manager.service.ts @@ -242,7 +242,9 @@ export class LocalPlaylistsManagerService { ...localBPList, duration: 0, nbMaps: localBPList.songs?.length ?? 0, - id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined + id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined, + minNps: Infinity, + maxNps: -Infinity, } const mappers = new Set(); @@ -254,12 +256,25 @@ export class LocalPlaylistsManagerService { bpListDetails.duration += songDetails?.duration ? +songDetails.duration : 0; mappers.add(songDetails.uploader?.id); - bpListDetails.minNps = Math.min(bpListDetails?.minNps ?? 0, Math.min(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0])); - bpListDetails.maxNps = Math.max(bpListDetails?.maxNps ?? 0, Math.max(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0])); + + const mapNps = songDetails.difficulties?.map(d => d?.nps).filter(nps => typeof nps === "number" && !Number.isNaN(nps)); + + if(mapNps?.length){ + bpListDetails.minNps = Math.min(bpListDetails.minNps, ...mapNps); + bpListDetails.maxNps = Math.max(bpListDetails.maxNps, ...mapNps); + } song.songDetails = songDetails; } + if(!Number.isFinite(bpListDetails.minNps)){ + bpListDetails.minNps = 0; + } + + if(!Number.isFinite(bpListDetails.maxNps)){ + bpListDetails.maxNps = 0; + } + bpListDetails.nbMappers = mappers.size; return bpListDetails;