Compare commits

..

24 Commits

Author SHA1 Message Date
MathieuG-P 29426f21b2 bumb to v1.5.0alpha-7 2025-01-03 16:55:53 +01:00
MathieuG-P db1e51b90a Refresh mods after import 2025-01-03 16:52:00 +01:00
MathieuG-P eba7e55539 [docs] put the "Known bugs" section at the bottom 2025-01-03 16:21:01 +01:00
MathieuG-P ef5f6ee5f6 Merge pull request #674 from basti564/patch-1
wiki: easier method to obtain oculus token
2025-01-03 16:15:57 +01:00
MathieuG-P 7c185b9d2c Merge branch 'master' into patch-1 2025-01-03 16:14:52 +01:00
MathieuG-P 8766809fb8 [docs] Update Oculus Token retrieval instructions and add known bugs section 2025-01-03 16:08:55 +01:00
MathieuG-P a424651f9d Merge pull request #728 from Zagrios/apply-changes-from-1.4.15-1.4.16-1.4.17
Apply changes from [1.4.15-1.4.17]
2025-01-03 15:52:42 +01:00
MathieuG-P 5113d72db2 fix lint issue 2025-01-03 15:46:51 +01:00
MathieuG-P 4968022b7e remove old mods models from the index 2025-01-03 15:43:03 +01:00
MathieuG-P 0c65b7b537 Add "OC" token pattern to log filter 2025-01-03 15:36:05 +01:00
MathieuG-P 8270ec800e Apply mods changes from [1.4.15-1.4.17] + some clean 2025-01-03 15:27:51 +01:00
MathieuG-P a70229c80b add patreon 2025-01-03 10:21:26 +01:00
MathieuG-P bf4e5aefa4 Merge pull request #721 from Davitekk/master
[Translation] Add Italian translation
2025-01-03 01:21:25 +01:00
Davitekk 3c49570f76 Update it.json 2025-01-02 17:17:27 +01:00
Davitekk 0f80f6ae28 Update it.json 2025-01-02 17:10:05 +01:00
Davitekk 066e740a62 Update it.json 2025-01-02 13:15:19 +01:00
Davitekk f296f34e62 Merge branch 'Zagrios:master' into master 2025-01-02 12:46:29 +01:00
Davitekk fb174b4451 Update it.json 2025-01-02 10:50:37 +01:00
Davitekk dfad8a09cc Update italy-icon.component.tsx 2025-01-02 10:47:01 +01:00
MathieuG-P 69b246949f Merge pull request #712 from silentrald/wiki/deb
[wiki] added deb install thru ppa repo in github
2025-01-02 02:33:57 +01:00
MathieuG-P 93ea2e1233 Merge pull request #724 from eaglesemanation/master
Update flatpak dependencies to non-EOL versions
2025-01-02 02:31:16 +01:00
Vladimir Romashchenko 23d9a605df Update flatpak dependencies to non-EOL versions 2025-01-01 18:11:39 -05:00
silentrald 03d4f007a3 [wiki] added deb install thru ppa repo in github 2024-12-25 16:15:34 +08:00
Ellie 
:3 5a46a67e1e wiki: easier method to obtain oculus token 2024-11-26 23:01:04 +01:00
22 changed files with 361 additions and 370 deletions
+3
View File
@@ -124,5 +124,8 @@
},
{
"username": "Maximilian"
},
{
"username": "Paul Smith"
}
]
+17 -10
View File
@@ -209,25 +209,25 @@
"en-US": "English, US",
"fr-FR": "Français",
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"ru-RU": "Русский",
"zh-CN": "简体中文",
"zh-TW": "正體中文",
"ja-JP": "日本語",
"ko-KR": "한국어",
"it-IT": "Italiano",
"translated": {
"en-EN": "Inglese, Regno Unito",
"en-US": "English, Stati Uniti d'America",
"en-US": "Inglese, Stati Uniti d'America",
"fr-FR": "Francese",
"es-ES": "Spagnolo",
"it-IT": "Italiano",
"de-DE": "Tedesco",
"ru-RU": "Russo",
"zh-CN": "Cinese (Semplificato)",
"zh-TW": "Cinese (Tradizionale)",
"ja-JP": "Giapponese",
"ko-KR": "Coreano",
"it-IT": "Italiano"
"ko-KR": "Coreano"
}
}
},
@@ -911,7 +911,8 @@
"tags": "tag",
"specificities": "generale",
"requirements": "requisiti",
"exclude": "escludi"
"exclude": "escludi",
"leaderboard": "classifica"
},
"map-types": {
"accuracy": "precisione",
@@ -967,15 +968,21 @@
"verified": "verificata",
"fullSpread": "completa"
},
"map-leaderboard": {
"All": "Tutte",
"Ranked": "Classificate",
"BeatLeader": "BeatLeader",
"ScoreSaber": "ScoreSaber"
},
"map-excludes": {
"installed": "installata"
},
"difficulties": {
"Easy": "Easy",
"Normal": "Normal",
"Hard": "Hard",
"Expert": "Expert",
"ExpertPlus": "Expert+"
"Easy": "Facile",
"Normal": "Normale",
"Hard": "Difficile",
"Expert": "Esperto",
"ExpertPlus": "Esperto+"
},
"map-item": {
"by": "Di {songAutor}",
@@ -4,6 +4,8 @@ Your token is a confidential piece of information. Possession of this token allo
However, you might wonder why it is necessary to provide this token to BSManager. The reason is that BSManager requires the token to continue the download with Oculus. Once you've input the token, it is used exclusively to communicate with Oculus servers to verify that you are the rightful owner of the game.
# Solution 1 - Using the Oculus app
## Step 1 - Install and log into the Oculus Rift app
- Get the Oculus Rift app setup from the [Meta website](https://www.oculus.com/rift/setup/)
- Install the Oculus Rift app
@@ -26,5 +28,23 @@ In the developer tools :
![image](https://raw.githubusercontent.com/Zagrios/bs-manager/refs/heads/master/docs/assets/oculus-token.png)
# Solution 2 - Using the Oculus website
## Steps
1. Log in to your Meta account at https://secure.oculus.com
2. Open the developer tools in your web browser:
- Chrome/Edge/Firefox: Press <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>I</kbd> or <kbd>F12</kbd>
- Safari: Press <kbd>Option</kbd>+<kbd>Command</kbd>+<kbd>I</kbd>
3. Go to the "Application" tab
4. Expand "Cookies" in the left sidebar
5. Click on `https://secure.oculus.com`
6. Find the cookie named `oc_ac_at` (value starts with `OC`)
7. Copy the entire `oc_ac_at` cookie value - this is your token
<img width="500" src="https://github.com/user-attachments/assets/8499035c-4e7c-423d-914d-f35b6a3d77af">
Paste the token into BSManager or other trusted apps to manage your Beat Saber game and mods. Keep it confidential.
# Known bugs
- [Nothing opens when I press `Ctrl`+`Shift`+`i`.](https://github.com/Zagrios/bs-manager/wiki/Nothing-opens-when-I-press-%60Ctrl%60%E2%80%90%60Shift%60%E2%80%90%60i%60.)
+23
View File
@@ -6,11 +6,34 @@ Go to [Releases](https://github.com/Zagrios/bs-manager/releases) page and go to
### Ubuntu, Debian (deb)
#### PPA Repository
Refer to [bs-manager-deb](https://github.com/silentrald/bs-manager-deb).
Add the BSManager PPA repository into your system with the following commands.
```bash
curl -fsSL https://raw.githubusercontent.com/silentrald/bs-manager-deb/refs/heads/main/KEY.gpg | sudo gpg --dearmor -o /usr/share/keyrings/bs-manager.gpg
echo "deb [signed-by=/usr/share/keyrings/bs-manager.gpg] https://raw.githubusercontent.com/silentrald/bs-manager-deb/refs/heads/main ./" | sudo tee /etc/apt/sources.list.d/bs-manager.list
sudo apt update
```
Install the `bs-manager` package using `apt`.
```bash
sudo apt install bs-manager
```
#### dpkg install
Download the `.deb` file in the releases and run the following command:
```bash
dpkg -i ./bsmanager.deb
```
**NOTE:** When installed using dpkg, BSManager will not automatically update to the latest version. You have to either:
* Download the latest `.deb` file on the Releases page; or
* Install thru PPA repository to automatically update with `sudo apt update & sudo apt upgrade`.
### Arch (AUR)
Refer to [bs-manager-git](https://aur.archlinux.org/packages/bs-manager-git).
+4
View File
@@ -51,6 +51,10 @@ const config = {
fpm: ["--after-install=build/after-install.sh"],
},
flatpak: {
// Version of org.electronjs.Electron2.BaseApp
baseVersion: "24.08",
// Version of org.freedesktop.Platform
runtimeVersion: "24.08",
finishArgs: [
// Wayland/X11 Rendering
"--socket=wayland",
+3 -2
View File
@@ -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.6",
"version": "1.5.0-alpha.7",
"scripts": {
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
@@ -21,7 +21,8 @@
"test": "jest",
"test:unit": "jest ./src/__tests__/unit",
"publish": "npm run build && electron-builder -c.win.certificateSha1=206941d969c4fa8a0e04d9427def361e13b02fd0 --config electron-builder.config.js --publish always --win --x64",
"publish:linux": "npm run build && electron-builder --config electron-builder.config.js --publish never --linux --x64"
"publish:linux": "npm run build && electron-builder --config electron-builder.config.js --publish never --linux --x64",
"publish:flatpak": "npm run build && env DEBUG='@malept/flatpak-bundler' npx electron-builder --config electron-builder.config.js --publish never --linux flatpak"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.6",
"version": "1.5.0-alpha.7",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.5.0-alpha.6",
"version": "1.5.0-alpha.7",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.6",
"version": "1.5.0-alpha.7",
"description": "BSManager",
"main": "./dist/main/main.js",
"author": {
+1 -1
View File
@@ -34,7 +34,7 @@ export const filterStrings = new Set<string>();
export const filterPatterns = new Set<RegExp>();
// Filter all occulus tokens
filterPatterns.add(/FRL\S{10,}/g);
filterPatterns.add(/(FRL|OC)\S{10,}/g);
initLogger();
deleteOlestLogs();
@@ -8,7 +8,6 @@ import { ConfigurationService } from "./configuration.service";
import { lstat, rename } from "fs/promises";
import log from "electron-log";
import { OculusService } from "./oculus.service";
import { DownloadLinkType } from "shared/models/mods";
import sanitize from "sanitize-filename";
import { Progression, copyDirectoryWithJunctions, deleteFolder, ensurePathNotAlreadyExist, getFoldersInFolder, rxCopy } from "../helpers/fs.helpers";
import { FolderLinkerService } from "./folder-linker.service";
@@ -242,16 +241,6 @@ export class BSLocalVersionService {
return version.name ?? version.BSVersion;
}
public getVersionType(version: BSVersion): DownloadLinkType {
if (version.steam) {
return "steam";
}
if (version.oculus) {
return "oculus";
}
return "universal";
}
private async getSteamVersion(): Promise<BSVersion> {
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
+25 -80
View File
@@ -1,22 +1,19 @@
import { BSVersion } from "shared/bs-version.interface";
import { Mod } from "shared/models/mods/mod.interface";
import { BbmFullMod, BbmMod, BbmModVersion, BbmPlatform } from "../../../shared/models/mods/mod.interface";
import { RequestService } from "../request.service";
import { BsStore } from "../../../shared/models/bs-store.enum";
import log from "electron-log"
export class BeatModsApiService {
private static instance: BeatModsApiService;
private readonly requestService: RequestService;
private readonly BEAT_MODS_ALIAS = "https://alias.beatmods.com/aliases.json";
public readonly MODS_REPO_URL = "https://beatmods.com";
private readonly MODS_REPO_API_URL = `${this.MODS_REPO_URL}/api`;
private readonly BEAT_MODS_API_URL = "https://beatmods.com/api/v1/";
public readonly BEAT_MODS_URL = "https://beatmods.com";
private readonly aliasesCache = new Map<string, BSVersion[]>();
private readonly versionModsCache = new Map<string, Mod[]>();
private readonly modsHashCache = new Map<string, Mod>();
private allModsCache: Mod[];
private readonly versionModsCache = new Map<string, BbmFullMod[]>();
private readonly modsHashCache = new Map<string, BbmModVersion>();
public static getInstance(): BeatModsApiService {
if (!BeatModsApiService.instance) {
@@ -30,101 +27,49 @@ export class BeatModsApiService {
}
private getVersionModsUrl(version: BSVersion): string {
return `${this.BEAT_MODS_API_URL}mod?status=approved&gameVersion=${version.BSVersion}&sort=&sortDirection=1`;
const platform: BbmPlatform = version.oculus || version.metadata?.store === BsStore.OCULUS ? BbmPlatform.OculusPC : BbmPlatform.SteamPC;
return `${this.MODS_REPO_API_URL}/mods?status=verified&gameVersion=${version.BSVersion}&gameName=BeatSaber&platform=${platform}`;
}
private getAllModsUrl(): string {
return `${this.BEAT_MODS_API_URL}mod`;
}
private async getVersionAlias(): Promise<Map<string, BSVersion[]>> {
if (this.aliasesCache.size) {
return this.aliasesCache;
}
return this.requestService.getJSON<Record<string, string[]>>(this.BEAT_MODS_ALIAS).then(({ data: rawAliases }) => {
Object.entries(rawAliases).forEach(([key, value]) => {
this.aliasesCache.set(
key,
value.map(s => ({ BSVersion: s } as BSVersion))
);
});
return this.aliasesCache;
});
}
private async getAliasOfVersion(version: BSVersion): Promise<BSVersion> {
return this.getVersionAlias().then(aliases => {
if (Array.from(aliases.keys()).some(k => k === version.BSVersion)) {
return version;
}
const alias = Array.from(aliases.entries()).find(([, value]) => value.find(v => v.BSVersion === version.BSVersion))?.[0];
return { BSVersion: alias } as BSVersion;
});
}
private asignDependencies(mod: Mod, mods: Mod[]): Mod {
mod.dependencies = mod.dependencies.map(dep => mods.find(mod => mod.name === dep.name));
return mod;
}
private updateModsHashCache(mods: Mod[]): void {
private updateModsHashCache(mods: BbmModVersion[]): void {
if(!Array.isArray(mods)){
return;
}
for (const mod of mods) {
for (const downloads of (mod.downloads ?? [])) {
for (const hashMd5 of (downloads.hashMd5 ?? [])) {
this.modsHashCache.set(hashMd5.hash, mod);
}
}
for (const dep of (mod.dependencies ?? [])) {
for (const downloads of (dep.downloads ?? [])) {
for (const hashMd5 of (downloads.hashMd5 ?? [])) {
this.modsHashCache.set(hashMd5.hash, dep);
}
}
for (const content of (mod.contentHashes ?? [])) {
this.modsHashCache.set(content.hash, mod);
}
}
}
public async getVersionMods(version: BSVersion): Promise<Mod[]> {
public async getVersionMods(version: BSVersion): Promise<BbmFullMod[]> {
if (this.versionModsCache.has(version.BSVersion)) {
return this.versionModsCache.get(version.BSVersion);
}
const alias = await this.getAliasOfVersion(version);
return this.requestService.getJSON<{ mods: {mod: BbmMod, latest: BbmModVersion}[] }>(this.getVersionModsUrl(version)).then(({ data }) => {
const fullMods: BbmFullMod[] = data?.mods?.map(mod => ({ mod: mod.mod, version: mod.latest })) ?? [];
this.versionModsCache.set(version.BSVersion, fullMods);
return this.requestService.getJSON<Mod[]>(this.getVersionModsUrl(alias)).then(({ data: mods }) => {
mods = mods.map(mod => this.asignDependencies(mod, mods));
this.versionModsCache.set(version.BSVersion, mods);
this.updateModsHashCache(fullMods.map(mod => mod.version));
this.updateModsHashCache(mods);
return mods;
return fullMods;
});
}
public async getAllMods(): Promise<Mod[]> {
if (this.allModsCache) {
return this.allModsCache;
}
return this.requestService.getJSON<Mod[]>(this.getAllModsUrl()).then(({ data: mods }) => {
this.allModsCache = mods;
return this.allModsCache;
});
}
public getModByHash(hash: string): Promise<Mod> {
public getModByHash(hash: string): Promise<BbmModVersion|undefined> {
if (this.modsHashCache.has(hash)) {
return Promise.resolve(this.modsHashCache.get(hash));
}
return this.requestService.getJSON<Mod[]>(`${this.BEAT_MODS_API_URL}mod?hash=${hash}`).then(({ data: mods }) => {
this.updateModsHashCache(mods);
return mods.at(0);
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);
return undefined;
});
}
}
@@ -1,5 +1,4 @@
import { BSVersion } from "shared/bs-version.interface";
import { DownloadLink, Mod } from "shared/models/mods";
import { BeatModsApiService } from "./beat-mods-api.service";
import { BSLocalVersionService } from "../bs-local-version.service";
import path from "path";
@@ -19,7 +18,7 @@ import { tryit } from "shared/helpers/error.helpers";
import crypto from "crypto";
import { BsmZipExtractor } from "main/models/bsm-zip-extractor.class";
import { bsmSpawn } from "main/helpers/os.helpers";
import { ExternalMod } from "shared/models/mods/mod.interface";
import { BbmFullMod, BbmModVersion, ExternalMod } from "../../../shared/models/mods/mod.interface";
export class BsModsManagerService {
private static instance: BsModsManagerService;
@@ -29,7 +28,7 @@ export class BsModsManagerService {
private readonly linuxService: LinuxService;
private readonly requestService: RequestService;
private manifestMatches: Mod[];
private manifestMatches: BbmModVersion[];
public static getInstance(): BsModsManagerService {
if (!BsModsManagerService.instance) {
@@ -45,18 +44,18 @@ export class BsModsManagerService {
this.requestService = RequestService.getInstance();
}
private async getModFromHash(hash: string): Promise<Mod> {
private async getModFromHash(hash: string): Promise<BbmModVersion | undefined> {
const mod = await this.beatModsApi.getModByHash(hash);
if(mod?.name?.toLowerCase() === "bsipa"){
if(mod?.contentHashes?.some(content => content.path.includes("IPA.exe"))){
return undefined;
}
return mod;
}
private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise<Mod[]> {
private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise<BbmModVersion[]> {
const bsPath = await this.bsLocalService.getVersionPath(version);
const modsPath = path.join(bsPath, modsDir);
@@ -85,7 +84,7 @@ export class BsModsManagerService {
}
if (filePath.toLowerCase().includes("libs")) {
const manifestIndex = this.manifestMatches.findIndex(m => m.name === mod.name);
const manifestIndex = this.manifestMatches.findIndex(m => m.id === mod.id);
if (manifestIndex < 0) {
return undefined;
@@ -101,7 +100,7 @@ export class BsModsManagerService {
return mods.filter(Boolean);
}
private async getBsipaInstalled(version: BSVersion): Promise<Mod> {
private async getBsipaInstalled(version: BSVersion): Promise<BbmModVersion> {
const bsPath = await this.bsLocalService.getVersionPath(version);
const injectorPath = path.join(bsPath, "Beat Saber_Data", "Managed", "IPA.Injector.dll");
if (!(await pathExist(injectorPath))) {
@@ -112,15 +111,14 @@ export class BsModsManagerService {
}
private async downloadZip(zipUrl: string): Promise<BsmZipExtractor> {
zipUrl = path.join(this.beatModsApi.BEAT_MODS_URL, zipUrl);
zipUrl = path.join(this.beatModsApi.MODS_REPO_URL, zipUrl);
log.info("Download mod zip", zipUrl);
const buffer = await lastValueFrom(this.requestService.downloadBuffer(zipUrl))
.then(progress => progress.data)
.catch(e => {
.catch((e: Error) => {
log.error("ZIP", "Error while downloading zip", e);
return undefined;
});
if (!buffer) {
@@ -129,7 +127,6 @@ export class BsModsManagerService {
return BsmZipExtractor.fromBuffer(buffer);
}
private async executeBSIPA(version: BSVersion, args: string[]): Promise<boolean> {
log.info("executeBSIPA", version?.BSVersion, args);
@@ -190,25 +187,22 @@ export class BsModsManagerService {
});
}
private getModDownload(mod: Mod, version: BSVersion): DownloadLink {
return mod.downloads.find(download => {
const type = download.type.toLowerCase();
return type === "universal" || type === this.bsLocalService.getVersionType(version);
});
private getModDownload(modVersion: BbmModVersion): string {
return `/cdn/mod/${modVersion.zipHash}.zip`
}
private async installMod(mod: Mod, version: BSVersion): Promise<boolean> {
log.info("INSTALL MOD", mod.name, "for version", `${version.BSVersion} - ${version.name}`);
private async installMod(mod: BbmFullMod, version: BSVersion): Promise<boolean> {
log.info("INSTALL MOD", mod.mod.name, "for version", `${version.BSVersion} - ${version.name}`);
const download = this.getModDownload(mod, version);
const downloadUrl = this.getModDownload(mod.version);
if (!download) {
if (!downloadUrl) {
return false;
}
log.info("Start download mod zip", mod.name, download.url);
const zip = await this.downloadZip(download.url);
log.info("Mod zip download end", mod.name, download.url);
log.info("Start download mod zip", mod.mod.name, downloadUrl);
const zip = await this.downloadZip(downloadUrl);
log.info("Mod zip download end", mod.mod.name, downloadUrl);
if (!zip) {
return false;
@@ -220,18 +214,18 @@ export class BsModsManagerService {
const md5Hash = crypto.createHash("md5")
.update(buffer)
.digest("hex");
hashCount += +download.hashMd5.some(md5 => md5.hash === md5Hash);
hashCount += +mod.version.contentHashes.some(content => content.hash === md5Hash);
}
if (hashCount !== download.hashMd5.length) {
if (hashCount !== mod.version.contentHashes.length) {
return false;
}
const versionPath = await this.bsLocalService.getVersionPath(version);
const isBSIPA = mod.name.toLowerCase() === "bsipa";
const isBSIPA = mod.mod.name.toLowerCase() === "bsipa";
const destDir = isBSIPA ? versionPath : path.join(versionPath, ModsInstallFolder.PENDING);
log.info("Start extracting mod zip", mod.name, "to", destDir);
log.info("Start extracting mod zip", mod.mod.name, "to", destDir);
const extracted = await zip.extract(destDir)
.then(() => true)
.catch(e => {
@@ -242,7 +236,7 @@ export class BsModsManagerService {
zip.close();
});
log.info("Mod zip extraction end", mod.name, "to", destDir, "success:", extracted);
log.info("Mod zip extraction end", mod.mod.name, "to", destDir, "success:", extracted);
const res = isBSIPA
? extracted &&
@@ -255,9 +249,7 @@ export class BsModsManagerService {
return res;
}
private async uninstallBSIPA(mod: Mod, version: BSVersion): Promise<void> {
const download = this.getModDownload(mod, version);
private async uninstallBSIPA(mod: BbmFullMod, version: BSVersion): Promise<void> {
const verionPath = await this.bsLocalService.getVersionPath(version);
const hasIPAExe = await pathExist(path.join(verionPath, "IPA.exe"));
const hasIPADir = await pathExist(path.join(verionPath, "IPA"));
@@ -268,34 +260,34 @@ export class BsModsManagerService {
await this.executeBSIPA(version, ["--revert", "-n"]);
const promises = download.hashMd5.map(files => {
const file = files.file.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
const promises = mod.version.contentHashes.map(content => {
const file = content.path.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
return unlinkPath(path.join(verionPath, file));
});
await Promise.all(promises);
}
private async uninstallMod(mod: Mod, version: BSVersion): Promise<void> {
if (mod.name.toLowerCase() === "bsipa") {
private async uninstallMod(mod: BbmFullMod, version: BSVersion): Promise<void> {
if (mod.mod.name.toLowerCase() === "bsipa") {
return this.uninstallBSIPA(mod, version);
}
const download = this.getModDownload(mod, version);
const versionPath = await this.bsLocalService.getVersionPath(version);
const promises = download.hashMd5.map(async files => {
return Promise.all([unlinkPath(path.join(versionPath, files.file)), unlinkPath(path.join(versionPath, "IPA", "Pending", files.file))]);
const promises = mod.version.contentHashes.map(async content => {
return Promise.all([unlinkPath(path.join(versionPath, content.path)), unlinkPath(path.join(versionPath, "IPA", "Pending", content.path))]);
});
await Promise.all(promises);
}
public getAvailableMods(version: BSVersion): Promise<Mod[]> {
return this.beatModsApi.getVersionMods(version);
public async getAvailableMods(version: BSVersion): Promise<BbmFullMod[]> {
return this.beatModsApi.getVersionMods(version).catch(() => {
return [] as BbmFullMod[];
});
}
public async getInstalledMods(version: BSVersion): Promise<Mod[]> {
public async getInstalledMods(version: BSVersion): Promise<BbmModVersion[]> {
this.manifestMatches = [];
const bsipa = await this.getBsipaInstalled(version);
@@ -305,17 +297,17 @@ export class BsModsManagerService {
const dirMods = pluginsMods.flat().concat(libsMods.flat());
const modsDict = new Map<string, Mod>();
const modsDict = new Map<number, BbmModVersion>();
if (bsipa) {
modsDict.set(bsipa.name, bsipa);
modsDict.set(bsipa.id, bsipa);
}
for (const mod of dirMods.flat()) {
if (modsDict.has(mod.name)) {
if (modsDict.has(mod.id)) {
continue;
}
modsDict.set(mod.name, mod);
modsDict.set(mod.id, mod);
}
return Array.from(modsDict.values());
@@ -423,7 +415,7 @@ export class BsModsManagerService {
});
}
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
public installMods(mods: BbmFullMod[], version: BSVersion): Observable<Progression> {
const progress = { current: 0, total: mods.length };
return new Observable<Progression>(obs => {
@@ -434,7 +426,7 @@ export class BsModsManagerService {
obs.next(progress);
const bsipa = popElement(mod => mod.name.toLowerCase() === "bsipa", mods);
const bsipa = popElement(mod => mod.mod.name.toLowerCase() === "bsipa", mods);
if(bsipa){
const bsipaInstalled = await this.installMod(bsipa, version).catch(err => {
@@ -460,7 +452,7 @@ export class BsModsManagerService {
});
}
public uninstallMods(mods: Mod[], version: BSVersion): Observable<Progression> {
public uninstallMods(mods: BbmFullMod[], version: BSVersion): Observable<Progression> {
const progress = { current: 0, total: mods.length };
return new Observable<Progression>(obs => {
@@ -485,16 +477,19 @@ export class BsModsManagerService {
public uninstallAllMods(version: BSVersion): Observable<Progression> {
return new Observable<Progression>(obs => {
(async () => {
const mods = await this.getInstalledMods(version).catch(err => {
log.error(err);
return [];
});
const progress = { current: 0, total: mods.length };
const versionMods = await this.getAvailableMods(version);
const installedMods = await this.getInstalledMods(version);
const fullInstalledMods: BbmFullMod[] = installedMods?.map(version => {
return { version, mod: versionMods.find(mod => version.modId === mod.mod.id)?.mod };
}) ?? [];
const progress = { current: 0, total: fullInstalledMods.length };
obs.next(progress);
for (const mod of mods) {
for (const mod of fullInstalledMods) {
await this.uninstallMod(mod, version);
progress.current++;
obs.next(progress);
@@ -1,38 +0,0 @@
import { ModalComponent, ModalExitCode } from "../../../services/modale.service";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { Mod } from "shared/models/mods/mod.interface";
export const UninstallModModal: ModalComponent<void, Mod> = ({ resolver, options: {data} }) => {
const mod = data;
const t = useTranslation();
const desc = mod.name.toLowerCase() === "bsipa" ? "modals.uninstall-mod.description-bsipa" : "modals.uninstall-mod.description";
return (
<form
onSubmit={e => {
e.preventDefault();
resolver({ exitCode: ModalExitCode.COMPLETED });
}}
>
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.uninstall-mod.title")}</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict} />
<p className="max-w-sm text-gray-800 dark:text-gray-200">{t(desc, { mod: mod.name })}</p>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
<BsmButton
typeColor="cancel"
className="rounded-md text-center transition-all"
onClick={() => {
resolver({ exitCode: ModalExitCode.CANCELED });
}}
withBar={false}
text="misc.cancel"
/>
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" type="submit" withBar={false} text="modals.bs-uninstall.buttons.submit" />
</div>
</form>
);
};
@@ -2,12 +2,12 @@ import { createSvgIcon } from "../svg-icon.type";
export const ItalyIcon = createSvgIcon((props, ref) => {
return (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 48" ref={ref} {...props} width="71" height="48">
<g fill="none">
<path d="M6.65.53A6.36 6.36 0 0 0 .29 6.89v34.27a6.36 6.36 0 0 0 6.36 6.36h17v-47z" fill="#57A863"/>
<path d="M23.63.53v47H47v-47z" fill="#FFF"/>
<path d="M63.92.53H47v47h16.92a6.36 6.36 0 0 0 6.36-6.36V6.89A6.36 6.36 0 0 0 63.92.53" fill="#ED5565"/>
</g>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 71 48" ref={ref} {...props} width="71" height="48">
<g fill="none">
<path d="M6.65.53A6.36 6.36 0 0 0 .29 6.89v34.27a6.36 6.36 0 0 0 6.36 6.36h17v-47z" fill="#57A863"/>
<path d="M23.63.53v47H47v-47z" fill="#FFF"/>
<path d="M63.92.53H47v47h16.92a6.36 6.36 0 0 0 6.36-6.36V6.89A6.36 6.36 0 0 0 63.92.53" fill="#ED5565"/>
</g>
</svg>
);
});
@@ -1,21 +1,22 @@
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { Mod } from "shared/models/mods/mod.interface";
import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
import { CSSProperties, MouseEvent, useMemo, useRef } from "react";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import useDoubleClick from "use-double-click";
import { gt } from "semver";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import striptags from "striptags";
import { safeGt } from "shared/helpers/semver.helpers";
type Props = {
className?: string;
mod: Mod;
mod: BbmFullMod;
installedVersion: string;
isDependency?: boolean;
isSelected?: boolean;
onChange?: (val: boolean) => void;
wantInfo?: boolean;
onWantInfo?: (mod: Mod) => void;
onWantInfo?: (mod: BbmFullMod) => void;
disabled?: boolean;
onUninstall?: () => void;
};
@@ -25,7 +26,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
const themeColor = useThemeColor("second-color");
const clickRef = useRef();
const isChecked = useMemo(() => isDependency || isSelected || mod.required, [isDependency, isSelected, mod.required]);
const isChecked = useMemo(() => isDependency || isSelected || mod.mod.category === BbmCategories.Core, [isDependency, isSelected, mod.mod.category]);
useDoubleClick({
onSingleClick: e => handleWantInfo(e),
@@ -39,7 +40,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
}, [isChecked]);
const wantInfoStyle: CSSProperties = wantInfo ? { borderColor: themeColor } : { borderColor: "transparent" };
const isOutDated = installedVersion ? gt(mod.version, installedVersion) : false;
const isOutDated = installedVersion ? safeGt(mod.version.modVersion, installedVersion) : false;
const handleWantInfo = (e: MouseEvent) => {
e.preventDefault();
@@ -53,19 +54,19 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
return (
<li ref={clickRef} className={`${className} group`}>
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-inherit ml-3 border-2 border-r-0 z-[1] group-hover:brightness-90" style={wantInfoStyle}>
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.required || isDependency || disabled} checked={isChecked} />
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.mod.category === BbmCategories.Core || isDependency || disabled} checked={isChecked} />
</div>
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2 blur-none group-hover:brightness-90" style={wantInfoStyle}>
{mod.name}
{mod.mod.name}
</span>
<span className={`min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90 ${installedVersion && isOutDated && "text-red-400 line-through"} ${installedVersion && !isOutDated && "text-green-400"}`} style={wantInfoStyle}>
{installedVersion || "-"}
</span>
<span className="min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
{mod.version}
{mod.version.modVersion}
</span>
<span title={mod.description} className="px-3 bg-inherit whitespace-nowrap text-ellipsis overflow-hidden py-2 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
{mod.description}
<span title={striptags(mod.mod?.description ?? "", { tagReplacementText: " " })} className="px-3 bg-inherit whitespace-nowrap text-ellipsis overflow-hidden py-2 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
{striptags(mod.mod?.summary ?? "", { tagReplacementText: " " })}
</span>
<div className="h-full bg-inherit flex items-center justify-center mr-3 rounded-r-md pr-2 border-t-2 border-b-2 border-r-2 group-hover:brightness-90" style={wantInfoStyle}>
{installedVersion && (
@@ -2,19 +2,19 @@ import { motion } from "framer-motion";
import { useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { Mod } from "shared/models/mods/mod.interface";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
import { ModItem } from "./mod-item.component";
type Props = {
modsMap: Map<string, Mod[]>;
installed: Map<string, Mod[]>;
modsSelected: Mod[];
onModChange: (selected: boolean, mod: Mod) => void;
moreInfoMod?: Mod;
onWantInfos: (mod: Mod) => void
modsMap: Map<BbmCategories, BbmFullMod[]>;
installed: Map<BbmCategories, BbmFullMod[]>;
modsSelected: BbmFullMod[];
onModChange: (selected: boolean, mod: BbmFullMod) => void;
moreInfoMod?: BbmFullMod;
onWantInfos: (mod: BbmFullMod) => void
disabled?: boolean;
uninstallMod?: (mods: Mod) => void;
uninstallMod?: (mods: BbmFullMod) => void;
uninstallAllMods?: () => void;
unselectAllMods?: () => void;
openModsDropZone?: () => void;
@@ -24,30 +24,31 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
const [filter, setFilter] = useState("");
const [filterEnabled, setFilterEnabled] = useState(false);
const t = useTranslation();
const { text: t } = useTranslationV2();
const installedModVersion = (key: string, mod: Mod): string => {
const installedModVersion = (key: BbmCategories, mod: BbmFullMod): string => {
if (!installed?.get(key)) {
return undefined;
}
const installedMod = installed.get(key).find(m => m.name === mod.name);
const installedMod = installed.get(key).find(m => m.mod.id === mod.mod.id);
if (!installedMod) {
return undefined;
}
return installedMod.version;
return installedMod.version.modVersion;
};
const isDependency = (mod: Mod): boolean => {
return modsSelected.some(m => {
const deps = m.dependencies?.map(dep => Array.from(modsMap.values()).flat().find(m => dep.name === m.name)) ?? [];
if (deps.some(depMod => depMod.name === mod.name)) {
return true;
}
return deps.some(depMod => depMod.dependencies?.some(depModDep => depModDep.name === mod.name));
});
const getAvailableMods = (): BbmFullMod[] => {
return Array.from(modsMap.values()).flat();
}
const isDependency = (mod: BbmFullMod): boolean => {
const selectedModsDepsIds = modsSelected.flatMap(m => m.version.dependencies);
const modsDeps = getAvailableMods().filter(m => selectedModsDepsIds.includes(m.version.id));
const modsDepsDepsIds = modsDeps.flatMap(m => m.version.dependencies);
return selectedModsDepsIds.includes(mod.version.id) || modsDepsDepsIds.includes(mod.version.id);
};
const isSelected = (mod: Mod): boolean => modsSelected.some(m => m.name === mod.name);
const isSelected = (mod: BbmFullMod): boolean => modsSelected.some(m => m.mod.id === mod.mod.id);
const handleInput = (val: string) => setFilter(val.toLowerCase());
@@ -75,21 +76,20 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
]} />
</span>
{Array.from(modsMap.keys()).map(
key =>
modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && (
{Array.from(modsMap.keys()).map(key => modsMap.get(key).some(mod => mod.mod.name.toLowerCase().includes(filter)) && (
<ul key={key} className="contents">
<h2 className="col-span-full py-1 font-bold pl-3">{key}</h2>
{modsMap.get(key).map(mod => mod.name?.toLowerCase().includes(filter) && (
{modsMap.get(key).map(mod => mod.mod.name.toLowerCase().includes(filter) && (
<ModItem
key={mod.name}
key={mod.mod.id}
className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1 hover:cursor-pointer"
mod={mod} installedVersion={installedModVersion(key, mod)}
mod={mod}
installedVersion={installedModVersion(key, mod)}
isDependency={isDependency(mod)}
isSelected={isSelected(mod)}
onChange={val => onModChange(val, mod)}
onWantInfo={onWantInfos}
wantInfo={mod.name === moreInfoMod?.name}
wantInfo={mod.mod.id === moreInfoMod?.mod.id}
disabled={disabled}
onUninstall={() => uninstallMod?.(mod)} />
))}
@@ -1,16 +1,15 @@
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { BSVersion } from "shared/bs-version.interface";
import { Mod } from "shared/models/mods/mod.interface";
import { BbmCategories, BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface";
import { ModsGrid } from "./mods-grid.component";
import { ConfigurationService } from "renderer/services/configuration.service";
import { DefaultConfigKey } from "renderer/config/default-configuration.config";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png";
import BeatConflictImg from "../../../../../../assets/images/apngs/beat-conflict.png";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { lastValueFrom } from "rxjs";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { useTranslation, useTranslationV2 } from "renderer/hooks/use-translation.hook";
import { LinkOpenerService } from "renderer/services/link-opener.service";
import { useInView } from "framer-motion";
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
@@ -26,7 +25,7 @@ import { Dropzone } from "renderer/components/shared/dropzone.component";
export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; onDisclamerDecline: () => void }) {
const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer";
const t = useTranslation();
const { text: t } = useTranslationV2();
const modsManager = useService(BsModsManagerService);
const configService = useService(ConfigurationService);
@@ -37,10 +36,10 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
const ref = useRef(null);
const isVisible = useInView(ref, { amount: 0.5 });
const [modsAvailable, setModsAvailable] = useState(null as Map<string, Mod[]>);
const [modsInstalled, setModsInstalled] = useState(null as Map<string, Mod[]>);
const [modsSelected, setModsSelected] = useState([] as Mod[]);
const [moreInfoMod, setMoreInfoMod] = useState(null as Mod);
const [modsAvailable, setModsAvailable] = useState(null as Map<BbmCategories, BbmFullMod[]>);
const [modsInstalled, setModsInstalled] = useState(null as Map<BbmCategories, BbmFullMod[]>);
const [modsSelected, setModsSelected] = useState([] as BbmFullMod[]);
const [moreInfoMod, setMoreInfoMod] = useState(null as BbmFullMod);
const [reinstallAllMods, setReinstallAllMods] = useState(false);
const isOnline = useObservable(() => os.isOnline$);
const [installing, setInstalling] = useState(false);
@@ -50,86 +49,53 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
const downloadRef = useRef(null);
const [downloadWith, setDownloadWidth] = useState(0);
const modsToCategoryMap = (mods: Mod[]): Map<string, Mod[]> => {
const modsToCategoryMap = (mods: BbmFullMod[]): Map<BbmCategories, BbmFullMod[]> => {
if (!mods) {
return new Map<string, Mod[]>();
return new Map<BbmCategories, BbmFullMod[]>();
}
const map = new Map<string, Mod[]>();
mods.forEach(mod => map.set(mod.category, [...(map.get(mod.category) ?? []), mod]));
const map = new Map<BbmCategories, BbmFullMod[]>();
mods.forEach(mod => map.set(mod.mod.category, [...(map.get(mod.mod.category) ?? []), mod]));
return map;
};
const handleModChange = (selected: boolean, mod: Mod) => {
const handleModChange = (selected: boolean, mod: BbmFullMod) => {
if (selected) {
return setModsSelected(mods => {
if (mods.some(m => m.name === mod.name)) {
if (mods.some(m => m.mod.id === mod.mod.id)) {
return mods;
}
return [...mods, mod];
});
}
setModsSelected(mods => mods.filter(m => m.name !== mod.name));
setModsSelected(mods => mods.filter(m => m.mod.id !== mod.mod.id));
};
const handleMoreInfo = (mod: Mod) => {
if (mod.name === moreInfoMod?.name) {
const handleMoreInfo = (mod: BbmFullMod) => {
if (mod.mod.id === moreInfoMod?.mod.id) {
return setMoreInfoMod(null);
}
setMoreInfoMod(mod);
};
const handleOpenMoreInfo = () => {
if (!moreInfoMod?.link) {
return;
if (moreInfoMod?.mod?.gitUrl) {
linkOpener.open(moreInfoMod.mod.gitUrl);
}
linkOpener.open(moreInfoMod.link);
};
// private isDependency(mod: Mod, selectedMods: Mod[], availableMods: Mod[]) {
// return selectedMods.some(m => {
// const deps = m.dependencies.map(dep => Array.from(availableMods.values()).find(m => dep.name === m.name));
// if (deps.some(depMod => depMod.name === mod.name)) {
// return true;
// }
// return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep.name === mod.name));
// });
// }
// private async resolveDependencies(mods: Mod[], version: BSVersion): Promise<Mod[]> {
// const availableMods = await this.beatModsApi.getVersionMods(version);
// return Array.from(
// new Map<string, Mod>(
// availableMods.reduce((res, mod) => {
// if (mod.required || this.isDependency(mod, mods, availableMods)) {
// res.push([mod.name, mod]);
// }
// return res;
// }, [])
// ).values()
// );
// }
const getAllDependencies = (mods: Mod[], availableMods: Mod[]): Mod[] => {
const collectedDependencies = new Set<Mod>();
const getAllDependencies = (mods: BbmFullMod[], availableMods: BbmFullMod[]): BbmFullMod[] => {
const collectedDependencies = new Set<BbmFullMod>();
const modIdsToProcess = new Set(mods.flatMap(m => m.version.dependencies));
const collectDependencies = (mod: Mod) => {
if (!mod.dependencies) { return; }
for (const dependency of mod.dependencies) {
const dependencyMod = availableMods.find(avMod => avMod.name === dependency.name);
if (dependencyMod && !collectedDependencies.has(dependencyMod)) {
collectedDependencies.add(dependencyMod);
collectDependencies(dependencyMod);
}
for (const currentId of modIdsToProcess) {
const dependency = availableMods.find(m => m.version.id === currentId);
if (dependency && !collectedDependencies.has(dependency)) {
collectedDependencies.add(dependency);
dependency.version.dependencies?.forEach(depId => modIdsToProcess.add(depId));
}
};
mods.forEach(collectDependencies);
availableMods.forEach(mod => {
if(mod.required){
collectedDependencies.add(mod);
}
});
}
return Array.from(collectedDependencies);
};
@@ -145,18 +111,23 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
let modsToInstall = [
...modsSelected,
...getAllDependencies(modsSelected, Array.from(modsAvailable.values()).flat())
]
];
modsToInstall = reinstallAll ? (
modsToInstall // If reinstalling all, we install all selected mods
) : (
modsToInstall.filter(mod => { // Else we only install the mods that are not installed or have a newer version
const installedMod = modsInstalled.get(mod.category)?.find(installedMod => installedMod.name === mod.name);
return !installedMod || lt(installedMod.version, mod.version);
const installedMod = modsInstalled.get(mod.mod.category)?.find(installedMod => installedMod.mod.id === mod.mod.id);
return !installedMod || lt(installedMod.version.modVersion, mod.version.modVersion);
})
);
modsToInstall = Array.from(new Set(modsToInstall)); // Remove duplicates
// Remove duplicates, null and undefined
const set = new Set(modsToInstall);
set.delete(null);
set.delete(undefined);
modsToInstall = Array.from(set); // Remove duplicates
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" });
@@ -172,10 +143,12 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
const importMods = (files: string[]): void => {
setModsDropZoneOpen(false);
modsManager.importMods(files, version);
modsManager.importMods(files, version).then(() => {
loadMods();
});
};
const uninstallMod = (mod: Mod): void => {
const uninstallMod = (mod: BbmFullMod): void => {
setUninstalling(() => true);
lastValueFrom(modsManager.uninstallMod(mod, version)).catch(noop).finally(() => {
loadMods();
@@ -206,17 +179,23 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
return Promise.resolve();
}
const promise = async () => {
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]) => {
const defaultMods = installed?.length ? [] : configService.get<string[]>("default_mods" as DefaultConfigKey);
const defaultMods = installed?.length ? [] : available.filter(m => m.mod.category === BbmCategories.Core || m.mod.category === BbmCategories.Essential);
setModsAvailable(() => modsToCategoryMap(available));
setModsSelected(() => available.filter(m => m.required || defaultMods.some(d => m.name?.toLowerCase() === d?.toLowerCase()) || installed.some(i => m.name === i.name)));
setModsInstalled(() => modsToCategoryMap(installed));
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));
});
};
@@ -1,12 +1,12 @@
import { Observable, BehaviorSubject, throwError, of, lastValueFrom } from "rxjs";
import { catchError, map, tap } from "rxjs/operators";
import { BSVersion } from "shared/bs-version.interface";
import { Mod } from "shared/models/mods";
import { IpcService } from "./ipc.service";
import { ProgressBarService } from "./progress-bar.service";
import { NotificationService } from "./notification.service";
import { Progression } from "main/helpers/fs.helpers";
import { ProgressionInterface } from "shared/models/progress-bar";
import { BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface";
export class BsModsManagerService {
private static instance: BsModsManagerService;
@@ -32,15 +32,15 @@ export class BsModsManagerService {
this.notifications = NotificationService.getInstance();
}
public getAvailableMods(version: BSVersion): Observable<Mod[]> {
public getAvailableMods(version: BSVersion): Observable<BbmFullMod[]> {
return this.ipcService.sendV2("bs-mods.get-available-mods", version);
}
public getInstalledMods(version: BSVersion): Observable<Mod[]> {
public getInstalledMods(version: BSVersion): Observable<BbmModVersion[]> {
return this.ipcService.sendV2("bs-mods.get-installed-mods", version);
}
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
public installMods(mods: BbmFullMod[], version: BSVersion): Observable<Progression> {
if (!this.progressBar.require()) {
return throwError(() => new Error("Action already in progress"));
@@ -72,7 +72,7 @@ export class BsModsManagerService {
});
}
public uninstallMod(mod: Mod, version: BSVersion): Observable<Progression> {
public uninstallMod(mod: BbmFullMod, version: BSVersion): Observable<Progression> {
if (!this.progressBar.require()) {
return throwError(() => new Error("Action already in progress"));
}
+6 -1
View File
@@ -1,4 +1,4 @@
import { coerce, lt, valid } from "semver";
import { coerce, lt, gt, valid } from "semver";
import { tryit } from "./error.helpers";
export function safeLt(a: string, b: string): boolean {
@@ -6,3 +6,8 @@ export function safeLt(a: string, b: string): boolean {
return result ?? false;
}
export function safeGt(a: string, b: string): boolean {
const { result } = tryit(() => gt(valid(coerce(a)), valid(coerce(b))));
return result ?? false;
}
+5 -6
View File
@@ -11,7 +11,6 @@ import { DepotDownloaderEvent } from "../bs-version-download/depot-downloader.mo
import { MSGetQuery, MSModel, MSModelType } from "../models/model-saber.model";
import { ModelDownload } from "renderer/services/models-management/models-downloader.service";
import { BsmLocalModel } from "../models/bsm-local-model.interface";
import { Mod } from "../mods";
import { BPList, DownloadPlaylistProgressionData } from "../playlists/playlist.interface";
import { VersionLinkerAction } from "renderer/services/version-folder-linker.service";
import { FileFilter, OpenDialogOptions, OpenDialogReturnValue } from "electron";
@@ -20,7 +19,7 @@ import { Supporter } from "../supporters";
import { AppWindow } from "../window-manager/app-window.model";
import { LocalBPList, LocalBPListsDetails } from "../playlists/local-playlist.models";
import { StaticConfigGetIpcRequestResponse, StaticConfigKeys, StaticConfigSetIpcRequest } from "main/services/static-configuration.service";
import { ExternalMod } from "../mods/mod.interface";
import { BbmFullMod, BbmModVersion, ExternalMod } from "../mods/mod.interface";
import { OculusDownloadInfo } from "main/services/bs-version-download/bs-oculus-downloader.service";
export type IpcReplier<T> = (data: Observable<T>) => void;
@@ -83,11 +82,11 @@ export interface IpcChannelMapping {
"delete-models": { request: BsmLocalModel[], response: Progression<BsmLocalModel[]> };
/* ** bs-mods-ipcs ** */
"bs-mods.get-available-mods": { request: BSVersion, response: Mod[] };
"bs-mods.get-installed-mods": { request: BSVersion, response: Mod[] };
"bs-mods.get-available-mods": { request: BSVersion, response: BbmFullMod[] };
"bs-mods.get-installed-mods": { request: BSVersion, response: BbmModVersion[] };
"bs-mods.import-mods": { request: { paths: string[]; version: BSVersion; }, response: Progression<ExternalMod> };
"bs-mods.install-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
"bs-mods.uninstall-mods": { request: { mods: Mod[]; version: BSVersion }, response: Progression };
"bs-mods.install-mods": { request: { mods: BbmFullMod[]; version: BSVersion }, response: Progression };
"bs-mods.uninstall-mods": { request: { mods: BbmFullMod[]; version: BSVersion }, response: Progression };
"bs-mods.uninstall-all-mods": { request: BSVersion, response: Progression };
/* ** bs-playlist-ipcs ** */
-1
View File
@@ -1,2 +1 @@
export { ModInstallProgression, InstallModsResult, UninstallModsResult } from "./mod-ipc.model";
export { Mod, DownloadLink, DownloadLinkType, FileHashes, ModAuthor } from "./mod.interface";
+96 -37
View File
@@ -1,42 +1,101 @@
export interface Mod {
_id: string;
name: string;
version: string;
gameVersion: string;
authorId: string;
uploadedDate: string;
updatedDate: string;
author: ModAuthor;
description: string;
link: string;
category: string;
downloads: DownloadLink[];
required: boolean;
dependencies: Mod[];
status: string;
}
export interface ModAuthor {
_id: string;
username: string;
lastLogin: string;
}
export interface DownloadLink {
type: DownloadLinkType;
url: string;
hashMd5: FileHashes[];
}
export type DownloadLinkType = "universal" | "steam" | "oculus";
export interface FileHashes {
hash: string;
file: string;
}
// Any mods that are not supported in beatmods
export interface ExternalMod {
name: string;
files: string[];
}
// BBM Mods
export interface BbmFullMod {
mod: BbmMod;
version: BbmModVersion;
}
export interface BbmMod {
id: number;
name: string;
summary: string;
description: string;
gameName: "BeatSaber";
category: BbmCategories;
authors: BbmUserAPIResponse[];
status: BbmStatus;
iconFileName: string;
gitUrl: string;
lastApprovedById: number;
lastUpdatedById: number;
createdAt: Date;
updatedAt: Date;
}
export interface BbmModVersion {
id: number;
modId: number;
author: BbmUserAPIResponse;
modVersion: string;
platform: BbmPlatform;
zipHash: string;
status: BbmStatus;
dependencies: number[];
contentHashes: BbmContentHash[];
supportedGameVersions: BbmGameVersion[];
downloadCount: number;
lastApprovedById?: number;
lastUpdatedById?: number;
createdAt?: Date;
updatedAt?: Date;
}
export interface BbmContentHash {
path: string;
hash: string;
}
export enum BbmStatus {
Private = "private",
Removed = "removed",
Unverified = "unverified",
Verified = "verified",
}
export enum BbmPlatform {
SteamPC = `steampc`,
OculusPC = `oculuspc`,
UniversalPC = `universalpc`,
UniversalQuest = `universalquest`,
}
export interface BbmGameVersion {
readonly id: number;
gameName: "BeatSaber";
version: string; // semver
defaultVersion: boolean;
}
export enum BbmCategories {
Core = "core",
Essential = "essential",
Library = "library",
Cosmetic = "cosmetic",
PracticeTraining = "practice",
Gameplay = "gameplay",
StreamTools = "streamtools",
UIEnhancements = "ui",
Lighting = "lighting",
TweaksTools = "tweaks",
Multiplayer = "multiplayer",
TextChanges = "text",
Editor = "editor",
Other = "other",
}
export interface BbmUserAPIResponse {
id: number;
username: string;
githubId: string;
sponsorUrl: string;
displayName: string;
bio: string;
createdAt?: Date;
updatedAt?: Date;
}