mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Merge branch 'mods-management'
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 260 KiB |
@@ -14,6 +14,24 @@
|
||||
"desktop": "Desktop Mod",
|
||||
"debug": "Debug Mod"
|
||||
},
|
||||
"mods":{
|
||||
"loading-mods": "Loading mods...",
|
||||
"buttons":{
|
||||
"more-infos": "More info",
|
||||
"install-or-update": "Install or update"
|
||||
},
|
||||
"mods-grid":{
|
||||
"header-bar":{
|
||||
"name": "Name",
|
||||
"installed": "Installed",
|
||||
"latest": "Latest",
|
||||
"description":"Description",
|
||||
"dropdown":{
|
||||
"uninstall-all":"Uninstall all"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown":{
|
||||
"open-folder": "Open folder",
|
||||
"verify-files": "Verify files",
|
||||
@@ -81,6 +99,11 @@
|
||||
"warning": "⚠️ Warning",
|
||||
"success": "🎉 Success"
|
||||
},
|
||||
"common":{
|
||||
"msg":{
|
||||
"error-occurred": "An error has occurred"
|
||||
}
|
||||
},
|
||||
"shared":{
|
||||
"errors":{
|
||||
"titles":{
|
||||
@@ -196,7 +219,44 @@
|
||||
"CloningFinished": "Cloning complete 🎉"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mods":{
|
||||
"install-mods":{
|
||||
"titles":{
|
||||
"success": "Mods installed 🎉",
|
||||
"warning": "Mods installed 🤔"
|
||||
},
|
||||
"msg":{
|
||||
"success": "All mods have been installed.",
|
||||
"warning": "One or more mods could not be installed.",
|
||||
"errors":{
|
||||
"no-mods": "No mods to install.",
|
||||
"cannot-install-bsipa": "BSIPA installation failed 😨"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"titles":{
|
||||
"success": "Mod uninstalled 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"errors":{
|
||||
"no-mods": "This mod isn't installed 😑"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"titles":{
|
||||
"success": "Mods uninstalled 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"success": "All mods have been uninstalled.",
|
||||
"errors": {
|
||||
"no-mods": "No mod is installed in this version 😑"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals":{
|
||||
"guard":{
|
||||
@@ -256,7 +316,16 @@
|
||||
"buttons":{
|
||||
"submit": "Clone"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"title": "Uninstall",
|
||||
"description": "Are you sure you want to uninstall {mod}? It could cause other installed mods to malfunction.",
|
||||
"description-bsipa": "Are you sure you want to uninstall BSIPA? After that all installed mods will not work anymore."
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"title": "Uninstalling mods",
|
||||
"description": "Are you sure you want to uninstall all the mods from version {version}? This operation cannot be undone."
|
||||
}
|
||||
},
|
||||
"auto-update":{
|
||||
"checking": "Checking for updates",
|
||||
|
||||
@@ -14,6 +14,24 @@
|
||||
"desktop": "Desktop Mod",
|
||||
"debug": "Debug Mod"
|
||||
},
|
||||
"mods":{
|
||||
"loading-mods": "Cargando mods...",
|
||||
"buttons":{
|
||||
"more-infos": "Más información",
|
||||
"install-or-update": "Instalar o actualizar"
|
||||
},
|
||||
"mods-grid":{
|
||||
"header-bar":{
|
||||
"name": "Nombre",
|
||||
"installed": "Instalado",
|
||||
"latest": "Último",
|
||||
"description":"Descripción",
|
||||
"dropdown":{
|
||||
"uninstall-all":"Desinstalar todos"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown":{
|
||||
"open-folder": "Abrir la carpeta",
|
||||
"verify-files": "Verificar los archivos",
|
||||
@@ -81,6 +99,11 @@
|
||||
"warning": "⚠️ Advertencia",
|
||||
"success": "🎉 Éxito"
|
||||
},
|
||||
"common":{
|
||||
"msg":{
|
||||
"error-occurred": "Se ha producido un error"
|
||||
}
|
||||
},
|
||||
"shared":{
|
||||
"errors":{
|
||||
"titles":{
|
||||
@@ -196,7 +219,44 @@
|
||||
"CloningFinished": "Clonación completa 🎉"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mods":{
|
||||
"install-mods":{
|
||||
"titles":{
|
||||
"success": "Mods instalados 🎉",
|
||||
"warning": "Mods instalados 🤔"
|
||||
},
|
||||
"msg":{
|
||||
"success": "Todos los mods han sido instalados.",
|
||||
"warning": "No se han podido instalar uno o más mods.",
|
||||
"errors":{
|
||||
"no-mods": "No hay que instalar mods.",
|
||||
"cannot-install-bsipa": "La instalación de BSIPA falló 😨"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"titles":{
|
||||
"success": "Mod desinstalado 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"errors":{
|
||||
"no-mods": "Este mod no está instalado 😑"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"titles":{
|
||||
"success": "Mods desinstalados 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"success": "Todos los mods han sido desinstalados.",
|
||||
"errors": {
|
||||
"no-mods": "No se instala ningún mod en esta versión 😑"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals":{
|
||||
"guard":{
|
||||
@@ -256,7 +316,16 @@
|
||||
"buttons":{
|
||||
"submit": "Clonar"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"title": "Desinstalar",
|
||||
"description": "¿Estás seguro de que quieres desinstalar {mod}? Podría provocar el mal funcionamiento de otros mods instalados.",
|
||||
"description-bsipa": "¿Estás seguro de que quieres desinstalar BSIPA? Después de eso, todos los mods instalados dejarán de funcionar."
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"title": "Desinstalación de mods",
|
||||
"description": "¿Estás seguro de que quieres desinstalar todos los mods de la versión {version}? Esta operación no se puede deshacer."
|
||||
}
|
||||
},
|
||||
"auto-update":{
|
||||
"checking": "Comprobar las actualizaciones",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"download": "Télécharger",
|
||||
"verify": "Vérifier",
|
||||
"launch": "Lancer",
|
||||
"mods": "Modes",
|
||||
"mods": "Mods",
|
||||
"maps": "Maps",
|
||||
"cancel": "Annuler"
|
||||
},
|
||||
@@ -14,6 +14,24 @@
|
||||
"desktop": "Mode Bureau",
|
||||
"debug": "Mode Debug"
|
||||
},
|
||||
"mods":{
|
||||
"loading-mods": "Chargement des mods...",
|
||||
"buttons":{
|
||||
"more-infos": "Plus d'infos",
|
||||
"install-or-update": "Installer ou mettre à jour"
|
||||
},
|
||||
"mods-grid":{
|
||||
"header-bar":{
|
||||
"name": "Nom",
|
||||
"installed": "Installé",
|
||||
"latest": "Récent",
|
||||
"description":"Description",
|
||||
"dropdown":{
|
||||
"uninstall-all":"Tout désinstaller"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"dropdown":{
|
||||
"open-folder": "Ouvrir le dossier",
|
||||
"verify-files": "Vérifier les fichiers",
|
||||
@@ -81,6 +99,11 @@
|
||||
"warning": "⚠️ Attention",
|
||||
"success": "🎉 Succès"
|
||||
},
|
||||
"common":{
|
||||
"msg":{
|
||||
"error-occurred": "Une erreur c'est produite"
|
||||
}
|
||||
},
|
||||
"shared":{
|
||||
"errors":{
|
||||
"titles":{
|
||||
@@ -196,7 +219,44 @@
|
||||
"CloningFinished": "Clonage terminer 🎉"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mods":{
|
||||
"install-mods":{
|
||||
"titles":{
|
||||
"success": "Mods installés 🎉",
|
||||
"warning": "Mods installés 🤔"
|
||||
},
|
||||
"msg":{
|
||||
"success": "Tous les mods on été installés.",
|
||||
"warning": "Un ou plusieurs mods n'ont pas pu être installés.",
|
||||
"errors":{
|
||||
"no-mods": "Aucun mods à installer.",
|
||||
"cannot-install-bsipa": "L'installation de BSIPA à échoué 😨"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"titles":{
|
||||
"success": "Mod désintallé 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"errors":{
|
||||
"no-mods": "Ce mod n'est pas installé 😑"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"titles":{
|
||||
"success": "Mods désintallés 🎉"
|
||||
},
|
||||
"msg":{
|
||||
"success": "Tous les mods on été désintallés.",
|
||||
"errors": {
|
||||
"no-mods": "Aucun mod n'est installé dans dans cette version 😑"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"modals":{
|
||||
"guard":{
|
||||
@@ -232,7 +292,7 @@
|
||||
},
|
||||
"bs-uninstall":{
|
||||
"title": "Désinstaller",
|
||||
"description": "Est tu sûr de vouloir désinstaller BeatSaber {version} ? Tu vas devoir la retélécharger pour y jouer.",
|
||||
"description": "Est-tu sûr de vouloir désinstaller BeatSaber {version} ? Tu vas devoir la retélécharger pour y jouer.",
|
||||
"buttons":{
|
||||
"submit": "Désinstaller"
|
||||
}
|
||||
@@ -256,7 +316,16 @@
|
||||
"buttons":{
|
||||
"submit": "Cloner"
|
||||
}
|
||||
}
|
||||
},
|
||||
"uninstall-mod":{
|
||||
"title": "Désinstaller",
|
||||
"description": "Es-tu sûr de vouloir désinstaller {mod} ? Cela pourrait faire dysfonctionner d'autres mods installés.",
|
||||
"description-bsipa": "Es-tu sûr de vouloir désinstaller BSIPA ? Après cela, tous les mods installés ne fonctionneront plus."
|
||||
},
|
||||
"uninstall-all-mods":{
|
||||
"title": "Désinstaller les mods",
|
||||
"description": "Es-tu sûr de vouloir désinstaller tous les mods de la version {version} ? Cette opération ne pourra pas être annulée."
|
||||
}
|
||||
},
|
||||
"auto-update":{
|
||||
"checking": "Vérification des mises à jour",
|
||||
|
||||
Generated
+18
@@ -9024,6 +9024,11 @@
|
||||
"escape-string-regexp": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"md5-file": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/md5-file/-/md5-file-5.0.0.tgz",
|
||||
"integrity": "sha512-xbEFXCYVWrSx/gEKS1VPlg84h/4L20znVIulKw6kMfmBUAZNAnF00eczz9ICMl+/hjQGo5KSXRxbL/47X3rmMw=="
|
||||
},
|
||||
"mdn-data": {
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz",
|
||||
@@ -9389,6 +9394,11 @@
|
||||
"integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==",
|
||||
"dev": true
|
||||
},
|
||||
"node-stream-zip": {
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/node-stream-zip/-/node-stream-zip-1.15.0.tgz",
|
||||
"integrity": "sha512-LN4fydt9TqhZhThkZIVQnF9cwjU3qmUH9h78Mx/K7d3VvfRqqwthLwJEUOEL0QPZ0XQmNN7be5Ggit5+4dq3Bw=="
|
||||
},
|
||||
"nopt": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz",
|
||||
@@ -10771,6 +10781,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-visibility-sensor": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmjs.org/react-visibility-sensor/-/react-visibility-sensor-5.1.1.tgz",
|
||||
"integrity": "sha512-cTUHqIK+zDYpeK19rzW6zF9YfT4486TIgizZW53wEZ+/GPBbK7cNS0EHyJVyHYacwFEvvHLEKfgJndbemWhB/w==",
|
||||
"requires": {
|
||||
"prop-types": "^15.7.2"
|
||||
}
|
||||
},
|
||||
"read-cache": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz",
|
||||
|
||||
@@ -232,12 +232,15 @@
|
||||
"framer-motion": "^7.2.1",
|
||||
"history": "^5.3.0",
|
||||
"is-online": "^9.0.1",
|
||||
"md5-file": "^5.0.0",
|
||||
"node-stream-zip": "^1.15.0",
|
||||
"react": "^18.2.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-fitty": "^1.0.1",
|
||||
"react-outside-click-handler": "^1.3.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"react-visibility-sensor": "^5.1.1",
|
||||
"regedit": "^5.1.1",
|
||||
"rxjs": "^7.5.6",
|
||||
"tailwind-scrollbar-hide": "^1.1.7",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "BSManager",
|
||||
"name": "bs-manager",
|
||||
"version": "0.1.0",
|
||||
"description": "A foundation for scalable desktop apps",
|
||||
"main": "./dist/main/main.js",
|
||||
|
||||
@@ -10,7 +10,6 @@ ipcMain.on('bs-launch.launch', (event, request: IpcRequest<LauchOption>) => {
|
||||
const utilsService = UtilsService.getInstance();
|
||||
|
||||
launcherService.launch(request.args).then(res => {
|
||||
console.log("BEAT SABER LANCER");
|
||||
utilsService.ipcSend(request.responceChannel, {success: true, data: res});
|
||||
}).catch((err: BsmException) => {
|
||||
utilsService.ipcSend(request.responceChannel, {success: false, error: err});
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
import { ipcMain } from "electron";
|
||||
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 { InstallModsResult } from "shared/models/mods";
|
||||
import log from "electron-log";
|
||||
|
||||
ipcMain.on("get-available-mods", (event, request: IpcRequest<BSVersion>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager.getAvailableMods(request.args).then(mods => {
|
||||
utils.ipcSend(request.responceChannel, {success: true, data: mods});
|
||||
}).catch(err =>{
|
||||
utils.ipcSend(request.responceChannel, {success: false});
|
||||
log.error("ipc", "get-available-mods", err, request);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on("get-installed-mods", (event, request: IpcRequest<BSVersion>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager.getInstalledMods(request.args).then(mods => {
|
||||
utils.ipcSend(request.responceChannel, {success: true, data: mods});
|
||||
}).catch(err => {
|
||||
utils.ipcSend(request.responceChannel, {success: false});
|
||||
log.error("ipc", "get-installed-mods", err, request);
|
||||
});
|
||||
});
|
||||
|
||||
ipcMain.on("install-mods", (event, request: IpcRequest<{mods: Mod[], version: BSVersion}>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager.installMods(request.args.mods, request.args.version).then(nbInstalled => {
|
||||
utils.ipcSend<InstallModsResult>(request.responceChannel, {success: true, data: nbInstalled})
|
||||
}).catch(err => {
|
||||
utils.ipcSend(request.responceChannel, {success: false, error: err});
|
||||
log.error("ipc", "install-mods", err, request);
|
||||
})
|
||||
});
|
||||
|
||||
ipcMain.on("uninstall-mods", (event, request: IpcRequest<{mods: Mod[], version: BSVersion}>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager.uninstallMods(request.args.mods, request.args.version).then(nbInstalled => {
|
||||
utils.ipcSend(request.responceChannel, {success: true, data: nbInstalled})
|
||||
}).catch(err => {
|
||||
utils.ipcSend(request.responceChannel, {success: false, error: err});
|
||||
log.error("ipc", "uninstall-mods", err, request);
|
||||
})
|
||||
});
|
||||
|
||||
ipcMain.on("uninstall-all-mods", (event, request: IpcRequest<BSVersion>) => {
|
||||
const utils = UtilsService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
modsManager.uninstallAllMods(request.args).then(nbInstalled => {
|
||||
utils.ipcSend(request.responceChannel, {success: true, data: nbInstalled})
|
||||
}).catch(err => {
|
||||
utils.ipcSend(request.responceChannel, {success: false, error: err});
|
||||
log.error("ipc", "uninstall-all-mods", err, request);
|
||||
})
|
||||
});
|
||||
@@ -7,3 +7,4 @@ import './map-ipcs';
|
||||
import './supporters-ipcs';
|
||||
import './launcher-ipcs';
|
||||
import './window-manager-ipcs';
|
||||
import './bs-mods-ipcs';
|
||||
|
||||
@@ -30,7 +30,6 @@ log.catchErrors();
|
||||
const utilsService = UtilsService.getInstance();
|
||||
utilsService.setAssetsPath(app.isPackaged ? path.join(process.resourcesPath, 'assets') : path.join(__dirname, '../../assets'));
|
||||
|
||||
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
const sourceMapSupport = require('source-map-support');
|
||||
sourceMapSupport.install();
|
||||
|
||||
@@ -90,11 +90,16 @@ export class BSLocalVersionService{
|
||||
return seq.replace( /[<>:"\/\\|?*]+/g, '' );
|
||||
}
|
||||
|
||||
public getVersionFolder(version: BSVersion){
|
||||
return version.name ? `${version.BSVersion}-${version.name}` : version.BSVersion;
|
||||
}
|
||||
public getVersionFolder(version: BSVersion){
|
||||
return version.name ? `${version.BSVersion}-${version.name}` : version.BSVersion;
|
||||
}
|
||||
|
||||
public async getInstalledVersions(): Promise<BSVersion[]>{
|
||||
public getVersionType(version: BSVersion): "steam"|"universal"{
|
||||
if(version.steam){ return "steam"; }
|
||||
return "universal"
|
||||
}
|
||||
|
||||
public async getInstalledVersions(): Promise<BSVersion[]>{
|
||||
const versions: BSVersion[] = [];
|
||||
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber")
|
||||
if(steamBsFolder && this.utilsService.pathExist(steamBsFolder)){
|
||||
|
||||
@@ -3,6 +3,7 @@ import { UtilsService } from './utils.service';
|
||||
import path from 'path';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { BSVersion } from 'shared/bs-version.interface';
|
||||
import { RequestService } from "./request.service"
|
||||
import isOnline from 'is-online';
|
||||
|
||||
export class BSVersionLibService{
|
||||
@@ -13,11 +14,13 @@ export class BSVersionLibService{
|
||||
private static instance: BSVersionLibService;
|
||||
|
||||
private utilsService: UtilsService;
|
||||
private requestService: RequestService
|
||||
|
||||
private bsVersions: BSVersion[] = [];
|
||||
|
||||
private constructor(){
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
this.requestService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
|
||||
@@ -26,19 +29,9 @@ export class BSVersionLibService{
|
||||
return BSVersionLibService.instance;
|
||||
}
|
||||
|
||||
private getRemoteVersions(): Promise<BSVersion[]>{
|
||||
return new Promise<BSVersion[]>((resolve, reject) => {
|
||||
let body = ''
|
||||
get(this.REMOTE_BS_VERSIONS_URL, (res) => {
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
this.bsVersions = JSON.parse(body);
|
||||
resolve(this.bsVersions);
|
||||
});
|
||||
res.on('error', () => reject(null))
|
||||
})
|
||||
})
|
||||
}
|
||||
private getRemoteVersions(): Promise<BSVersion[]>{
|
||||
return this.requestService.get<BSVersion[]>(this.REMOTE_BS_VERSIONS_URL);
|
||||
}
|
||||
|
||||
private async getLocalVersions(): Promise<BSVersion[]>{
|
||||
const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE);
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { Mod } from "shared/models/mods/mod.interface";
|
||||
import { RequestService } from "../request.service";
|
||||
|
||||
export class BeatModsApiService {
|
||||
|
||||
private static instance: BeatModsApiService;
|
||||
|
||||
private readonly requestService: RequestService;
|
||||
|
||||
private readonly BEAT_MODS_VERSIONS = "https://versions.beatmods.com/versions.json";
|
||||
private readonly BEAT_MODS_ALIAS = "https://alias.beatmods.com/aliases.json";
|
||||
|
||||
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 allModsCache: Mod[];
|
||||
|
||||
public static getInstance(): BeatModsApiService{
|
||||
if(!BeatModsApiService.instance){ BeatModsApiService.instance = new BeatModsApiService(); }
|
||||
return BeatModsApiService.instance;
|
||||
}
|
||||
|
||||
private constructor(){
|
||||
this.requestService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
private getVersionModsUrl(version: BSVersion): string{
|
||||
return `${this.BEAT_MODS_API_URL}mod?status=approved&gameVersion=${version.BSVersion}&sort=&sortDirection=1`;
|
||||
}
|
||||
|
||||
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.get<Record<string, string[]>>(this.BEAT_MODS_ALIAS).then(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(([key, 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;
|
||||
}
|
||||
|
||||
public async getVersionMods(version: BSVersion): Promise<Mod[]>{
|
||||
if(this.versionModsCache.has(version.BSVersion)){ return this.versionModsCache.get(version.BSVersion); }
|
||||
|
||||
const alias = await this.getAliasOfVersion(version);
|
||||
|
||||
return this.requestService.get<Mod[]>(this.getVersionModsUrl(alias)).then(mods => {
|
||||
mods = mods.map(mod => this.asignDependencies(mod, mods));
|
||||
this.versionModsCache.set(version.BSVersion, mods);
|
||||
return mods;
|
||||
});
|
||||
}
|
||||
|
||||
public async getAllMods(): Promise<Mod[]>{
|
||||
if(!!this.allModsCache){ return this.allModsCache; }
|
||||
return this.requestService.get<Mod[]>(this.getAllModsUrl()).then(mods => {
|
||||
this.allModsCache = mods;
|
||||
return this.allModsCache;
|
||||
});
|
||||
}
|
||||
|
||||
public loadAllMods(){ return this.getAllMods(); }
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,330 @@
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { DownloadLink, InstallModsResult, Mod, ModInstallProgression, UninstallModsResult } from "shared/models/mods";
|
||||
import { BeatModsApiService } from "./beat-mods-api.service";
|
||||
import { BSLocalVersionService } from "../bs-local-version.service"
|
||||
import path from "path";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import md5File from "md5-file";
|
||||
import fs from "fs"
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { RequestService } from "../request.service";
|
||||
import { spawn } from "child_process";
|
||||
import { BS_EXECUTABLE } from "../../constants";
|
||||
import log from "electron-log";
|
||||
|
||||
export class BsModsManagerService {
|
||||
|
||||
private static instance: BsModsManagerService;
|
||||
|
||||
private readonly beatModsApi: BeatModsApiService;
|
||||
private readonly bsLocalService: BSLocalVersionService;
|
||||
private readonly utilsService: UtilsService;
|
||||
private readonly requestService: RequestService
|
||||
|
||||
private manifestMatches: Mod[];
|
||||
|
||||
private nbModsToInstall = 0;
|
||||
private nbInstalledMods = 0;
|
||||
|
||||
private nbModsToUninstall = 0;
|
||||
private nbUninstalledMods = 0;
|
||||
|
||||
public static getInstance(): BsModsManagerService{
|
||||
if(!BsModsManagerService.instance){ BsModsManagerService.instance = new BsModsManagerService(); }
|
||||
return BsModsManagerService.instance;
|
||||
}
|
||||
|
||||
private constructor(){
|
||||
this.beatModsApi = BeatModsApiService.getInstance();
|
||||
this.bsLocalService = BSLocalVersionService.getInstance();
|
||||
this.utilsService = UtilsService.getInstance();
|
||||
this.requestService = RequestService.getInstance();
|
||||
}
|
||||
|
||||
private async getModFromHash(hash: string): Promise<Mod>{
|
||||
const allMods = await this.beatModsApi.getAllMods();
|
||||
return allMods.find(mod => {
|
||||
if(mod.name.toLowerCase() === "bsipa" || mod.status === "declined"){ return false; }
|
||||
return mod.downloads.some(download => download.hashMd5.some(md5 => md5.hash === hash));
|
||||
})
|
||||
}
|
||||
|
||||
private async getIpaFromHash(hash: string): Promise<Mod>{
|
||||
const allMods = await this.beatModsApi.getAllMods();
|
||||
return allMods.find(mod => {
|
||||
if(mod.name.toLowerCase() !== "bsipa"){ return false; }
|
||||
return mod.downloads.some(download => download.hashMd5.some(md5 => md5.hash === hash));
|
||||
})
|
||||
}
|
||||
|
||||
private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise<Mod[]>{
|
||||
const bsPath = await this.bsLocalService.getVersionPath(version);
|
||||
const modsPath = path.join(bsPath, modsDir);
|
||||
if(!this.utilsService.pathExist(modsPath)){ return []; }
|
||||
const files = fs.readdirSync(modsPath);
|
||||
const promises = files.map(f => {
|
||||
return (async() => {
|
||||
const filePath = path.join(modsPath, f)
|
||||
const ext = path.extname(f);
|
||||
if(ext !== ".dll" && ext !== ".exe" && ext !== ".manifest"){ return undefined; }
|
||||
const hash = await md5File(filePath);
|
||||
const mod = await this.getModFromHash(hash);
|
||||
if(!mod){ return undefined; }
|
||||
if(ext === ".manifest"){
|
||||
this.manifestMatches.push(mod);
|
||||
return undefined;
|
||||
}
|
||||
if(filePath.includes("Libs")){
|
||||
if(!this.manifestMatches.some(m => m.name === mod.name)){ return undefined; }
|
||||
const modIndex = this.manifestMatches.indexOf(mod);
|
||||
if(modIndex > -1){ this.manifestMatches.splice(modIndex, 1); }
|
||||
}
|
||||
return mod;
|
||||
})()
|
||||
});
|
||||
const mods = await Promise.all(promises)
|
||||
return mods.filter(m => !!m);
|
||||
}
|
||||
|
||||
private async getBsipaInstalled(version: BSVersion): Promise<Mod>{
|
||||
const bsPath = await this.bsLocalService.getVersionPath(version);
|
||||
const injectorPath = path.join(bsPath, "Beat Saber_Data", "Managed", "IPA.Injector.dll");
|
||||
if(!this.utilsService.pathExist(injectorPath)){ return undefined; }
|
||||
const injectorMd5 = await md5File(injectorPath);
|
||||
return this.getIpaFromHash(injectorMd5);
|
||||
}
|
||||
|
||||
private async downloadZip(zipUrl: string): Promise<{zip: StreamZip.StreamZipAsync, zipPath: string}>{
|
||||
zipUrl = path.join(this.beatModsApi.BEAT_MODS_URL, zipUrl);
|
||||
const fileName = path.basename(zipUrl);
|
||||
const tempPath = this.utilsService.getTempPath();
|
||||
this.utilsService.createFolderIfNotExist(this.utilsService.getTempPath());
|
||||
const dest = path.join(tempPath, fileName);
|
||||
|
||||
const zipPath = await this.requestService.downloadFile(zipUrl, dest);
|
||||
const zip = new StreamZip.async({file : zipPath});
|
||||
|
||||
return {zip, zipPath};
|
||||
}
|
||||
|
||||
private async executeBSIPA(version: BSVersion, args: string[]): Promise<boolean>{
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const ipaPath = path.join(versionPath, "IPA.exe");
|
||||
const bsExePath = path.join(versionPath, BS_EXECUTABLE);
|
||||
if(!this.utilsService.pathExist(ipaPath) || !this.utilsService.pathExist(bsExePath)){ return false; }
|
||||
|
||||
return new Promise<boolean>(resolve => {
|
||||
const processIPA = spawn(`start /wait /min "" "${ipaPath}" ${args.join(" ")}`, {cwd: versionPath, detached: true, shell: true});
|
||||
processIPA.once("exit", code => {
|
||||
if(code === 0){ return resolve(true); }
|
||||
log.error("IPA PROCESS", "exit code", code);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
setTimeout(() => resolve(false), (1 * 60) * 1000); //timeout 1min
|
||||
});
|
||||
}
|
||||
|
||||
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 async installMod(mod: Mod, version: BSVersion): Promise<boolean>{
|
||||
|
||||
this.utilsService.ipcSend<ModInstallProgression>("mod-installed", {success: true, data: {name: mod.name, progression: ((this.nbInstalledMods + 1) / this.nbModsToInstall) * 100}})
|
||||
|
||||
const download = this.getModDownload(mod, version);
|
||||
|
||||
if(!download){ return false; }
|
||||
|
||||
const {zip, zipPath} = await this.downloadZip(download.url);
|
||||
|
||||
if(!zip){ return false; }
|
||||
|
||||
const crypto = require('crypto');
|
||||
const entries = await zip.entries();
|
||||
|
||||
const checkedEntries = (await Promise.all(Object.values(entries).map(async (entry) => {
|
||||
if(!entry.isFile){ return undefined; }
|
||||
const data = await zip.entryData(entry);
|
||||
const entryMd5 = crypto.createHash('md5').update(data).digest('hex')
|
||||
return download.hashMd5.some(md5 => md5.hash === entryMd5) ? entry : undefined;
|
||||
}))).filter(entry => !!entry);
|
||||
|
||||
if(checkedEntries.length != download.hashMd5.length){ return false; }
|
||||
|
||||
const verionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const isBSIPA = mod.name.toLowerCase() === "bsipa";
|
||||
const destDir = isBSIPA ? verionPath : path.join(verionPath, ModsInstallFolder.PENDING);
|
||||
|
||||
const extracted = await zip.extract(null, destDir).then(() => true).catch(err => {log.error(err); return false});
|
||||
|
||||
await zip.close();
|
||||
await this.utilsService.unlinkIfExist(zipPath);
|
||||
|
||||
const res = isBSIPA ? (extracted && (await this.executeBSIPA(version, ["-n"]))) : extracted;
|
||||
|
||||
res && this.nbInstalledMods++;
|
||||
|
||||
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 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(this.isDependency(mod, mods, availableMods)){
|
||||
res.push([mod.name, mod]);
|
||||
}
|
||||
return res;
|
||||
}, [])).values());
|
||||
}
|
||||
|
||||
private async uninstallBSIPA(mod: Mod, version: BSVersion): Promise<void>{
|
||||
const download = this.getModDownload(mod, version);
|
||||
|
||||
const verionPath = await this.bsLocalService.getVersionPath(version);
|
||||
const hasIPAExe = this.utilsService.pathExist(path.join(verionPath, "IPA.exe"));
|
||||
const hasIPADir = this.utilsService.pathExist(path.join(verionPath, "IPA"));
|
||||
|
||||
if(!hasIPADir || !hasIPAExe){ return; }
|
||||
|
||||
await this.executeBSIPA(version, ["--revert", "-n"]);
|
||||
|
||||
const promises = download.hashMd5.map(files => {
|
||||
const file = files.file.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
|
||||
return this.utilsService.unlinkIfExist(path.join(verionPath, file));
|
||||
})
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
private async uninstallMod(mod: Mod, version: BSVersion): Promise<void>{
|
||||
|
||||
this.nbUninstalledMods++;
|
||||
this.utilsService.ipcSend<ModInstallProgression>("mod-uninstalled", {success: true, data: {name: mod.name, progression: (this.nbUninstalledMods / this.nbModsToUninstall) * 100}})
|
||||
|
||||
if(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 => {
|
||||
this.utilsService.unlinkIfExist(path.join(versionPath, files.file));
|
||||
this.utilsService.unlinkIfExist(path.join(versionPath, "IPA", "Pending", files.file));
|
||||
});
|
||||
|
||||
await Promise.all(promises);
|
||||
}
|
||||
|
||||
public getAvailableMods(version: BSVersion): Promise<Mod[]>{
|
||||
return this.beatModsApi.getVersionMods(version);
|
||||
}
|
||||
|
||||
public async getInstalledMods(version: BSVersion): Promise<Mod[]>{
|
||||
this.manifestMatches = [];
|
||||
await this.beatModsApi.loadAllMods();
|
||||
const bsipa = await this.getBsipaInstalled(version);
|
||||
return Promise.all([
|
||||
this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING),
|
||||
this.getModsInDir(version, ModsInstallFolder.LIBS_PENDING),
|
||||
this.getModsInDir(version, ModsInstallFolder.PLUGINS),
|
||||
this.getModsInDir(version, ModsInstallFolder.LIBS)
|
||||
]).then(dirMods => {
|
||||
const res = [];
|
||||
if(!!bsipa){ res.push(bsipa); }
|
||||
const installedMods = Array.from(new Map<string, Mod>(dirMods.flat().map(m => [m.name, m])).values());
|
||||
res.push(...installedMods);
|
||||
return res;
|
||||
});
|
||||
}
|
||||
|
||||
public async installMods(mods: Mod[], version: BSVersion): Promise<InstallModsResult>{
|
||||
if(!mods || !mods.length){ throw "no-mods"; }
|
||||
|
||||
const deps = await this.resolveDependencies(mods, version);
|
||||
mods.push(...deps);
|
||||
|
||||
const bsipa = mods.find(mod => mod.name.toLowerCase() === "bsipa");
|
||||
if(bsipa){ mods = mods.filter(mod => mod.name.toLowerCase() !== "bsipa"); }
|
||||
|
||||
this.nbModsToInstall = mods.length + (bsipa && 1);
|
||||
this.nbInstalledMods = 0;
|
||||
|
||||
if(bsipa){
|
||||
const installed = await this.installMod(bsipa, version).catch(err => {log.error("INSTALL BSIPA", err); return false; })
|
||||
if(!installed){ throw "cannot-install-bsipa"; }
|
||||
}
|
||||
|
||||
for(const mod of mods){
|
||||
await this.installMod(mod, version);;
|
||||
}
|
||||
|
||||
return {
|
||||
nbModsToInstall: this.nbModsToInstall,
|
||||
nbInstalledMods: this.nbInstalledMods
|
||||
};
|
||||
}
|
||||
|
||||
public async uninstallMods(mods: Mod[], version: BSVersion): Promise<UninstallModsResult>{
|
||||
if(!mods || !mods.length){ throw "no-mods"; }
|
||||
|
||||
this.nbModsToUninstall = mods.length;
|
||||
this.nbUninstalledMods = 0;
|
||||
|
||||
for(const mod of mods){
|
||||
await this.uninstallMod(mod, version);
|
||||
}
|
||||
|
||||
return {
|
||||
nbModsToUninstall: this.nbModsToUninstall,
|
||||
nbUninstalledMods: this.nbUninstalledMods
|
||||
};
|
||||
}
|
||||
|
||||
public async uninstallAllMods(version: BSVersion): Promise<UninstallModsResult>{
|
||||
const mods = await this.getInstalledMods(version);
|
||||
|
||||
if(!mods || !mods.length){ throw "no-mods"; }
|
||||
|
||||
this.nbModsToUninstall = mods.length;
|
||||
this.nbUninstalledMods = 0;
|
||||
|
||||
for(const mod of mods){
|
||||
await this.uninstallMod(mod, version);
|
||||
}
|
||||
|
||||
const versionPath = await this.bsLocalService.getVersionPath(version);
|
||||
|
||||
this.utilsService.rmDirIfExist(path.join(versionPath, ModsInstallFolder.PLUGINS));
|
||||
this.utilsService.rmDirIfExist(path.join(versionPath, ModsInstallFolder.LIBS));
|
||||
this.utilsService.rmDirIfExist(path.join(versionPath, ModsInstallFolder.IPA));
|
||||
|
||||
path.resolve
|
||||
|
||||
return {
|
||||
nbModsToUninstall: this.nbModsToUninstall,
|
||||
nbUninstalledMods: this.nbUninstalledMods
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
const enum ModsInstallFolder {
|
||||
PLUGINS = "Plugins",
|
||||
LIBS = "Libs",
|
||||
IPA = "IPA",
|
||||
PENDING = "IPA/Pending",
|
||||
PLUGINS_PENDING = "IPA/Pending/Plugins",
|
||||
LIBS_PENDING = "IPA/Pending/Libs"
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import { get } from "https";
|
||||
import { createWriteStream, unlink } from "fs";
|
||||
|
||||
export class RequestService {
|
||||
|
||||
private static instance: RequestService;
|
||||
|
||||
public static getInstance(): RequestService{
|
||||
if(!RequestService.instance){ RequestService.instance = new RequestService(); }
|
||||
return RequestService.instance;
|
||||
}
|
||||
|
||||
private constructor(){}
|
||||
|
||||
public get<T = any>(url: string): Promise<T>{
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = ''
|
||||
get(url, (res) => {
|
||||
res.on('data', chunk => body += chunk);
|
||||
res.on('end', () => {
|
||||
resolve(JSON.parse(body));
|
||||
});
|
||||
res.on('error', (err) => reject(err))
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
public downloadFile(url: string, dest: string): Promise<string>{
|
||||
return new Promise((resolve, reject) => {
|
||||
const file = createWriteStream(dest);
|
||||
get(url, res => {
|
||||
res.pipe(file);
|
||||
file.on("close", () => {
|
||||
file.close(() => resolve(dest))
|
||||
}).on("error", err => {
|
||||
unlink(dest, () => reject(err));
|
||||
});
|
||||
}).on("error", err => {
|
||||
unlink(dest, () => reject(err));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import { existsSync, mkdirSync, readdirSync, readFile } from "fs";
|
||||
import { existsSync, mkdirSync, readdirSync, readFile, unlinkSync } from "fs";
|
||||
import { spawnSync } from "child_process";
|
||||
import { homedir } from "os";
|
||||
import path from "path";
|
||||
import { BrowserWindow } from "electron";
|
||||
import { rm } from "fs/promises";
|
||||
import { app, BrowserWindow } from "electron";
|
||||
import { rm, unlink } from "fs/promises";
|
||||
import { IpcResponse } from "shared/models/ipc";
|
||||
import log from "electron-log";
|
||||
|
||||
@@ -27,6 +27,7 @@ export class UtilsService{
|
||||
|
||||
public getAssetsScriptsPath(): string { return this.getAssetsPath("scripts") }
|
||||
public getAssestsJsonsPath(): string { return this.getAssetsPath("jsons"); }
|
||||
public getTempPath(): string{ return path.join(app.getPath("temp"), app.getName()) }
|
||||
|
||||
public setMainWindow(win: BrowserWindow){ this.mainWindow = win; }
|
||||
public getMainWindow(){ return this.mainWindow; }
|
||||
@@ -37,6 +38,16 @@ export class UtilsService{
|
||||
if(!this.pathExist(path)){ mkdirSync(path, {recursive: true}); }
|
||||
}
|
||||
|
||||
public async unlinkIfExist(pathToFile: string): Promise<void>{
|
||||
if(!this.pathExist(pathToFile)){ return }
|
||||
return unlink(pathToFile);
|
||||
}
|
||||
|
||||
public rmDirIfExist(path: string): Promise<void>{
|
||||
if(!this.pathExist(path)){ return; }
|
||||
return rm(path, {recursive: true, force: true});
|
||||
}
|
||||
|
||||
public taskRunning(task: string): boolean{
|
||||
const tasks = spawnSync('tasklist').stdout.toString();
|
||||
return tasks.includes(task);
|
||||
@@ -67,7 +78,7 @@ export class UtilsService{
|
||||
return rm(folderPath, {recursive: true});
|
||||
}
|
||||
|
||||
public ipcSend(channel: string, response: IpcResponse<any>): void{
|
||||
public ipcSend<T = any>(channel: string, response: IpcResponse<T>): void{
|
||||
try {
|
||||
this.mainWindow.webContents.send(channel, response);
|
||||
} catch (error) {
|
||||
@@ -75,4 +86,6 @@ export class UtilsService{
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
+14
-8
@@ -1,6 +1,6 @@
|
||||
import { NavBar } from "./components/nav-bar/nav-bar.component";
|
||||
import TitleBar from "./components/title-bar/title-bar.component";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { Routes, Route, useLocation } from "react-router-dom";
|
||||
import { AvailableVersionsList } from "./pages/available-versions-list.components";
|
||||
import { VersionViewer } from "./pages/version-viewer.component";
|
||||
import { Modal } from "./components/modal/modal.component";
|
||||
@@ -9,17 +9,23 @@ import { BsmProgressBar } from "./components/progress-bar/bsm-progress-bar.compo
|
||||
import { useEffect } from "react";
|
||||
import { ThemeService } from "./services/theme.service";
|
||||
import { NotificationOverlay } from "./components/notification/notification-overlay.component";
|
||||
import { PageStateService } from "./services/page-state.service";
|
||||
|
||||
export default function App() {
|
||||
|
||||
const themeService = ThemeService.getInstance();
|
||||
const themeService = ThemeService.getInstance();
|
||||
const pageState = PageStateService.getInstance();
|
||||
|
||||
useEffect(() => {
|
||||
themeService.theme$.subscribe(() => {
|
||||
if(themeService.isDark || (themeService.isOS && window.matchMedia('(prefers-color-scheme: dark)').matches)){ document.documentElement.classList.add('dark'); }
|
||||
else { document.documentElement.classList.remove('dark'); }
|
||||
});
|
||||
}, []);
|
||||
const location = useLocation();
|
||||
|
||||
pageState.setState(location.state);
|
||||
|
||||
useEffect(() => {
|
||||
themeService.theme$.subscribe(() => {
|
||||
if(themeService.isDark || (themeService.isOS && window.matchMedia('(prefers-color-scheme: dark)').matches)){ return document.documentElement.classList.add('dark'); }
|
||||
document.documentElement.classList.remove('dark');
|
||||
});
|
||||
}, []);
|
||||
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
.glow-on-hover {
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(45deg, #ff0000, #ff7300, #fffb00, #48ff00, #00ffd5, #002bff, #7a00ff, #ff00c8, #ff0000);
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left:-4px;
|
||||
background-size: 400%;
|
||||
z-index: -1;
|
||||
filter: blur(3px);
|
||||
width: calc(100% + 8px);
|
||||
height: calc(100% + 8px);
|
||||
animation: glowing 20s linear infinite;
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease-in-out;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@keyframes glowing {
|
||||
0% { background-position: 0 0; }
|
||||
50% { background-position: 400% 0; }
|
||||
100% { background-position: 0 0; }
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export const AvailableVersionItem = memo(function AvailableVersionItem(props: {v
|
||||
|
||||
return (
|
||||
<div className="group m-3 relative w-72 h-60 transition-transform active:scale-[.98]" onClick={toggleSelect}>
|
||||
<div className={`absolute glow-on-hover group-hover:opacity-100 ${selected && "opacity-100"}`}></div>
|
||||
<div className={`absolute glow-on-hover group-hover:opacity-100 ${selected && "!opacity-100"}`}/>
|
||||
<div className={`relative flex flex-col overflow-hidden rounded-md w-72 h-60 cursor-pointer group-hover:shadow-none duration-300 bg-light-main-color-2 dark:bg-main-color-2 ${!selected && "shadow-lg shadow-gray-900"}`}>
|
||||
<BsmImage image={props.version.ReleaseImg ? props.version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="absolute top-0 right-0 w-full h-full opacity-40 blur-xl object-cover" loading="lazy"/>
|
||||
<BsmImage image={props.version.ReleaseImg ? props.version.ReleaseImg : defaultImage} errorImage={defaultImage} placeholder={defaultImage} className="bg-black w-full h-3/4 object-cover" loading="lazy"/>
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ModalExitCode, ModalResponse, ModalService } 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 { BSVersion } from "shared/bs-version.interface";
|
||||
|
||||
|
||||
export function UninstallAllModsModal({resolver}: {resolver: (x: ModalResponse) => void}) {
|
||||
|
||||
const version = ModalService.getInsance().getModalData<BSVersion>();
|
||||
const t = useTranslation();
|
||||
|
||||
const desc = "modals.uninstall-all-mods.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-all-mods.title")}</h1>
|
||||
<BsmImage className="mx-auto h-24" image={BeatConflict}/>
|
||||
<p className="max-w-sm text-gray-800 dark:text-gray-200">{t(desc, {version: version.name ?? version.BSVersion})}</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>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { ModalExitCode, ModalResponse, ModalService } 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 function UninstallModModal({resolver}: {resolver: (x: ModalResponse) => void}) {
|
||||
|
||||
const mod = ModalService.getInsance().getModalData<Mod>();
|
||||
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>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import { InstallationFolderModal } from "./modal-types/installation-folder-modal
|
||||
import { EditVersionModal } from "./modal-types/edit-version-modal.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { UninstallModModal } from "./modal-types/uninstall-mod-modal.component";
|
||||
import { UninstallAllModsModal } from "./modal-types/uninstall-all-mods-modal.component";
|
||||
|
||||
export function Modal() {
|
||||
|
||||
@@ -29,6 +31,8 @@ export function Modal() {
|
||||
{modalType === ModalType.INSTALLATION_FOLDER && <InstallationFolderModal resolver={modalSevice.getResolver()}/>}
|
||||
{modalType === ModalType.EDIT_VERSION && <EditVersionModal resolver={modalSevice.getResolver()}/>}
|
||||
{modalType === ModalType.CLONE_VERSION && <EditVersionModal resolver={modalSevice.getResolver()} clone/>}
|
||||
{modalType === ModalType.UNINSTALL_MOD && <UninstallModModal resolver={modalSevice.getResolver()}/>}
|
||||
{modalType === ModalType.UNINSTALL_ALL_MODS && <UninstallAllModsModal resolver={modalSevice.getResolver()}/>}
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
|
||||
@@ -9,27 +9,34 @@ export function BsmProgressBar() {
|
||||
|
||||
const progressBarService = ProgressBarService.getInstance();
|
||||
|
||||
const progress = useObservable(progressBarService.progression$);
|
||||
const progressData = useObservable(progressBarService.progressData$);
|
||||
const visible = useObservable(progressBarService.visible$);
|
||||
const style = useObservable(progressBarService.style$);
|
||||
|
||||
const progressLabel = (() => {
|
||||
if(!progressData){ return ""; }
|
||||
if(progressData?.label){ return progressData.label; }
|
||||
return `${Math.floor(progressData.progression)}%`
|
||||
})();
|
||||
const progressValue: number = progressData?.progression;
|
||||
|
||||
const {firstColor, secondColor} = useThemeColor();
|
||||
|
||||
return (
|
||||
<AnimatePresence> { visible &&
|
||||
<motion.div initial={{y: "120%"}} animate={{y:"0%"}} exit={{y:"120%"}} className="w-full absolute h-14 flex justify-center items-center bottom-2">
|
||||
<div className={`flex items-center content-center justify-center bottom-9 z-10 rounded-lg bg-light-main-color-2 dark:bg-main-color-2 shadow-center shadow-black cursor-pointer transition-all duration-300 ${!progress && "h-14 w-14 rounded-full"} ${!!progress && "h-5 w-3/4 rounded-full p-[6px]"}`}>
|
||||
{ !!progress && (
|
||||
<>
|
||||
<div className="relative flex items-center h-full w-full rounded-full bg-black">
|
||||
<div className="w-0 h-full relative rounded-full download-progress flex items-center transition-all" style={{width: `${progress}%`, background: `linear-gradient(90deg, ${firstColor}, ${secondColor}, ${firstColor}, ${secondColor})`}}>
|
||||
<img className="h-[70px] w-[70px] min-w-fit absolute z-[1] -translate-y-1 -right-8 transition-all" src={BeatRunningImg} />
|
||||
</div>
|
||||
<span className="absolute w-full text-center text-white -top-[3px] left-0 text-[10px]">{`${Math.floor(progress)}%`}</span>
|
||||
return (
|
||||
<AnimatePresence> { visible &&
|
||||
<motion.div initial={{y: "120%"}} animate={{y:"0%"}} exit={{y:"120%"}} className="w-full absolute h-14 flex justify-center items-center bottom-2 pointer-events-none" style={style}>
|
||||
<div className={`flex items-center content-center justify-center bottom-9 z-10 rounded-lg bg-light-main-color-2 dark:bg-main-color-2 shadow-center shadow-black cursor-pointer transition-all duration-300 ${!progressValue && "h-14 w-14 !rounded-full"} ${!!progressValue && "h-5 w-3/4 !rounded-full p-[6px]"}`}>
|
||||
{ !!progressValue && (
|
||||
<div className="relative flex items-center h-full w-full rounded-full bg-black">
|
||||
<div className="w-0 h-full relative rounded-full download-progress flex items-center transition-all" style={{width: `${progressValue}%`, background: `linear-gradient(90deg, ${firstColor}, ${secondColor}, ${firstColor}, ${secondColor})`}}>
|
||||
<img className="h-[70px] w-[70px] min-w-fit absolute z-[1] -translate-y-1 -right-8 transition-all" src={BeatRunningImg} />
|
||||
</div>
|
||||
<span className="absolute w-full text-center text-white -top-[3px] left-0 text-[10px]">{progressLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
{ !progressValue && <img className="w-12 h-12 spin-loading" src={BeatWaitingImg}></img> }
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{ !progress && <img className="w-12 h-12 spin-loading" src={BeatWaitingImg}></img> }
|
||||
</div>
|
||||
</motion.div>
|
||||
} </AnimatePresence>
|
||||
</motion.div>
|
||||
} </AnimatePresence>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import {motion, useCycle} from "framer-motion"
|
||||
import { useState } from "react"
|
||||
|
||||
type Props = {className?: string, nbClickToShow?: number, children?: JSX.Element}
|
||||
|
||||
export function SpoilerClick({className, children, nbClickToShow = 5}: Props) {
|
||||
|
||||
const [remainingClicks, setRemainingClicks] = useState(nbClickToShow);
|
||||
const [translateY, setTranslateY] = useState("100%");
|
||||
|
||||
const handleClicks = () => {
|
||||
if(remainingClicks - 1 > 0){ setRemainingClicks(nb => nb - 1); }
|
||||
setTranslateY(`${(((remainingClicks - 1) / nbClickToShow) * 100)}%`)
|
||||
}
|
||||
|
||||
return (
|
||||
<motion.div className={className} initial={{y: translateY}} animate={{y: translateY}} transition={{type: "spring", stiffness: 300, mass: 1 }} onClick={handleClicks}>
|
||||
{children}
|
||||
</motion.div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,10 @@
|
||||
import { BsmIcon, BsmIconType } from "../svgs/bsm-icon.component"
|
||||
import OutsideClickHandler from 'react-outside-click-handler';
|
||||
import React from "react";
|
||||
import React, { MutableRefObject, Ref } from "react";
|
||||
import { BsmImage } from "./bsm-image.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
|
||||
|
||||
type BsmButtonType = "primary"|"success"|"cancel"|"error";
|
||||
|
||||
@@ -32,19 +33,10 @@ export function BsmButton({className, style, imgClassName, iconClassName, icon,
|
||||
|
||||
const primaryColor = typeColor === "primary" ? useThemeColor("first-color") : null;
|
||||
|
||||
const textColor = !primaryColor ? null : (() => {
|
||||
const hex = primaryColor.replaceAll("#", "");
|
||||
const uicolors = [parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255];
|
||||
const c = uicolors.map(col => {
|
||||
if (col <= 0.03928) { return col / 12.92; }
|
||||
return Math.pow((col + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
|
||||
return (L > 0.179) ? "#000" : "fff";
|
||||
})();
|
||||
const textColor = !primaryColor ? null : getCorrectTextColor(primaryColor);
|
||||
|
||||
const renderTypeColor = (() => {
|
||||
if(typeColor === "primary"){ return null; }
|
||||
if(typeColor === "primary"){ return ""; }
|
||||
if(!typeColor){ return `bg-light-main-color-2 dark:bg-main-color-2 ${!withBar && "hover:bg-light-main-color-3 dark:hover:bg-main-color-3"}`; }
|
||||
if(typeColor === "cancel"){ return "bg-gray-500"; }
|
||||
if(typeColor === "error"){ return "bg-red-500"; }
|
||||
@@ -54,10 +46,10 @@ export function BsmButton({className, style, imgClassName, iconClassName, icon,
|
||||
|
||||
return (
|
||||
<OutsideClickHandler onOutsideClick={e => onClickOutside && onClickOutside(e)}>
|
||||
<div onClick={e => onClick && onClick(e)} className={`${className} overflow-hidden cursor-pointer group ${!withBar && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, ...((!!primaryColor || !!color) && {backgroundColor: primaryColor ?? color})}}>
|
||||
<div onClick={onClick} className={`${className} overflow-hidden cursor-pointer group ${(!withBar && !disabled && (!!typeColor || !!color)) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, ...((!!primaryColor || !!color) && {backgroundColor: primaryColor ?? color})}}>
|
||||
{ image && <BsmImage image={image} className={imgClassName}/> }
|
||||
{ icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"}/> }
|
||||
{text && (type === "submit" ? <button className="w-full h-full" style={{...(!!textColor && {color: textColor})}}>{t(text)}</button> : <span style={{...(!!textColor && {color: textColor})}} >{t(text)}</span>)}
|
||||
{text && (type === "submit" ? <button type="submit" className="w-full h-full" style={{...(!!textColor && {color: textColor})}}>{t(text)}</button> : <span style={{...(!!textColor && {color: textColor})}} >{t(text)}</span>)}
|
||||
{ withBar && (
|
||||
<div className="absolute bottom-0 left-0 w-full h-1 bg-current" style={{color: secondColor}}>
|
||||
<div className="absolute top-0 left-0 h-full w-full bg-current brightness-50"/>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import { BsmIcon } from "../svgs/bsm-icon.component"
|
||||
import { motion } from "framer-motion"
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { getCorrectTextColor } from "renderer/helpers/correct-text-color"
|
||||
|
||||
type Props = {className?:string, checked?: boolean, onChange?: (val: boolean) => void, disabled?: boolean}
|
||||
|
||||
export function BsmCheckbox({className, checked, onChange, disabled} : Props) {
|
||||
|
||||
const checkedColor = useThemeColor("first-color");
|
||||
const iconColor = getCorrectTextColor(checkedColor);
|
||||
|
||||
const handleClick = () => {
|
||||
if(disabled){ return; }
|
||||
onChange && onChange(!checked);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`group ${className}`}>
|
||||
{!disabled && <div className="glow-on-hover !w-[calc(100%+6px)] !h-[calc(100%+6px)] !-top-[3px] !-left-[3px] group-hover:opacity-100"/>}
|
||||
|
||||
<div className={`w-full h-full bg-inherit border-2 border-current rounded-md overflow-hidden flex items-center justify-center ${disabled ? "brightness-50 cursor-not-allowed" : "cursor-pointer"}`} onClickCapture={e => {e.stopPropagation(); handleClick()}}>
|
||||
<motion.span className="w-full h-full flex items-center justify-center" style={{backgroundColor: checkedColor}} initial={checked ? {opacity: 1} : {opacity: 0}} animate={checked ? {opacity: 1} : {opacity: 0}} transition={{duration: .15}}>
|
||||
<BsmIcon icon="check" style={{color: iconColor}}/>
|
||||
</motion.span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -6,17 +6,21 @@ import { useTranslation } from "renderer/hooks/use-translation.hook"
|
||||
|
||||
export interface DropDownItem {text: string, icon?: BsmIconType, onClick?: () => void}
|
||||
|
||||
export function BsmDropdownButton({className, items, align}: {className?: string, items?: DropDownItem[], align?: "left"|"right"}) {
|
||||
type Props = {className?: string, items?: DropDownItem[], align?: "left"|"right", withBar?: boolean, icon?: BsmIconType, buttonClassName?: string, menuTranslationY?: string|number}
|
||||
|
||||
export function BsmDropdownButton({className, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY}: Props) {
|
||||
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const t = useTranslation()
|
||||
const t = useTranslation();
|
||||
|
||||
const defaultButtonClassName = "relative z-[1] p-1 rounded-md text-inherit w-full h-full shadow-md shadow-black"
|
||||
|
||||
return (
|
||||
<div className={`${className}`}>
|
||||
<BsmButton onClick={() => setExpanded(!expanded)} className='relative z-[1] p-1 rounded-md text-inherit w-full h-full shadow-md shadow-black' icon="settings" active={expanded} onClickOutside={() => {setExpanded(false)}}/>
|
||||
<div className={`pt-1 pb-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md overflow-hidden bg-light-main-color-2 dark:bg-main-color-2 text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-transform ease-in-out ${align === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"}`} style={{transform: expanded ? "scale(1)" : "scale(0)"}}>
|
||||
<BsmButton onClick={() => setExpanded(!expanded)} className={buttonClassName ?? defaultButtonClassName} icon={icon} active={expanded} onClickOutside={() => {setExpanded(false)}} withBar={withBar}/>
|
||||
<div className={`pt-1 pb-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md overflow-hidden bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] ease-in-out ${align === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"}`} style={{scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}`}}>
|
||||
{ items?.map((i, index) => !!i &&(
|
||||
<div key={index} onClick={!!i.onClick ? i.onClick : undefined} className="flex justify-start items-center w-full pr-3 pl-3 pt-2 pb-2 hover:bg-light-main-color-3 dark:hover:bg-main-color-3">
|
||||
<div key={index} onClick={!!i.onClick ? i.onClick : undefined} className="flex justify-start items-center w-full pr-3 pl-3 pt-2 pb-2 hover:backdrop-brightness-150">
|
||||
{i.icon && <BsmIcon icon={i.icon} className="h-5 w-5 mr-1 text-gray-800 dark:text-white"></BsmIcon>}
|
||||
<span className="w-max">{t(i.text)}</span>
|
||||
</div>
|
||||
|
||||
@@ -19,11 +19,14 @@ import { CSSProperties, memo } from "react";
|
||||
import EditIcon from "./icons/edit-icon.component";
|
||||
import { ExportIcon } from "./icons/export-icon.component";
|
||||
import PatreonIcon from "./icons/patreon-icon.component";
|
||||
import { SearchIcon } from "./icons/search-icon.component";
|
||||
import { CheckIcon } from "./icons/check-icon.component";
|
||||
import { ThreeDotsIcon } from "./icons/three-dots-icon.component";
|
||||
|
||||
export type BsmIconType = (
|
||||
"settings"|"trash"|"favorite"|"folder"|"bsNote"|
|
||||
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|
|
||||
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|
|
||||
"copy"|"steam"|"edit"|"export"|"patreon"|
|
||||
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|
|
||||
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"
|
||||
);
|
||||
|
||||
@@ -50,6 +53,9 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
|
||||
if(icon === "edit"){ return <EditIcon className={className} style={style}/> }
|
||||
if(icon === "export"){ return <ExportIcon className={className} style={style}/> }
|
||||
if(icon === "patreon"){ return <PatreonIcon className={className} style={style}/> }
|
||||
if(icon === "search"){ return <SearchIcon className={className} style={style}/> }
|
||||
if(icon === "check"){ return <CheckIcon className={className} style={style}/> }
|
||||
if(icon === "three-dots"){ return <ThreeDotsIcon className={className} style={style}/> }
|
||||
return <TrashIcon className={className} style={style}/>
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function CheckIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" height="40" width="40">
|
||||
<path fill="currentColor" d="M15.792 29.458q-.292 0-.563-.104-.271-.104-.521-.354l-7.416-7.417q-.459-.458-.459-1.145 0-.688.459-1.146.458-.459 1.104-.459.646 0 1.146.459l6.25 6.291 14.666-14.666q.459-.459 1.125-.459.667 0 1.125.459.459.458.459 1.145 0 .688-.459 1.146L16.917 29q-.25.25-.521.354-.271.104-.604.104Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function SearchIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 40 40" height="40" width="40">
|
||||
<path fill="currentColor" d="m31.833 34.167-9.708-9.75q-1.25 1.041-2.896 1.625-1.646.583-3.521.583-4.583 0-7.77-3.208-3.188-3.209-3.188-7.709 0-4.541 3.188-7.729 3.187-3.187 7.729-3.187 4.541 0 7.729 3.187 3.187 3.188 3.187 7.729 0 1.792-.583 3.438-.583 1.646-1.625 3.021l9.792 9.75q.458.458.458 1.104 0 .646-.5 1.146-.458.458-1.146.458-.687 0-1.146-.458ZM15.667 23.458q3.25 0 5.5-2.27 2.25-2.271 2.25-5.48 0-3.25-2.25-5.5t-5.5-2.25q-3.209 0-5.479 2.25-2.271 2.25-2.271 5.5 0 3.209 2.25 5.48 2.25 2.27 5.5 2.27Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function ThreeDotsIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" height="24" width="24">
|
||||
<path fill="currentColor" d="M12.025 21.15q-1 0-1.7-.7t-.7-1.675q0-1 .7-1.7t1.675-.7q1 0 1.688.7.687.7.687 1.7 0 .975-.687 1.675-.688.7-1.663.7Zm0-6.775q-1 0-1.7-.7T9.625 12q0-1 .7-1.688.7-.687 1.675-.687 1 0 1.688.687.687.688.687 1.663 0 1-.687 1.7-.688.7-1.663.7Zm0-6.75q-1 0-1.7-.713-.7-.712-.7-1.687 0-1 .7-1.688.7-.687 1.675-.687 1 0 1.688.687.687.688.687 1.688 0 .975-.687 1.687-.688.713-1.663.713Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -31,7 +31,7 @@ export default function TitleBar({template = "main"} : {template?: "update"|"mai
|
||||
}
|
||||
|
||||
return (
|
||||
<header id="titlebar" className="min-h-[22px] bg-light-main-color-1 dark:bg-main-color-1 w-screen h-[22px] flex content-center items-center justify-start z-10">
|
||||
<header id="titlebar" className="min-h-[22px] bg-light-main-color-1 shrink-0 dark:bg-main-color-1 w-screen h-[22px] flex content-center items-center justify-start z-10">
|
||||
<div id="drag-region" className='grow basis-0 h-full'>
|
||||
{template !== "update" && (<div id="window-title" className='pl-1'>
|
||||
<span className='text-gray-800 dark:text-gray-100 font-bold text-xs italic'>BSManager</span>
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
import { BsmIcon, BsmIconType } from "renderer/components/svgs/bsm-icon.component"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
|
||||
type props = {onClick: (active: boolean) => void, active: boolean, text: string, icon: BsmIconType}
|
||||
|
||||
export function LaunchModToogle({onClick, active, text, icon}: props) {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={`relative rounded-full cursor-pointer group active:scale-95 transition-transform ${!active && "shadow-md shadow-black"}`} onClick={() => onClick(!active)}>
|
||||
<div className={`absolute glow-on-hover !rounded-full ${active && "!opacity-100 blur-[2px]"}`}/>
|
||||
<div className='w-full h-full pl-6 pr-6 flex justify-center items-center bg-light-main-color-2 dark:bg-main-color-2 p-3 rounded-full text-gray-800 dark:text-white group-hover:bg-light-main-color-1 dark:group-hover:bg-main-color-1'>
|
||||
<BsmIcon icon={icon} className='mr-1 h-7 text-gray-800 dark:text-white'/>
|
||||
<span className='w-fit min-w-fit h-full text-lg font-bold uppercase tracking-wide italic'>{t(text)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useState } from "react";
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service";
|
||||
import { ConfigurationService } from "renderer/services/configuration.service";
|
||||
import { BSVersion } from "shared/bs-version.interface"
|
||||
import { LaunchModToogle } from "./launch-mod-toogle.component";
|
||||
|
||||
type props = {version: BSVersion};
|
||||
|
||||
export function LaunchSlide({version}: props) {
|
||||
|
||||
const configService = ConfigurationService.getInstance();
|
||||
const bsLauncherService = BSLauncherService.getInstance();
|
||||
|
||||
const [oculusMode, setOculusMode] = useState(!!configService.get<boolean>(LaunchMods.OCULUS_MOD));
|
||||
const [desktopMode, setDesktopMode] = useState(!!configService.get<boolean>(LaunchMods.DESKTOP_MOD));
|
||||
const [debugMode, setDebugMode] = useState(!!configService.get<boolean>(LaunchMods.DEBUG_MOD));
|
||||
|
||||
const launchState = useObservable(bsLauncherService.launchState$);
|
||||
|
||||
const setMode = (mode: LaunchMods, value: boolean) => {
|
||||
if(mode === LaunchMods.DEBUG_MOD){ setDebugMode(value); }
|
||||
else if(mode === LaunchMods.OCULUS_MOD){
|
||||
setOculusMode(value);
|
||||
setDesktopMode(false);
|
||||
configService.set(LaunchMods.DESKTOP_MOD, false);
|
||||
}
|
||||
else if(mode === LaunchMods.DESKTOP_MOD){
|
||||
setDesktopMode(value);
|
||||
setOculusMode(false);
|
||||
configService.set(LaunchMods.OCULUS_MOD, false);
|
||||
}
|
||||
configService.set(mode, value);
|
||||
}
|
||||
|
||||
const launch = () => bsLauncherService.launch(version, oculusMode, desktopMode, debugMode)
|
||||
|
||||
return (
|
||||
<div className="w-full shrink-0 items-center relative flex flex-col justify-start">
|
||||
<div className='grid grid-flow-col grid-cols-3 gap-6'>
|
||||
<LaunchModToogle icon='oculus' onClick={() => setMode(LaunchMods.OCULUS_MOD, !oculusMode)} active={oculusMode} text="pages.version-viewer.launch-mods.oculus"/>
|
||||
<LaunchModToogle icon='desktop' onClick={() => setMode(LaunchMods.DESKTOP_MOD, !desktopMode)} active={desktopMode} text="pages.version-viewer.launch-mods.desktop"/>
|
||||
<LaunchModToogle icon='terminal' onClick={() => setMode(LaunchMods.DEBUG_MOD, !debugMode)} active={debugMode} text="pages.version-viewer.launch-mods.debug"/>
|
||||
</div>
|
||||
<div className='grow flex justify-center items-center'>
|
||||
<BsmButton onClick={launch} active={JSON.stringify(version) === JSON.stringify(launchState)} className='relative text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform' text="misc.launch"/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
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 } from "react"
|
||||
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { PageStateService } from "renderer/services/page-state.service";
|
||||
|
||||
type Props = {mod: Mod, installedVersion: string, isDependency?: boolean, isSelected?: boolean, onChange?: (val: boolean) => void, wantInfo?: boolean}
|
||||
|
||||
export function ModItem({mod, installedVersion, isDependency, isSelected, onChange, wantInfo}: Props) {
|
||||
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
const pageState = PageStateService.getInstance();
|
||||
|
||||
const themeColor = useThemeColor("second-color");
|
||||
const uninstalling = useObservable(modsManager.isUninstalling$);
|
||||
|
||||
const wantInfoStyle: CSSProperties = wantInfo ? {borderColor: themeColor} : {borderColor: "transparent"};
|
||||
const isOutDated = installedVersion < mod.version;
|
||||
|
||||
const uninstall = () => {
|
||||
modsManager.uninstallMod(mod, pageState.getState());
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1">
|
||||
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-inherit ml-3 border-t-2 border-b-2 border-l-2" style={wantInfoStyle}>
|
||||
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={onChange} disabled={mod.required || isDependency} checked={isDependency || isSelected || mod.required}/>
|
||||
</div>
|
||||
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2" style={wantInfoStyle}>{mod.name}</span>
|
||||
<span className={`min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 ${(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" style={wantInfoStyle}>{mod.version}</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" style={wantInfoStyle}>{mod.description}</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" style={wantInfoStyle}>
|
||||
{installedVersion && <BsmButton className="z-[1] h-7 w-7 p-[5px] rounded-full" icon="trash" disabled={uninstalling} withBar={false} onClick={e => {e.stopPropagation(); uninstall()}}/>}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
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 { 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 { 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}
|
||||
|
||||
export function ModsGrid({modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos}: Props) {
|
||||
|
||||
const pageState = PageStateService.getInstance();
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
|
||||
const [filter, setFilter] = useState("");
|
||||
const [filterEnabled, setFilterEnabled] = useState(false);
|
||||
const t = useTranslation();
|
||||
|
||||
const installedModVersion = (key: string, mod: Mod): string => {
|
||||
if(!installed || !installed.get(key)){ return undefined; }
|
||||
const installedMod = installed.get(key).find(m => m.name === mod.name);
|
||||
if(!installedMod){ return undefined }
|
||||
return installedMod.version
|
||||
}
|
||||
|
||||
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 isSelected = (mod: Mod): boolean => modsSelected.some(m => m.name === mod.name);
|
||||
|
||||
const handleInput = (val: string) => setFilter(val.toLowerCase());
|
||||
|
||||
const handleToogleFilter = () => {
|
||||
setFilter("");
|
||||
setFilterEnabled(b => !b);
|
||||
}
|
||||
|
||||
const handleUninstallAll = () => {
|
||||
modsManager.uninstallAllMods(pageState.getState())
|
||||
}
|
||||
|
||||
return modsMap && (
|
||||
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_1fr_min-content] bg-light-main-color-2 dark:bg-main-color-2 text-main-color-1 dark:text-light-main-color-1">
|
||||
<span className="absolute z-10 top-0 w-full h-8 bg-inherit"/>
|
||||
<span className="z-10 sticky flex items-center justify-end top-0 bg-inherit border-b-2 border-main-color-1">
|
||||
<BsmButton className="rounded-full h-6 w-6 p-[2px]" withBar={false} icon="search" onClick={handleToogleFilter}/>
|
||||
</span>
|
||||
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1">
|
||||
{(filterEnabled ? (
|
||||
<motion.input autoFocus className="bg-main-color-1 rounded-md h-6 px-2" initial={{width: 0}} animate={{width: "250px"}} transition={{ease:"easeInOut", duration:.15}} onChange={e => handleInput(e.target.value)}/>
|
||||
):(
|
||||
<span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>
|
||||
))}
|
||||
</span>
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2">{t("pages.version-viewer.mods.mods-grid.header-bar.installed")}</span>
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2">{t("pages.version-viewer.mods.mods-grid.header-bar.latest")}</span>
|
||||
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8">{t("pages.version-viewer.mods.mods-grid.header-bar.description")}</span>
|
||||
<span className="z-10 sticky top-0 bg-inherit border-b-2 border-main-color-1 h-8 flex justify-start items-center py-1 pl-[3px] min-w-[50px]">
|
||||
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[
|
||||
({text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: handleUninstallAll}),
|
||||
]}/>
|
||||
</span>
|
||||
{
|
||||
Array.from(modsMap.keys()).map(key => modsMap.get(key).some(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) && (
|
||||
<li className="contents cursor-pointer" onClick={() => onWantInfos(mod)} key={mod.name}>
|
||||
<ModItem mod={mod} installedVersion={installedModVersion(key, mod)} isDependency={isDependency(mod)} isSelected={isSelected(mod)} onChange={(val) => onModChange(val, mod)} wantInfo={mod.name === moreInfoMod?.name}/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
||||
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import VisibilitySensor from 'react-visibility-sensor';
|
||||
import { Mod } 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 { IpcService } from "renderer/services/ipc.service";
|
||||
import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png"
|
||||
import { SpoilerClick } from "renderer/components/shared/UwU/spoiler-click.component";
|
||||
import YuruYuriDance from "../../../../../../assets/images/gifs/yuruyuri-dance.gif"
|
||||
import { useObservable } from "renderer/hooks/use-observable.hook";
|
||||
import { skip, filter } from "rxjs/operators";
|
||||
import { Subscription } from "rxjs";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
|
||||
export function ModsSlide({version}: {version: BSVersion}) {
|
||||
|
||||
const modsManager = BsModsManagerService.getInstance();
|
||||
const configService = ConfigurationService.getInstance();
|
||||
const ipcServive = IpcService.getInstance();
|
||||
|
||||
const [isVisible, setIsVisible] = useState(false);
|
||||
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 installing = useObservable(modsManager.isInstalling$);
|
||||
const t = useTranslation();
|
||||
|
||||
const downloadRef = useRef(null);
|
||||
const [downloadWith, setDownloadWidth] = useState(0);
|
||||
|
||||
const modsToCategoryMap = (mods: Mod[]): Map<string, Mod[]> => {
|
||||
if(!mods){ return new Map<string, Mod[]>(); }
|
||||
const map = new Map<string, Mod[]>();
|
||||
mods.forEach(mod => map.set(mod.category, [...map.get(mod.category) ?? [], mod]));
|
||||
return map;
|
||||
}
|
||||
|
||||
const handleModChange = (selected: boolean, mod: Mod) => {
|
||||
if(selected){ return setModsSelected([...modsSelected, mod]); }
|
||||
const mods = [...modsSelected];
|
||||
mods.splice(mods.findIndex(m => m.name === mod.name), 1);
|
||||
setModsSelected(mods);
|
||||
}
|
||||
|
||||
const handleMoreInfo = (mod: Mod) => {
|
||||
if(mod.name === moreInfoMod?.name){ return setMoreInfoMod(null); }
|
||||
setMoreInfoMod(mod);
|
||||
}
|
||||
|
||||
const handleOpenMoreInfo = () => {
|
||||
if(!moreInfoMod || !moreInfoMod.link){ return; }
|
||||
ipcServive.sendLazy("new-window", {args: moreInfoMod.link});
|
||||
}
|
||||
|
||||
const installMods = () => {
|
||||
if(installing){ return; }
|
||||
modsManager.installMods(modsSelected, version).then(() => {
|
||||
loadMods();
|
||||
});
|
||||
}
|
||||
|
||||
const loadMods = () => {
|
||||
Promise.all([
|
||||
modsManager.getAvailableMods(version),
|
||||
modsManager.getInstalledMods(version)
|
||||
]).then(([available, installed]) => {
|
||||
const defaultMods = 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))
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
const subs: Subscription[] = [];
|
||||
|
||||
if(isVisible){
|
||||
loadMods();
|
||||
subs.push(modsManager.isUninstalling$.pipe(skip(1), filter(uninstalling => !uninstalling)).subscribe(() => {
|
||||
loadMods();
|
||||
}))
|
||||
}
|
||||
|
||||
return () => {
|
||||
setMoreInfoMod(null);
|
||||
setModsAvailable(null);
|
||||
setModsInstalled(null);
|
||||
subs.forEach(s => s.unsubscribe());
|
||||
}
|
||||
|
||||
}, [isVisible, version]);
|
||||
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if(modsAvailable){
|
||||
setDownloadWidth(downloadRef?.current?.offsetWidth)
|
||||
}
|
||||
}, [modsAvailable])
|
||||
|
||||
|
||||
return (
|
||||
<VisibilitySensor onChange={setIsVisible}>
|
||||
<div className='shrink-0 w-full h-full px-8 pb-7 flex justify-center'>
|
||||
<div className='relative flex flex-col grow-0 bg-light-main-color-2 dark:bg-main-color-2 h-full w-full rounded-md shadow-black shadow-center overflow-hidden'>
|
||||
{modsAvailable ? (
|
||||
<>
|
||||
<div className="overflow-scroll w-full min-h-0 scrollbar-thin scrollbar-thumb-neutral-900 scrollbar-thumb-rounded-full">
|
||||
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo}/>
|
||||
</div>
|
||||
<div className="h-10 shrink-0 flex items-center justify-between px-3">
|
||||
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{width: downloadWith}}/>
|
||||
<div ref={downloadRef}>
|
||||
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.install-or-update" withBar={false} disabled={installing} typeColor="primary" onClick={installMods}/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center relative">
|
||||
<img className="w-32 h-32 spin-loading" src={BeatWaitingImg}></img>
|
||||
<span className="text-xl mt-3 h-0 italic">{t("pages.version-viewer.mods.loading-mods")}</span>
|
||||
<SpoilerClick className="absolute right-5 w-20 h-[120px] bottom-5 cursor-pointer">
|
||||
<img className="relative w-full" src={YuruYuriDance}/>
|
||||
</SpoilerClick>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</VisibilitySensor>
|
||||
|
||||
)
|
||||
}
|
||||
@@ -4,8 +4,9 @@ export const defaultConfiguration: {[key in DefaultConfigKey]: any} = {
|
||||
"theme": "os",
|
||||
"language": window.navigator.language.length <= 2 ? `${window.navigator.language}-${window.navigator.language.toLocaleUpperCase()}` : window.navigator.language,
|
||||
"supported_languages": ["en-US", "en-EN", "fr-FR", "es-ES"],
|
||||
"default_mods": ["SongCore", "ScoreSaber", "BeatSaverDownloader", "BeatSaverVoting", "PlaylistManager", "ModelDownloader"]
|
||||
}
|
||||
|
||||
export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages";
|
||||
export type DefaultConfigKey = "first-color" | "second-color" | "theme" | "language" | "supported_languages" | "default_mods";
|
||||
|
||||
export type ThemeConfig = "dark" | "light" | "os"
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Return text color for best constrast with background color
|
||||
* @param {string} bgHex
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getCorrectTextColor(bgHex: string): string{
|
||||
const hex = bgHex.replaceAll("#", "");
|
||||
const uicolors = [parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255, parseInt(hex.substring(0, 2), 16) / 255];
|
||||
const c = uicolors.map(col => {
|
||||
if (col <= 0.03928) { return col / 12.92; }
|
||||
return (col + 0.055) / 1.055 ** 2.4;
|
||||
});
|
||||
const L = (0.2126 * c[0]) + (0.7152 * c[1]) + (0.0722 * c[2]);
|
||||
return (L > 0.128) ? "#000" : "fff";
|
||||
}
|
||||
@@ -33,6 +33,29 @@
|
||||
animation: spin-loading 2s cubic-bezier(.46,-0.55,.49,1.52) 0s forwards infinite;
|
||||
}
|
||||
|
||||
.glow-on-hover {
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(45deg, #ff0000, #ff7300, #fffb00, #48ff00, #00ffd5, #002bff, #7a00ff, #ff00c8, #ff0000);
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left:-4px;
|
||||
background-size: 400%;
|
||||
z-index: -1;
|
||||
filter: blur(3px);
|
||||
width: calc(100% + 8px);
|
||||
height: calc(100% + 8px);
|
||||
animation: glowing 20s linear infinite;
|
||||
opacity: 0;
|
||||
transition: opacity .2s ease-in-out;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
@keyframes glowing {
|
||||
0% { background-position: 0 0; }
|
||||
50% { background-position: 400% 0; }
|
||||
100% { background-position: 0 0; }
|
||||
}
|
||||
|
||||
@keyframes spin-loading{
|
||||
0%{ transform: rotate(-360deg); }
|
||||
50%{ transform: rotate(-360deg); }
|
||||
|
||||
@@ -1,100 +1,56 @@
|
||||
import { BSVersion } from 'shared/bs-version.interface';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import BSLogo from '../../../assets/images/apngs/bs-logo.png';
|
||||
import { BSLauncherService, LaunchMods } from '../services/bs-launcher.service';
|
||||
import { TabNavBar } from 'renderer/components/shared/tab-nav-bar.component';
|
||||
import wipGif from "../../../assets/images/gifs/wip.gif"
|
||||
import { ConfigurationService } from 'renderer/services/configuration.service';
|
||||
import { BsmDropdownButton } from 'renderer/components/shared/bsm-dropdown-button.component';
|
||||
import { BsmIcon, BsmIconType } from 'renderer/components/svgs/bsm-icon.component';
|
||||
import { BsmImage } from 'renderer/components/shared/bsm-image.component';
|
||||
import { BsmButton } from 'renderer/components/shared/bsm-button.component';
|
||||
import { BSUninstallerService } from '../services/bs-uninstaller.service';
|
||||
import { BSVersionManagerService } from '../services/bs-version-manager.service';
|
||||
import { ModalExitCode, ModalService, ModalType } from '../services/modale.service';
|
||||
import DefautVersionImage from "../../../assets/images/default-version-img.jpg";
|
||||
import { BsDownloaderService } from 'renderer/services/bs-downloader.service';
|
||||
import { useTranslation } from 'renderer/hooks/use-translation.hook';
|
||||
import { IpcService } from 'renderer/services/ipc.service';
|
||||
import { useObservable } from 'renderer/hooks/use-observable.hook';
|
||||
import { MapService } from 'renderer/services/maps.service';
|
||||
import { LaunchSlide } from 'renderer/components/version-viewer/slides/launch/launch-slide.component';
|
||||
import { ModsSlide } from 'renderer/components/version-viewer/slides/mods/mods-slide.component';
|
||||
|
||||
export function VersionViewer() {
|
||||
|
||||
const configService = ConfigurationService.getInstance();
|
||||
const bsUninstallerService = BSUninstallerService.getInstance();
|
||||
const bsVersionManagerService = BSVersionManagerService.getInstance();
|
||||
const modalService = ModalService.getInsance();
|
||||
const bsDownloaderService = BsDownloaderService.getInstance();
|
||||
const ipcService = IpcService.getInstance();
|
||||
const mapService = MapService.getInstance();
|
||||
const bsUninstallerService = BSUninstallerService.getInstance();
|
||||
const bsVersionManagerService = BSVersionManagerService.getInstance();
|
||||
const modalService = ModalService.getInsance();
|
||||
const bsDownloaderService = BsDownloaderService.getInstance();
|
||||
const ipcService = IpcService.getInstance();
|
||||
const mapService = MapService.getInstance();
|
||||
|
||||
const {state} = useLocation() as {state: BSVersion};
|
||||
const navigate = useNavigate();
|
||||
const bsLauncherService = BSLauncherService.getInstance();
|
||||
const {state} = useLocation() as {state: BSVersion};
|
||||
const navigate = useNavigate();
|
||||
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
||||
|
||||
const [oculusMode, setOculusMode] = useState(false);
|
||||
const [desktopMode, setDesktopMode] = useState(false);
|
||||
const [debugMode, setDebugMode] = useState(false);
|
||||
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
||||
const launchState = useObservable(bsLauncherService.launchState$);
|
||||
const navigateToVersion = (version: BSVersion) => navigate(`/bs-version/${version.BSVersion}`, {state: version});
|
||||
const openFolder = () => ipcService.sendLazy("bs-version.open-folder", {args: state});
|
||||
const exportMaps = () => mapService.exportVersionMaps(state);
|
||||
const verifyFiles = () => bsDownloaderService.download(state, true);
|
||||
|
||||
useEffect(() => {
|
||||
setOculusMode(!!configService.get<boolean>(LaunchMods.OCULUS_MOD));
|
||||
setDesktopMode(!!configService.get<boolean>(LaunchMods.DESKTOP_MOD));
|
||||
setDebugMode(!!configService.get<boolean>(LaunchMods.DEBUG_MOD));
|
||||
}, []);
|
||||
|
||||
const navigateToVersion = (version: BSVersion) => {
|
||||
navigate(`/bs-version/${version.BSVersion}`, {state: version});
|
||||
}
|
||||
|
||||
const setMode = (mode: LaunchMods, value: boolean) => {
|
||||
if(mode === LaunchMods.DEBUG_MOD){ setDebugMode(value); }
|
||||
else if(mode === LaunchMods.OCULUS_MOD){
|
||||
setOculusMode(value);
|
||||
setDesktopMode(false);
|
||||
configService.set(LaunchMods.DESKTOP_MOD, false);
|
||||
const uninstall = async () => {
|
||||
const modalCompleted = await modalService.openModal(ModalType.UNINSTALL, state)
|
||||
if(modalCompleted.exitCode === ModalExitCode.COMPLETED){
|
||||
bsUninstallerService.uninstall(state).then(() => {
|
||||
bsVersionManagerService.askInstalledVersions();
|
||||
const newVersionPage = bsVersionManagerService.getInstalledVersions()[0];
|
||||
navigateToVersion(newVersionPage);
|
||||
})
|
||||
}
|
||||
}
|
||||
else if(mode === LaunchMods.DESKTOP_MOD){
|
||||
setDesktopMode(value);
|
||||
setOculusMode(false);
|
||||
configService.set(LaunchMods.OCULUS_MOD, false);
|
||||
|
||||
const edit = () => {
|
||||
bsVersionManagerService.editVersion(state).then(newVersion => {
|
||||
if(!newVersion){ return; }
|
||||
navigateToVersion(newVersion);
|
||||
});
|
||||
}
|
||||
configService.set(mode, value);
|
||||
}
|
||||
|
||||
const openFolder = () => {
|
||||
ipcService.sendLazy("bs-version.open-folder", {args: state});
|
||||
}
|
||||
|
||||
const uninstall = async () => {
|
||||
const modalCompleted = await modalService.openModal(ModalType.UNINSTALL, state)
|
||||
if(modalCompleted.exitCode === ModalExitCode.COMPLETED){
|
||||
bsUninstallerService.uninstall(state)
|
||||
.then(() => {
|
||||
bsVersionManagerService.askInstalledVersions();
|
||||
const newVersionPage = bsVersionManagerService.getInstalledVersions()[0];
|
||||
navigateToVersion(newVersionPage);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const verifyFiles = () => {
|
||||
bsDownloaderService.download(state, true);
|
||||
}
|
||||
|
||||
const launchBs = () => {
|
||||
bsLauncherService.launch(state, oculusMode, desktopMode, debugMode);
|
||||
}
|
||||
|
||||
const edit = () => {
|
||||
bsVersionManagerService.editVersion(state).then(newVersion => {
|
||||
if(!newVersion){ return; }
|
||||
navigateToVersion(newVersion);
|
||||
});
|
||||
}
|
||||
|
||||
const clone = () => {
|
||||
bsVersionManagerService.cloneVersion(state).then(newVersion => {
|
||||
@@ -103,9 +59,6 @@ export function VersionViewer() {
|
||||
});
|
||||
}
|
||||
|
||||
const exportMaps = () => {
|
||||
mapService.exportVersionMaps(state);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -114,32 +67,18 @@ export function VersionViewer() {
|
||||
<BsmImage className='relative object-cover h-28' image={BSLogo}/>
|
||||
<h1 className='relative text-4xl font-bold italic -top-3'>{state.name ? `${state.BSVersion} - ${state.name}` : state.BSVersion}</h1>
|
||||
<TabNavBar className='mt-3' tabsText={["misc.launch", "misc.maps", "misc.mods"]} onTabChange={(i : number) => setCurrentTabIndex(i)}/>
|
||||
<div className='mt-2 w-full grow flex transition-transform duration-300 pt-5' style={{transform: `translate(${-(currentTabIndex * 100)}%, 0)`}}>
|
||||
<div className='w-full shrink-0 items-center relative flex flex-col justify-start -top-2'>
|
||||
<div className='grid grid-flow-col grid-cols-3 gap-6'>
|
||||
<ToogleLunchMod icon='oculus' onClick={() => setMode(LaunchMods.OCULUS_MOD, !oculusMode)} active={oculusMode} text="pages.version-viewer.launch-mods.oculus"/>
|
||||
<ToogleLunchMod icon='desktop' onClick={() => setMode(LaunchMods.DESKTOP_MOD, !desktopMode)} active={desktopMode} text="pages.version-viewer.launch-mods.desktop"/>
|
||||
<ToogleLunchMod icon='terminal' onClick={() => setMode(LaunchMods.DEBUG_MOD, !debugMode)} active={debugMode} text="pages.version-viewer.launch-mods.debug"/>
|
||||
</div>
|
||||
<div className='grow flex justify-center items-center'>
|
||||
<BsmButton onClick={launchBs} active={JSON.stringify(state) === JSON.stringify(launchState)} className='relative text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform' text="misc.launch"/>
|
||||
</div>
|
||||
</div>
|
||||
<div className='shrink-0 w-full h-full flex justify-center'>
|
||||
<div className='p-4 bg-light-main-color-2 dark:bg-main-color-2 h-fit rounded-md'>
|
||||
<img src={wipGif} alt="" />
|
||||
<span className='block w-full text-center font-bold mt-2 text-gray-800 dark:text-white'>Work In Progress</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-2 w-full min-h-0 grow flex transition-transform duration-300 pt-3' style={{transform: `translate(${-(currentTabIndex * 100)}%, 0)`}}>
|
||||
<LaunchSlide version={state}/>
|
||||
<div className='shrink-0 w-full h-full flex justify-center'>
|
||||
<div className='p-4 bg-light-main-color-2 dark:bg-main-color-2 h-fit rounded-md'>
|
||||
<img src={wipGif} alt="" />
|
||||
<span className='block w-full text-center font-bold mt-2 text-gray-800 dark:text-white'>Work In Progress</span>
|
||||
</div>
|
||||
</div>
|
||||
<ModsSlide version={state}/>
|
||||
</div>
|
||||
</div>
|
||||
<BsmDropdownButton className='absolute top-5 right-5 h-9 w-9' items={[
|
||||
<BsmDropdownButton className='absolute top-5 right-5 h-9 w-9 bg-light-main-color-2 dark:bg-main-color-2 rounded-md' items={[
|
||||
{text: "pages.version-viewer.dropdown.open-folder", icon: "folder", onClick: openFolder},
|
||||
(!state.steam && {text: "pages.version-viewer.dropdown.verify-files", icon: "task", onClick: verifyFiles}),
|
||||
(!state.steam && {text: "Exporter les misc.maps", icon: "export", onClick: exportMaps}),
|
||||
@@ -149,19 +88,4 @@ export function VersionViewer() {
|
||||
]}/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
function ToogleLunchMod(props: {onClick: () => void, active: boolean, text: string, icon: BsmIconType}) {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
return (
|
||||
<div className={`relative rounded-full cursor-pointer group active:scale-95 transition-transform ${!props.active && "shadow-md shadow-black"}`} onClick={() => props.onClick()}>
|
||||
<div className={`absolute glow-on-hover rounded-full ${props.active && "opacity-100 blur-[2px]"}`}/>
|
||||
<div className='w-full h-full pl-6 pr-6 flex justify-center items-center bg-light-main-color-2 dark:bg-main-color-2 p-3 rounded-full text-gray-800 dark:text-white group-hover:bg-light-main-color-1 dark:group-hover:bg-main-color-1'>
|
||||
<BsmIcon icon={props.icon} className='mr-1 h-7 text-gray-800 dark:text-white'/>
|
||||
<span className='w-fit min-w-fit h-full text-lg font-bold uppercase tracking-wide italic'>{t(props.text)}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -114,7 +114,7 @@ export class BsDownloaderService{
|
||||
this.progressBarService.hide(true);
|
||||
this.resetDownload();
|
||||
if(res.success && isFirstCall){ this.notificationService.notifySuccess({title: `notifications.bs-download.success.titles.${isVerification ? "verification-finished" : "download-success"}`, duration: 3000}); }
|
||||
else if(res.data && isFirstCall){ console.log("aaaa"); this.notificationService.notifyError({title: `notifications.types.error`, desc: `notifications.bs-download.errors.msg.${res.data}`, duration: 3000}); }
|
||||
else if(res.data && isFirstCall){ this.notificationService.notifyError({title: `notifications.types.error`, desc: `notifications.bs-download.errors.msg.${res.data}`, duration: 3000}); }
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
import { BSVersion } from "shared/bs-version.interface";
|
||||
import { InstallModsResult, UninstallModsResult } from "shared/models/mods";
|
||||
import { Mod, ModInstallProgression } from "shared/models/mods";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { ModalExitCode, ModalService, ModalType } from "./modale.service";
|
||||
import { NotificationService, NotificationType } from "./notification.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
|
||||
export class BsModsManagerService {
|
||||
|
||||
private static instance: BsModsManagerService;
|
||||
|
||||
private readonly NOTIFICATION_DURATION = 3000;
|
||||
|
||||
private readonly ipcService: IpcService;
|
||||
private readonly progressBar: ProgressBarService;
|
||||
private readonly modals: ModalService;
|
||||
private readonly notifications: NotificationService;
|
||||
|
||||
public readonly isInstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
public readonly isUninstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
public static getInstance(): BsModsManagerService{
|
||||
if(!BsModsManagerService.instance){ BsModsManagerService.instance = new BsModsManagerService(); }
|
||||
return BsModsManagerService.instance;
|
||||
}
|
||||
|
||||
private constructor(){
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.progressBar = ProgressBarService.getInstance();
|
||||
this.modals = ModalService.getInsance();
|
||||
this.notifications = NotificationService.getInstance();
|
||||
}
|
||||
|
||||
public getAvailableMods(version: BSVersion): Promise<Mod[]>{
|
||||
return this.ipcService.send<Mod[], BSVersion>("get-available-mods", {args: version}).then(res => res.data);
|
||||
}
|
||||
|
||||
public getInstalledMods(version: BSVersion): Promise<Mod[]>{
|
||||
return this.ipcService.send<Mod[], BSVersion>("get-installed-mods", {args: version}).then(res => res.data);
|
||||
}
|
||||
|
||||
public installMods(mods: Mod[], version: BSVersion): Promise<void>{
|
||||
if(!this.progressBar.require()){ return undefined; }
|
||||
|
||||
const progress$: Observable<ProgressionInterface> = this.ipcService.watch<ModInstallProgression>("mod-installed").pipe(map(res => {
|
||||
return {progression: res.data.progression, label: res.data.name} as ProgressionInterface
|
||||
}));
|
||||
|
||||
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 => {
|
||||
|
||||
if(res.success && res.data){
|
||||
const isFullyInstalled = res.data.nbInstalledMods === res.data.nbModsToInstall;
|
||||
const title = `notifications.mods.install-mods.titles.${isFullyInstalled ? "success" : "warning"}`;
|
||||
const desc = `notifications.mods.install-mods.msg.${isFullyInstalled ? "success" : "warning"}`;
|
||||
|
||||
this.notifications.notify({type: isFullyInstalled ? NotificationType.SUCCESS : NotificationType.WARNING, title, desc, duration: this.NOTIFICATION_DURATION});
|
||||
}
|
||||
else{
|
||||
this.notifications.notifyError({title: "notifications.types.error", desc: `notifications.mods.install-mods.msg.errors.${res.error}`, duration: this.NOTIFICATION_DURATION})
|
||||
}
|
||||
this.isInstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
});
|
||||
}
|
||||
public async uninstallMod(mod: Mod, version: BSVersion): Promise<void>{
|
||||
|
||||
if(!this.progressBar.require()){ return; }
|
||||
|
||||
const modalRes = await this.modals.openModal(ModalType.UNINSTALL_MOD, mod);
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
|
||||
this.progressBar.show(progress$, true, {paddingLeft: "190px", paddingRight: "190px", bottom: "20px"});
|
||||
|
||||
this.isUninstalling$.next(true);
|
||||
return this.ipcService.send("uninstall-mods", {args: {mods: [mod], version}}).then(res => {
|
||||
|
||||
if(res.success){
|
||||
this.notifications.notifySuccess({title: "notifications.mods.uninstall-mod.titles.success", duration: this.NOTIFICATION_DURATION})
|
||||
}
|
||||
else{
|
||||
this.notifications.notifyError({title: "notifications.types.error", desc: `notifications.mods.uninstall-mod.msg.errors.${res.error}`, duration: this.NOTIFICATION_DURATION})
|
||||
}
|
||||
|
||||
this.isUninstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
});
|
||||
}
|
||||
|
||||
public async uninstallAllMods(version: BSVersion){
|
||||
|
||||
if(!this.progressBar.require()){ return; }
|
||||
|
||||
const modalRes = await this.modals.openModal(ModalType.UNINSTALL_ALL_MODS, version);
|
||||
|
||||
if(modalRes.exitCode !== ModalExitCode.COMPLETED){ return; }
|
||||
|
||||
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
|
||||
this.progressBar.show(progress$, true, {paddingLeft: "190px", paddingRight: "190px", bottom: "20px"});
|
||||
|
||||
this.isUninstalling$.next(true);
|
||||
return this.ipcService.send<UninstallModsResult>("uninstall-all-mods", {args: version}).then(res => {
|
||||
|
||||
if(res.success){
|
||||
this.notifications.notifySuccess({title: "notifications.mods.uninstall-all-mods.titles.success", desc: "notifications.mods.uninstall-all-mods.msg.success", duration: this.NOTIFICATION_DURATION})
|
||||
}
|
||||
else{
|
||||
this.notifications.notifyError({title: "notifications.types.error", desc: `notifications.mods.uninstall-all-mods.msg.errors.${res.error}`, duration: this.NOTIFICATION_DURATION})
|
||||
}
|
||||
|
||||
this.isUninstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
@@ -53,11 +53,10 @@ export class I18nService {
|
||||
public translate(translationKey: string, args?: Record<string, string>): string{
|
||||
let translated = this.cache.get(translationKey);
|
||||
if(!translated){
|
||||
console.log(translationKey);
|
||||
translated = getProperty(this.dictionary, translationKey);
|
||||
translated ? this.cache.set(translated, translationKey) : translated = translationKey;
|
||||
}
|
||||
args && Object.keys(args).forEach(key => translated = translated.replaceAll(`{${key}}`, args[key]));
|
||||
args && Object.keys(args).forEach(key => {translated = translated.replaceAll(`{${key}}`, args[key])});
|
||||
return translated;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,23 +50,25 @@ export class ModalService{
|
||||
|
||||
}
|
||||
|
||||
export enum ModalType {
|
||||
export const enum ModalType {
|
||||
STEAM_LOGIN = "STEAM_LOGIN",
|
||||
GUARD_CODE = "GUARD_CODE",
|
||||
UNINSTALL = "UNINSTALL",
|
||||
INSTALLATION_FOLDER = "INSTALLATION_FOLDER",
|
||||
EDIT_VERSION = "EDIT_VERSION",
|
||||
CLONE_VERSION = "CLONE_VERSION"
|
||||
CLONE_VERSION = "CLONE_VERSION",
|
||||
UNINSTALL_MOD = "UNINSTALL_MOD",
|
||||
UNINSTALL_ALL_MODS = "UNINSTALL_ALL_MODS",
|
||||
}
|
||||
|
||||
export enum ModalExitCode {
|
||||
export const enum ModalExitCode {
|
||||
NO_CHOICE = -1,
|
||||
COMPLETED = 0,
|
||||
CLOSED = 1,
|
||||
CANCELED = 2,
|
||||
}
|
||||
|
||||
export interface ModalResponse<T> {
|
||||
export interface ModalResponse<T = unknown> {
|
||||
exitCode: ModalExitCode,
|
||||
data?: T
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
|
||||
export class PageStateService {
|
||||
|
||||
private static instance: PageStateService;
|
||||
|
||||
private readonly _state$: BehaviorSubject<any> = new BehaviorSubject(undefined);
|
||||
|
||||
public static getInstance(): PageStateService{
|
||||
if(!PageStateService.instance){ PageStateService.instance = new PageStateService(); }
|
||||
return PageStateService.instance;
|
||||
}
|
||||
|
||||
private constructor(){}
|
||||
|
||||
public setState(state: unknown){
|
||||
this._state$.next(state);
|
||||
}
|
||||
|
||||
public getState<T = unknown>(): T{
|
||||
return this._state$.value;
|
||||
}
|
||||
|
||||
public get state$(){
|
||||
return this._state$.asObservable();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -2,6 +2,8 @@ import { distinctUntilChanged, map } from "rxjs/operators";
|
||||
import { BehaviorSubject, Observable, Subscription, timer } from "rxjs";
|
||||
import { IpcService } from "./ipc.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { CSSProperties } from "react";
|
||||
import { ProgressionInterface } from "shared/models/progress-bar";
|
||||
|
||||
export class ProgressBarService{
|
||||
|
||||
@@ -10,8 +12,9 @@ export class ProgressBarService{
|
||||
private readonly ipcService: IpcService;
|
||||
private notificationService: NotificationService;
|
||||
|
||||
private readonly _progression$: BehaviorSubject<number>;
|
||||
private readonly _progression$: BehaviorSubject<ProgressionInterface>;
|
||||
private readonly _visible$: BehaviorSubject<boolean>;
|
||||
private readonly _style$: BehaviorSubject<CSSProperties>;
|
||||
|
||||
private subscription: Subscription;
|
||||
|
||||
@@ -24,51 +27,55 @@ export class ProgressBarService{
|
||||
this.ipcService = IpcService.getInstance();
|
||||
this.notificationService = NotificationService.getInstance();
|
||||
|
||||
this._progression$ = new BehaviorSubject<number>(0);
|
||||
this._progression$ = new BehaviorSubject<ProgressionInterface>({progression: 0});
|
||||
this._visible$ = new BehaviorSubject<boolean>(false);
|
||||
this._style$ = new BehaviorSubject<CSSProperties>(undefined);
|
||||
|
||||
this.progression$.subscribe(progression => this.setSystemProgression(progression));
|
||||
this.progress$.subscribe(value => this.setSystemProgression(value));
|
||||
}
|
||||
|
||||
private setSystemProgression(progression: number){
|
||||
this.ipcService.sendLazy("window.progression", {args: progression});
|
||||
}
|
||||
|
||||
public subscribreTo(obs: Observable<number>){
|
||||
public subscribreTo(obs: Observable<ProgressionInterface|number>){
|
||||
if(this.subscription){ this.unsubscribe(); }
|
||||
this.subscription = obs.pipe(distinctUntilChanged()).subscribe(value => {
|
||||
this.progression$.next(value);
|
||||
if(typeof value === "number"){ return this._progression$.next({progression: value}); }
|
||||
this._progression$.next(value);
|
||||
});
|
||||
}
|
||||
|
||||
public unsubscribe(){
|
||||
this._progression$.next(0);
|
||||
this._progression$.next({progression: 0});
|
||||
this.subscription && this.subscription.unsubscribe();
|
||||
this.subscription = null;
|
||||
}
|
||||
|
||||
public show(obs?: Observable<number>, unsubscribe? : boolean){
|
||||
public show(obs?: Observable<ProgressionInterface|number>, unsubscribe? : boolean, style?: CSSProperties){
|
||||
if(unsubscribe){ this.unsubscribe(); }
|
||||
if(obs){ this.subscribreTo(obs); }
|
||||
this.visible$.next(true);
|
||||
this._visible$.next(true);
|
||||
this._style$.next(style);
|
||||
}
|
||||
|
||||
public showFake(speed: number): void{
|
||||
const obs = timer(1000, 100).pipe(map(val => {
|
||||
if(this.progression$.value >= 100){ return 100; }
|
||||
public showFake(speed: number, style?: CSSProperties): void{
|
||||
const obs: Observable<ProgressionInterface> = timer(1000, 100).pipe(map(val => {
|
||||
if(this._progression$.value.progression >= 100){ return {progression: 100}; }
|
||||
const currentProgress = speed * (val + 1);
|
||||
return Math.round(Math.atan(currentProgress) / (Math.PI / 2) * 100 * 1000) / 1000;
|
||||
const progress = Math.round(Math.atan(currentProgress) / (Math.PI / 2) * 100 * 1000) / 1000;
|
||||
return {progression: progress} as ProgressionInterface;
|
||||
}));
|
||||
this.show(obs, true);
|
||||
this.show(obs, true, style);
|
||||
}
|
||||
|
||||
public complete(): void{
|
||||
this.progression$.next(100);
|
||||
this._progression$.next({progression: 100});
|
||||
}
|
||||
|
||||
public hide(unsubscribe: boolean){
|
||||
public hide(unsubscribe: boolean = true){
|
||||
if(unsubscribe){ this.unsubscribe(); }
|
||||
this.visible$.next(false);
|
||||
this._visible$.next(false);
|
||||
}
|
||||
|
||||
public require(): boolean{
|
||||
@@ -79,9 +86,13 @@ export class ProgressBarService{
|
||||
return true;
|
||||
}
|
||||
|
||||
public get progression$(): BehaviorSubject<number>{ return this._progression$; }
|
||||
public get visible$(): BehaviorSubject<boolean>{ return this._visible$; }
|
||||
public get progressData$(): Observable<ProgressionInterface>{ return this._progression$.asObservable(); }
|
||||
public get progress$(): Observable<number>{ return this._progression$.pipe(map(data => data.progression)); }
|
||||
public get progressLabel(): Observable<string>{ return this._progression$.pipe(map(data => data?.label)); }
|
||||
|
||||
public get visible$(): Observable<boolean>{ return this._visible$.asObservable(); }
|
||||
public get isVisible(): boolean{ return this._visible$.value; }
|
||||
public get style$(): Observable<CSSProperties>{ return this._style$.asObservable(); }
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export { ModInstallProgression, InstallModsResult, UninstallModsResult } from "./mod-ipc.model"
|
||||
export { Mod, DownloadLink, DownloadLinkType, FileHashes, ModAuthor } from "./mod.interface"
|
||||
@@ -0,0 +1,14 @@
|
||||
export interface ModInstallProgression{
|
||||
name: string,
|
||||
progression: number
|
||||
}
|
||||
|
||||
export interface InstallModsResult {
|
||||
nbModsToInstall: number,
|
||||
nbInstalledMods: number
|
||||
}
|
||||
|
||||
export interface UninstallModsResult {
|
||||
nbModsToUninstall: number,
|
||||
nbUninstalledMods: number
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { ProgressionInterface } from "./progress-bar.model"
|
||||
@@ -0,0 +1,4 @@
|
||||
export interface ProgressionInterface {
|
||||
progression: number,
|
||||
label?: string
|
||||
};
|
||||
Reference in New Issue
Block a user