mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
[chore] migrate to the new mods repo API
This commit is contained in:
@@ -3,7 +3,7 @@ import { BsModsManagerService } from "../services/mods/bs-mods-manager.service";
|
||||
import { UtilsService } from "../services/utils.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
import { BbmFullMod } from "shared/models/mods/mod.interface";
|
||||
import { InstallModsResult } from "shared/models/mods";
|
||||
import log from "electron-log";
|
||||
import { IpcService } from "../services/ipc.service";
|
||||
@@ -21,7 +21,7 @@ ipc.on<BSVersion>("get-installed-mods", (req, reply) => {
|
||||
reply(from(modsManager.getInstalledMods(req.args)));
|
||||
});
|
||||
|
||||
ipcMain.on("install-mods", (event, request: IpcRequest<{ mods: Mod[]; version: BSVersion }>) => {
|
||||
ipcMain.on("install-mods", (event, request: IpcRequest<{ mods: BbmFullMod[]; version: BSVersion }>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
@@ -36,7 +36,7 @@ ipcMain.on("install-mods", (event, request: IpcRequest<{ mods: Mod[]; version: B
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on("uninstall-mods", (event, request: IpcRequest<{ mods: Mod[]; version: BSVersion }>) => {
|
||||
ipcMain.on("uninstall-mods", (event, request: IpcRequest<{ mods: BbmFullMod[]; version: BSVersion }>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
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";
|
||||
|
||||
export class BeatModsApiService {
|
||||
private static instance: BeatModsApiService;
|
||||
|
||||
private readonly requestService: RequestService;
|
||||
|
||||
public readonly MODS_REPO_URL = "https://bbm.saera.gay";
|
||||
private readonly BEAT_MODS_API_URL = `${this.MODS_REPO_URL}/api`;
|
||||
public readonly MODS_REPO_URL = "https://beatmods.com";
|
||||
private readonly MODS_REPO_API_URL = `${this.MODS_REPO_URL}/api`;
|
||||
|
||||
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) {
|
||||
@@ -26,65 +25,47 @@ export class BeatModsApiService {
|
||||
this.requestService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
private convertToBeatModsMod(mod: Mod): Mod {
|
||||
|
||||
}
|
||||
|
||||
private getVersionModsUrl(version: BSVersion): string {
|
||||
return `${this.BEAT_MODS_API_URL}/mods?status=verified&gameVersion=${version.BSVersion}`;
|
||||
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 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);
|
||||
}
|
||||
|
||||
return this.requestService.getJSON<Mod[]>(this.getVersionModsUrl(version)).then(mods => {
|
||||
mods = mods.map(mod => this.asignDependencies(mod, mods));
|
||||
this.versionModsCache.set(version.BSVersion, mods);
|
||||
return this.requestService.getJSON<{ mods: {mod: BbmMod, latest: BbmModVersion}[] }>(this.getVersionModsUrl(version)).then(res => {
|
||||
const fullMods: BbmFullMod[] = res?.mods?.map(mod => ({ mod: mod.mod, version: mod.latest })) ?? [];
|
||||
this.versionModsCache.set(version.BSVersion, fullMods);
|
||||
|
||||
this.updateModsHashCache(mods);
|
||||
this.updateModsHashCache(fullMods.map(mod => mod.version));
|
||||
|
||||
return mods;
|
||||
return fullMods;
|
||||
});
|
||||
}
|
||||
|
||||
public getModByHash(hash: string): Promise<Mod> {
|
||||
public getModByHash(hash: string): Promise<BbmModVersion> {
|
||||
if (this.modsHashCache.has(hash)) {
|
||||
return Promise.resolve(this.modsHashCache.get(hash));
|
||||
}
|
||||
|
||||
return this.requestService.getJSON<Mod[]>(`${this.BEAT_MODS_API_URL}/hashlookup?hash=${hash}`).then(mods => {
|
||||
this.updateModsHashCache(mods);
|
||||
return mods.at(0);
|
||||
return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`).then(res => {
|
||||
this.updateModsHashCache(res?.modVersions ?? []);
|
||||
return res?.modVersions?.at(0);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { DownloadLink, InstallModsResult, Mod, ModInstallProgression, UninstallModsResult } from "shared/models/mods";
|
||||
import { InstallModsResult, ModInstallProgression, UninstallModsResult } from "shared/models/mods";
|
||||
import { BeatModsApiService } from "./beat-mods-api.service";
|
||||
import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
import path from "path";
|
||||
@@ -16,6 +16,7 @@ import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
import { pathExistsSync } from "fs-extra";
|
||||
import { BsmZipExtractor } from "../../models/bsm-zip-extractor.class";
|
||||
import crypto from "crypto";
|
||||
import { BbmCategories, BbmFullMod, BbmModVersion } from "../../../shared/models/mods/mod.interface";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
@@ -25,7 +26,7 @@ export class BsModsManagerService {
|
||||
private readonly utilsService: UtilsService;
|
||||
private readonly requestService: RequestService;
|
||||
|
||||
private manifestMatches: Mod[];
|
||||
private manifestMatches: BbmModVersion[];
|
||||
|
||||
private nbModsToInstall = 0;
|
||||
private nbInstalledMods = 0;
|
||||
@@ -48,18 +49,18 @@ export class BsModsManagerService {
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
}
|
||||
|
||||
private async getModFromHash(hash: string): Promise<Mod> {
|
||||
private async getModFromHash(hash: string): Promise<BbmModVersion> {
|
||||
|
||||
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);
|
||||
|
||||
@@ -88,7 +89,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;
|
||||
@@ -104,7 +105,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))) {
|
||||
@@ -177,26 +178,23 @@ 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}`);
|
||||
this.utilsService.ipcSend<ModInstallProgression>("mod-installed", { success: true, data: { name: mod.name, progression: ((this.nbInstalledMods + 1) / this.nbModsToInstall) * 100 } });
|
||||
private async installMod(mod: BbmFullMod, version: BSVersion): Promise<boolean> {
|
||||
log.info("INSTALL MOD", mod.mod.name, "for version", `${version.BSVersion} - ${version.name}`);
|
||||
this.utilsService.ipcSend<ModInstallProgression>("mod-installed", { success: true, data: { name: mod.mod.name, progression: ((this.nbInstalledMods + 1) / this.nbModsToInstall) * 100 } });
|
||||
|
||||
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;
|
||||
@@ -208,18 +206,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 => {
|
||||
@@ -230,7 +228,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 &&
|
||||
@@ -245,23 +243,20 @@ export class BsModsManagerService {
|
||||
return res;
|
||||
}
|
||||
|
||||
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 isDependency(mod: BbmFullMod, selectedMods: BbmFullMod[], availableMods: BbmFullMod[]): boolean {
|
||||
const selectedDepsIds = selectedMods.flatMap(mod => mod.version.dependencies);
|
||||
const modsDeps = availableMods.filter(m => selectedDepsIds.includes(m.version.id));
|
||||
const modsDepsDepsIds = modsDeps.flatMap(m => m.version.dependencies);
|
||||
return selectedDepsIds.includes(mod.version.id) || modsDepsDepsIds.includes(mod.version.id);
|
||||
}
|
||||
|
||||
private async resolveDependencies(mods: Mod[], version: BSVersion): Promise<Mod[]> {
|
||||
private async resolveDependencies(mods: BbmFullMod[], version: BSVersion): Promise<BbmFullMod[]> {
|
||||
const availableMods = await this.beatModsApi.getVersionMods(version);
|
||||
return Array.from(
|
||||
new Map<string, Mod>(
|
||||
new Map<number, BbmFullMod>(
|
||||
availableMods.reduce((res, mod) => {
|
||||
if (mod.required || this.isDependency(mod, mods, availableMods)) {
|
||||
res.push([mod.name, mod]);
|
||||
if (mod.mod.category === BbmCategories.Core || this.isDependency(mod, mods, availableMods)) {
|
||||
res.push([mod.mod.id, mod]);
|
||||
}
|
||||
return res;
|
||||
}, [])
|
||||
@@ -269,9 +264,7 @@ export class BsModsManagerService {
|
||||
);
|
||||
}
|
||||
|
||||
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"));
|
||||
@@ -282,37 +275,38 @@ 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> {
|
||||
private async uninstallMod(mod: BbmFullMod, version: BSVersion): Promise<void> {
|
||||
this.nbUninstalledMods++;
|
||||
this.utilsService.ipcSend<ModInstallProgression>("mod-uninstalled", { success: true, data: { name: mod.name, progression: (this.nbUninstalledMods / this.nbModsToUninstall) * 100 } });
|
||||
this.utilsService.ipcSend<ModInstallProgression>("mod-uninstalled", { success: true, data: { name: mod.mod.name, progression: (this.nbUninstalledMods / this.nbModsToUninstall) * 100 } });
|
||||
|
||||
if (mod.name.toLowerCase() === "bsipa") {
|
||||
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 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);
|
||||
@@ -322,23 +316,23 @@ 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());
|
||||
}
|
||||
|
||||
public async installMods(mods: Mod[], version: BSVersion): Promise<InstallModsResult> {
|
||||
public async installMods(mods: BbmFullMod[], version: BSVersion): Promise<InstallModsResult> {
|
||||
const deps = await this.resolveDependencies(mods, version);
|
||||
mods.push(...deps);
|
||||
|
||||
@@ -346,9 +340,9 @@ export class BsModsManagerService {
|
||||
throw "no-mods";
|
||||
}
|
||||
|
||||
const bsipa = mods.find(mod => mod.name.toLowerCase() === "bsipa");
|
||||
const bsipa = mods.find(mod => mod.mod.name.toLowerCase() === "bsipa");
|
||||
if (bsipa) {
|
||||
mods = mods.filter(mod => mod.name.toLowerCase() !== "bsipa");
|
||||
mods = mods.filter(mod => mod.mod.name.toLowerCase() !== "bsipa");
|
||||
}
|
||||
|
||||
this.nbModsToInstall = mods.length + (bsipa && 1);
|
||||
@@ -374,7 +368,7 @@ export class BsModsManagerService {
|
||||
};
|
||||
}
|
||||
|
||||
public async uninstallMods(mods: Mod[], version: BSVersion): Promise<UninstallModsResult> {
|
||||
public async uninstallMods(mods: BbmFullMod[], version: BSVersion): Promise<UninstallModsResult> {
|
||||
if (!mods?.length) {
|
||||
throw "no-mods";
|
||||
}
|
||||
@@ -393,16 +387,21 @@ export class BsModsManagerService {
|
||||
}
|
||||
|
||||
public async uninstallAllMods(version: BSVersion): Promise<UninstallModsResult> {
|
||||
const mods = await this.getInstalledMods(version);
|
||||
const versionMods = await this.getAvailableMods(version);
|
||||
const installedMods = await this.getInstalledMods(version);
|
||||
|
||||
if (!mods?.length) {
|
||||
const fullInstalledMods: BbmFullMod[] = installedMods?.map(version => {
|
||||
return { version, mod: versionMods.find(mod => version.modId === mod.mod.id)?.mod };
|
||||
}) ?? [];
|
||||
|
||||
if (!fullInstalledMods?.length) {
|
||||
throw "no-mods";
|
||||
}
|
||||
|
||||
this.nbModsToUninstall = mods.length;
|
||||
this.nbModsToUninstall = fullInstalledMods.length;
|
||||
this.nbUninstalledMods = 0;
|
||||
|
||||
for (const mod of mods) {
|
||||
for (const mod of fullInstalledMods) {
|
||||
await this.uninstallMod(mod, version);
|
||||
}
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ export class RequestService {
|
||||
const response = await fetch(url, this.getInitWithOptions(options));
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status} ${url}`);
|
||||
throw new Error(`HTTP error! status: ${response.status} response: ${await response.text().catch(() => "No response")} url: ${url}`);
|
||||
}
|
||||
|
||||
return await response.json();
|
||||
|
||||
@@ -3,9 +3,9 @@ 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";
|
||||
import { BbmMod } from "shared/models/mods/mod.interface";
|
||||
|
||||
export const UninstallModModal: ModalComponent<void, Mod> = ({ resolver, data }) => {
|
||||
export const UninstallModModal: ModalComponent<void, BbmMod> = ({ resolver, data }) => {
|
||||
const mod = data;
|
||||
const t = useTranslation();
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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 { CSSProperties, useRef } from "react";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
|
||||
@@ -9,8 +8,9 @@ import { PageStateService } from "renderer/services/page-state.service";
|
||||
import useDoubleClick from "use-double-click";
|
||||
import { gt } from "semver";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
|
||||
|
||||
type Props = { className?: string; mod: Mod; installedVersion: string; isDependency?: boolean; isSelected?: boolean; onChange?: (val: boolean) => void; wantInfo?: boolean; onWantInfo?: (mod: Mod) => void };
|
||||
type Props = { className?: string; mod: BbmFullMod; installedVersion: string; isDependency?: boolean; isSelected?: boolean; onChange?: (val: boolean) => void; wantInfo?: boolean; onWantInfo?: (mod: BbmFullMod) => void };
|
||||
|
||||
export function ModItem({ className, mod, installedVersion, isDependency, isSelected, onChange, wantInfo, onWantInfo }: Props) {
|
||||
const modsManager = useService(BsModsManagerService);
|
||||
@@ -28,7 +28,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
|
||||
});
|
||||
|
||||
const wantInfoStyle: CSSProperties = wantInfo ? { borderColor: themeColor } : { borderColor: "transparent" };
|
||||
const isOutDated = installedVersion ? gt(mod.version, installedVersion) : false;
|
||||
const isOutDated = installedVersion ? gt(mod.version.modVersion, installedVersion) : false;
|
||||
|
||||
const uninstall = () => {
|
||||
modsManager.uninstallMod(mod, pageState.getState());
|
||||
@@ -43,24 +43,24 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
|
||||
onChange(!isChecked);
|
||||
};
|
||||
|
||||
const isChecked = isDependency || isSelected || mod.required;
|
||||
const isChecked = isDependency || isSelected || mod.mod.category === BbmCategories.Core;
|
||||
|
||||
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} disabled={mod.required || isDependency} checked={isChecked} />
|
||||
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={onChange} disabled={mod.mod.category === BbmCategories.Core || isDependency} 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={mod.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.mod.summary}
|
||||
</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 && (
|
||||
|
||||
@@ -5,17 +5,17 @@ import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-butto
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
|
||||
import { PageStateService } from "renderer/services/page-state.service";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
|
||||
import { ModItem } from "./mod-item.component";
|
||||
import { useService } from "renderer/hooks/use-service.hook";
|
||||
|
||||
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
|
||||
unselectAllMods?: () => void;
|
||||
};
|
||||
|
||||
@@ -28,32 +28,29 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
|
||||
const [filterEnabled, setFilterEnabled] = useState(false);
|
||||
const t = useTranslation();
|
||||
|
||||
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());
|
||||
|
||||
@@ -86,10 +83,10 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
|
||||
|
||||
{Array.from(modsMap.keys()).map(
|
||||
key =>
|
||||
modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && (
|
||||
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) && <ModItem key={mod.name} 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)} isDependency={isDependency(mod)} isSelected={isSelected(mod)} onChange={val => onModChange(val, mod)} onWantInfo={onWantInfos} wantInfo={mod.name === moreInfoMod?.name} />)}
|
||||
<h2 className="col-span-full py-1 font-bold pl-3 capitalize">{key}</h2>
|
||||
{modsMap.get(key).map(mod => mod.mod.name.toLowerCase().includes(filter) && <ModItem 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)} isDependency={isDependency(mod)} isSelected={isSelected(mod)} onChange={val => onModChange(val, mod)} onWantInfo={onWantInfos} wantInfo={mod.mod.id === moreInfoMod?.mod.id} />)}
|
||||
</ul>
|
||||
)
|
||||
)}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { 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 } 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";
|
||||
@@ -31,49 +31,46 @@ 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 isOnline = useObservable(() => os.isOnline$);
|
||||
const installing = useObservable(() => modsManager.isInstalling$);
|
||||
|
||||
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([...modsSelected, mod]);
|
||||
}
|
||||
const mods = [...modsSelected];
|
||||
mods.splice(
|
||||
mods.findIndex(m => m.name === mod.name),
|
||||
1
|
||||
);
|
||||
mods.splice(mods.findIndex(m => m.mod.id === mod.mod.id), 1);
|
||||
setModsSelected(mods);
|
||||
};
|
||||
|
||||
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) {
|
||||
if (!moreInfoMod?.mod?.gitUrl) {
|
||||
return;
|
||||
}
|
||||
linkOpener.open(moreInfoMod.link);
|
||||
linkOpener.open(moreInfoMod.mod.gitUrl);
|
||||
};
|
||||
|
||||
const installMods = () => {
|
||||
@@ -81,14 +78,14 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
return;
|
||||
}
|
||||
const modsToInstall = modsSelected.filter(mod => {
|
||||
const corespondingMod = modsAvailable.get(mod.category).find(availabeMod => availabeMod._id === mod._id);
|
||||
const installedMod = modsInstalled.get(mod.category)?.find(installedMod => installedMod.name === mod.name);
|
||||
const corespondingMod = modsAvailable.get(mod.mod.category).find(availabeMod => availabeMod.mod.id === mod.mod.id);
|
||||
const installedMod = modsInstalled.get(mod.mod.category)?.find(installedMod => installedMod.mod.id === mod.mod.id);
|
||||
|
||||
if (corespondingMod?.version && lt(corespondingMod.version, mod.version)) {
|
||||
if (corespondingMod?.version && lt(corespondingMod.version.modVersion, mod.version.modVersion)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if(installedMod?.version && lt(mod.version, installedMod?.version)){
|
||||
if(installedMod?.version && lt(mod.version.modVersion, installedMod?.version.modVersion)){
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -111,8 +108,12 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
|
||||
]).then(([available, installed]) => {
|
||||
const defaultMods = installed?.length ? [] : configService.get<string[]>("default_mods" as DefaultConfigKey);
|
||||
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;
|
||||
});
|
||||
setModsSelected(available.filter(m => m.mod.category === BbmCategories.Core || defaultMods.some(d => m.mod.name.toLowerCase() === d.toLowerCase()) || installedMods.some(i => m.mod.id === i.mod.id)));
|
||||
setModsInstalled(modsToCategoryMap(installedMods));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import { UninstallModModal } from "renderer/components/modal/modal-types/uninsta
|
||||
import { Observable, BehaviorSubject } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { InstallModsResult, UninstallModsResult, Mod, ModInstallProgression } from "shared/models/mods";
|
||||
import { InstallModsResult, UninstallModsResult, ModInstallProgression } from "shared/models/mods";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ModalExitCode, ModalService } from "./modale.service";
|
||||
@@ -11,6 +11,7 @@ import { NotificationType } from "../../shared/models/notification/notification.
|
||||
import { OsDiagnosticService } from "./os-diagnostic.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
@@ -41,15 +42,15 @@ export class BsModsManagerService {
|
||||
this.os = OsDiagnosticService.getInstance();
|
||||
}
|
||||
|
||||
public getAvailableMods(version: BSVersion): Observable<Mod[]> {
|
||||
return this.ipcService.sendV2<Mod[], BSVersion>("get-available-mods", { args: version });
|
||||
public getAvailableMods(version: BSVersion): Observable<BbmFullMod[]> {
|
||||
return this.ipcService.sendV2<BbmFullMod[], BSVersion>("get-available-mods", { args: version });
|
||||
}
|
||||
|
||||
public getInstalledMods(version: BSVersion): Observable<Mod[]> {
|
||||
return this.ipcService.sendV2<Mod[], BSVersion>("get-installed-mods", { args: version });
|
||||
public getInstalledMods(version: BSVersion): Observable<BbmModVersion[]> {
|
||||
return this.ipcService.sendV2<BbmModVersion[], BSVersion>("get-installed-mods", { args: version });
|
||||
}
|
||||
|
||||
public installMods(mods: Mod[], version: BSVersion): Promise<void> {
|
||||
public installMods(mods: BbmFullMod[], version: BSVersion): Promise<void> {
|
||||
if (this.os.isOffline) {
|
||||
this.notifications.notifyError({
|
||||
title: "notifications.shared.errors.titles.no-internet",
|
||||
@@ -71,7 +72,7 @@ export class BsModsManagerService {
|
||||
this.progressBar.show(progress$, true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
|
||||
|
||||
this.isInstalling$.next(true);
|
||||
return this.ipcService.send<InstallModsResult, { mods: Mod[]; version: BSVersion }>("install-mods", { args: { mods, version } }).then(res => {
|
||||
return this.ipcService.send<InstallModsResult, { mods: BbmFullMod[]; version: BSVersion }>("install-mods", { args: { mods, version } }).then(res => {
|
||||
if (res.success && res.data) {
|
||||
const isFullyInstalled = res.data.nbInstalledMods === res.data.nbModsToInstall;
|
||||
const title = `notifications.mods.install-mods.titles.${isFullyInstalled ? "success" : "warning"}`;
|
||||
@@ -85,12 +86,12 @@ export class BsModsManagerService {
|
||||
this.progressBar.hide();
|
||||
});
|
||||
}
|
||||
public async uninstallMod(mod: Mod, version: BSVersion): Promise<void> {
|
||||
public async uninstallMod(mod: BbmFullMod, version: BSVersion): Promise<void> {
|
||||
if (!this.progressBar.require()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const modalRes = await this.modals.openModal(UninstallModModal, mod);
|
||||
const modalRes = await this.modals.openModal(UninstallModModal, mod.mod);
|
||||
|
||||
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
|
||||
return;
|
||||
|
||||
@@ -37,6 +37,28 @@ export interface FileHashes {
|
||||
|
||||
// BBM MOD
|
||||
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user