mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Apply mods changes from [1.4.15-1.4.17] + some clean
This commit is contained in:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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" });
|
||||
@@ -175,7 +146,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
modsManager.importMods(files, version);
|
||||
};
|
||||
|
||||
const uninstallMod = (mod: Mod): void => {
|
||||
const uninstallMod = (mod: BbmFullMod): void => {
|
||||
setUninstalling(() => true);
|
||||
lastValueFrom(modsManager.uninstallMod(mod, version)).catch(noop).finally(() => {
|
||||
loadMods();
|
||||
@@ -206,17 +177,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"));
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user