mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
49 Commits
v1.0.2-alpha.1
...
v1.0.4
| Author | SHA1 | Date | |
|---|---|---|---|
| ccbcc8296d | |||
| bea435439e | |||
| d9ff78c21d | |||
| 14a4bf3408 | |||
| 172d7e4578 | |||
| 5986cf7a15 | |||
| a5e4b745ef | |||
| 4cd538fe21 | |||
| c406dfee79 | |||
| 8a168d20b3 | |||
| b16f7150e8 | |||
| c4319f1c6c | |||
| d9003fa07b | |||
| eb2235e017 | |||
| 1e784c243b | |||
| 23e9b52af2 | |||
| e42ac4eca3 | |||
| 622595a7a0 | |||
| 556ca1b1e6 | |||
| cef4f22305 | |||
| ee1dd7b21f | |||
| 487b73a859 | |||
| a56fdde0e7 | |||
| 99f91d25c3 | |||
| 8579b0697d | |||
| f0761c8359 | |||
| b1833721d0 | |||
| aba099e15a | |||
| d25c0cced5 | |||
| bc4f6304db | |||
| 4357454b14 | |||
| 528abbcaa3 | |||
| 9bcc605e00 | |||
| f7e3b9ae9d | |||
| 1b963a99c1 | |||
| 363cc3cc5e | |||
| 8307a975ea | |||
| f5ecdfca60 | |||
| 5bb92d804b | |||
| 5b4b962015 | |||
| b43cfdd70b | |||
| 77350a823e | |||
| 5844aea281 | |||
| 8af5d8c3e4 | |||
| f0f3721ec5 | |||
| 5641c25cc1 | |||
| c1b32c2fc3 | |||
| dc70d2889f | |||
| 72daaeee0e |
+1
-1
@@ -1 +1 @@
|
||||
custom: https://mee6.gg/m/bsmanager
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
<a href="https://github.com/Zagrios/bs-manager/issues"><img alt="GitHub issues" src="https://img.shields.io/github/issues/Zagrios/bs-manager?style=for-the-badge"></a>
|
||||
<br>
|
||||
<a href="https://discord.gg/uSqbHVpKdV"><img src ="https://img.shields.io/badge/-DISCORD-5865f2?style=for-the-badge&logo=discord&logoColor=ffffff" alt="discord"/></a>
|
||||
<a href="https://mee6.xyz/fr/m/bsmanager"><img src ="https://img.shields.io/badge/-🥰%20Support%20BSM-EC4546?style=for-the-badge" alt="Donation"/></a>
|
||||
<!--<a href="https://mee6.xyz/fr/m/bsmanager"><img src ="https://img.shields.io/badge/-🥰%20Support%20BSM-EC4546?style=for-the-badge" alt="Donation"/></a>-->
|
||||
<a href="https://twitter.com/BSManager_"><img src ="https://img.shields.io/badge/-Twitter-F5F8FA?style=for-the-badge&logo=Twitter" alt="Twitter"/></a>
|
||||
<br>
|
||||
[<a href="#readme-bot">Go to bottom</a>]
|
||||
|
||||
@@ -509,5 +509,13 @@
|
||||
"ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans//32055887/d9ed8771498be7fc83721aaaaa5b15a08fbeadb1.png",
|
||||
"ReleaseDate": "1670943003",
|
||||
"year": "2022"
|
||||
},
|
||||
{
|
||||
"BSVersion": "1.28.0",
|
||||
"BSManifest": "7463243155526905423",
|
||||
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/3666530358620442392",
|
||||
"ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans//32055887/8f978bbf292810004bca12523e76be8a7eec8c4e.png",
|
||||
"ReleaseDate": "1677593822",
|
||||
"year": "2023"
|
||||
}
|
||||
]
|
||||
@@ -18,16 +18,20 @@
|
||||
"settings": "Settings"
|
||||
},
|
||||
"pages": {
|
||||
"shared-maps":{
|
||||
"title": "Shared maps"
|
||||
},
|
||||
"version-viewer": {
|
||||
"launch-mods": {
|
||||
"oculus": "Oculus Mod",
|
||||
"oculus-description": "If you are running Beat Saber through Steam, this allows you to play the game on an Oculus headset.",
|
||||
"desktop": "Desktop Mod",
|
||||
"oculus": "Oculus Mode",
|
||||
"oculus-description": "If you are using Beat Saber via Steam, it allows you to use Oculus's VR composer without passing by SteamVR, to gain performances This is not necessary for using Oculus headsets.",
|
||||
"desktop": "Desktop Mode",
|
||||
"desktop-description": "This allows you to use WASD and the mouse to navigate around the menu in game. This makes testing much easier, because you don't have to put on your headset!",
|
||||
"debug": "Debug Mod",
|
||||
"debug": "Debug Mode",
|
||||
"debug-description": "Enables the output log window for IPA. This will show the debug console that mods use."
|
||||
},
|
||||
"maps": {
|
||||
"title": "Shared maps",
|
||||
"search-bar": {
|
||||
"search-placeholder": "Search a map",
|
||||
"filters-btn": "Filters",
|
||||
@@ -45,7 +49,7 @@
|
||||
"link-maps": {
|
||||
"tooltips": {
|
||||
"link": "Link maps",
|
||||
"unlink": "Unling maps"
|
||||
"unlink": "Unlink maps"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -443,10 +447,16 @@
|
||||
"label": "Stay connected"
|
||||
}
|
||||
},
|
||||
"why-credentials": "Why do you need my credentials ?",
|
||||
"buttons": {
|
||||
"submit": "Login"
|
||||
}
|
||||
},
|
||||
"steam-credentials":{
|
||||
"title": "Steam Credentials",
|
||||
"p-1": "The credentials are only used to download the game because steam need to verify that you paid the game in order to be allowed to download it. They aren't saved and directly passed to DepotDownloader. If you don't want to enter your credentials you can follow this tutorial :",
|
||||
"p-2": "and then click on the gear icon in the top right corner and select \"Import a version\", select the folder where beat saber has been downloaded (If you follow the tutorial above you should have the correct location)"
|
||||
},
|
||||
"bs-import-version":{
|
||||
"title": "Import a version",
|
||||
"description": "Imports a version of BeatSaber to use with BSManager. The import will copy the selected BeatSaber installation folder to the BSManager versions folder.",
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
"settings": "Ajustes"
|
||||
},
|
||||
"pages": {
|
||||
"shared-maps":{
|
||||
"title": "Mapas comunes"
|
||||
},
|
||||
"version-viewer": {
|
||||
"launch-mods": {
|
||||
"oculus": "Oculus Mod",
|
||||
"oculus-description": "Si estás ejecutando Beat Saber a través de Steam, esto te permite jugar en un auricular Oculus.",
|
||||
"oculus-description": "Si estás usando Beat Saber a través de Steam, te permite usar el compositor VR de Oculus sin pasar por SteamVR, para ganar prestaciones. Esto no es necesario para usar auriculares Oculus.",
|
||||
"desktop": "Desktop Mod",
|
||||
"desktop-description": "Esto te permite usar WASD y el mouse para navegar por el menú en juego. Esto hace que las pruebas sean mucho más fáciles, ¡porque no tienes que ponerte el auricular!",
|
||||
"debug": "Debug Mod",
|
||||
@@ -443,10 +446,16 @@
|
||||
"label": "Manténgase conectado"
|
||||
}
|
||||
},
|
||||
"why-credentials": "¿Por qué necesitas mis credenciales?",
|
||||
"buttons": {
|
||||
"submit": "Inicio de sesión"
|
||||
}
|
||||
},
|
||||
"steam-credentials":{
|
||||
"title": "Steam Credentials",
|
||||
"p-1": "Las credenciales sólo se utilizan para descargar el juego porque steam necesita verificar que has pagado el juego para que se te permita descargarlo. No se guardan y se pasan directamente a DepotDownloader. Si no quieres introducir tus credenciales puedes seguir este tutorial :",
|
||||
"p-2": "y, a continuación, haga clic en el icono de engranaje en la esquina superior derecha y seleccione \"Importar una versión\", seleccione la carpeta donde se ha descargado beat saber (Si usted sigue el tutorial anterior debe tener la ubicación correcta)"
|
||||
},
|
||||
"bs-import-version":{
|
||||
"title": "Importar una versión",
|
||||
"description": "Importa una versión de BeatSaber para usar con BSManager. La importación copiará la carpeta de instalación seleccionada de BeatSaber en la carpeta de versiones de BSManager.",
|
||||
|
||||
@@ -18,10 +18,13 @@
|
||||
"settings": "Paramètres"
|
||||
},
|
||||
"pages": {
|
||||
"shared-maps":{
|
||||
"title": "Maps partagées"
|
||||
},
|
||||
"version-viewer": {
|
||||
"launch-mods": {
|
||||
"oculus": "Mode Oculus",
|
||||
"oculus-description": "Si vous exécutez Beat Saber via Steam, cela vous permet de jouer au jeu sur un casque Oculus.",
|
||||
"oculus-description": "Si vous utilisez Beat Saber via Steam, cela vous permet d'utiliser le compositeur VR d'Oculus sans passer par SteamVR, pour gagner en performances. Ceci n'est pas nécessaire pour utiliser les casques Oculus.",
|
||||
"desktop": "Mode Bureau",
|
||||
"desktop-description": "Cela vous permet d'utiliser WASD et la souris pour naviguer dans le menu en jeu. Cela rend les tests beaucoup plus faciles, car vous n'avez pas à mettre votre casque!",
|
||||
"debug": "Mode Debug",
|
||||
@@ -443,10 +446,16 @@
|
||||
"label": "Rester connecté"
|
||||
}
|
||||
},
|
||||
"why-credentials": "Pourquoi mes identifiants sont nécessaires ?",
|
||||
"buttons": {
|
||||
"submit": "Se connecter"
|
||||
}
|
||||
},
|
||||
"steam-credentials":{
|
||||
"title": "Steam Credentials",
|
||||
"p-1": "Les informations d'identification ne sont utilisées que pour télécharger le jeu, car Steam doit vérifier que vous avez payé le jeu afin d'être autorisé à le télécharger. Ils ne sont pas sauvegardés et transmis directement à DepotDownloader. Si vous ne voulez pas entrer vos informations d'identification, vous pouvez suivre ce tutoriel :",
|
||||
"p-2": "puis cliquez sur l'icône de l'engrenage dans le coin supérieur droit et sélectionnez \"Importer une version\", sélectionnez le dossier où beat saber a été téléchargé (Si vous suivez le tutoriel ci-dessus, vous devriez avoir le bon emplacement)."
|
||||
},
|
||||
"bs-import-version":{
|
||||
"title": "Importer une version",
|
||||
"description": "Importe une version de BeatSaber pour profiter des fonctionnalités de BSManager. L'importation copiera le dossier d'installation de BeatSaber sélectionné dans le dossier de versions de BSManager.",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.0.2-alpha.1",
|
||||
"version": "1.0.4",
|
||||
"description": "A foundation for scalable desktop apps",
|
||||
"main": "./dist/main/main.js",
|
||||
"author": {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { UtilsService } from "../services/utils.service";
|
||||
import { IpcRequest } from "shared/models/ipc";
|
||||
import { SearchParams } from "shared/models/maps/beat-saver.model";
|
||||
import { BeatSaverService } from "../services/thrid-party/beat-saver/beat-saver.service";
|
||||
import log from "electron-log";
|
||||
|
||||
ipcMain.on("bsv-search-map", async (event, request: IpcRequest<SearchParams>) => {
|
||||
const utlis = UtilsService.getInstance();
|
||||
@@ -22,6 +23,7 @@ ipcMain.on("bsv-get-map-details-from-hashs", async (event, request: IpcRequest<s
|
||||
bsvService.getMapDetailsFromHashs(request.args).then(maps => {
|
||||
utlis.ipcSend(request.responceChannel, {success: true, data: maps});
|
||||
}).catch(e => {
|
||||
log.error(e);
|
||||
utlis.ipcSend(request.responceChannel, {success: false, error: e});
|
||||
})
|
||||
});
|
||||
|
||||
@@ -6,19 +6,17 @@ import { BSLocalVersionService } from "../bs-local-version.service";
|
||||
import { InstallationLocationService } from "../installation-location.service";
|
||||
import { UtilsService } from "../utils.service";
|
||||
import crypto from "crypto";
|
||||
import { lstatSync, symlinkSync, unlinkSync, readdirSync, createWriteStream } from "fs";
|
||||
import { lstatSync, symlinkSync, unlinkSync } from "fs";
|
||||
import { copy, copySync } from "fs-extra";
|
||||
import StreamZip from "node-stream-zip";
|
||||
import { RequestService } from "../request.service";
|
||||
import sanitize from "sanitize-filename";
|
||||
import archiver from "archiver";
|
||||
import { DeepLinkService } from "../deep-link.service";
|
||||
import log from 'electron-log';
|
||||
import { WindowManagerService } from "../window-manager.service";
|
||||
import { ipcMain } from "electron";
|
||||
import { IpcRequest } from 'shared/models/ipc';
|
||||
import { Observable } from "rxjs";
|
||||
import { defer } from "rxjs";
|
||||
import { Archive } from "../../models/archive.class";
|
||||
|
||||
export class LocalMapsManagerService {
|
||||
@@ -53,12 +51,12 @@ export class LocalMapsManagerService {
|
||||
this.deepLink = DeepLinkService.getInstance();
|
||||
this.windows = WindowManagerService.getInstance();
|
||||
|
||||
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, (link) => {
|
||||
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, link => {
|
||||
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link);
|
||||
this.openOneClickDownloadMapWindow(new URL(link).host);
|
||||
});
|
||||
|
||||
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ScoreSaber, (link) => {
|
||||
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ScoreSaber, link => {
|
||||
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.ScoreSaber, link);
|
||||
this.openOneClickDownloadMapWindow(new URL(link).host, true);
|
||||
});
|
||||
@@ -76,18 +74,16 @@ export class LocalMapsManagerService {
|
||||
|
||||
private async computeMapHash(mapPath: string, rawInfoString: string): Promise<string>{
|
||||
const mapRawInfo = JSON.parse(rawInfoString);
|
||||
let content = rawInfoString;
|
||||
const shasum = crypto.createHash("sha1");
|
||||
shasum.update(rawInfoString);
|
||||
for(const set of mapRawInfo._difficultyBeatmapSets){
|
||||
for(const diff of set._difficultyBeatmaps){
|
||||
const diffFilePath = path.join(mapPath, diff._beatmapFilename);
|
||||
if(!await this.utils.pathExist(diffFilePath)){ continue; }
|
||||
const diffContent = (await this.utils.readFileAsync(diffFilePath)).toString();
|
||||
content += diffContent;
|
||||
const diffContent = await this.utils.readFileAsync(diffFilePath).catch(() => null);
|
||||
diffContent && shasum.update(diffContent);
|
||||
}
|
||||
}
|
||||
|
||||
const shasum = crypto.createHash("sha1");
|
||||
shasum.update(content);
|
||||
|
||||
return shasum.digest("hex");
|
||||
}
|
||||
|
||||
@@ -96,7 +92,7 @@ export class LocalMapsManagerService {
|
||||
|
||||
if(!(await this.utils.pathExist(infoFilePath))){ return null; }
|
||||
|
||||
const rawInfoString = await (await (this.utils.readFileAsync(infoFilePath))).toString();
|
||||
const rawInfoString = await this.utils.readFileAsync(infoFilePath);
|
||||
|
||||
const rawInfo: RawMapInfoData = JSON.parse(rawInfoString);
|
||||
const coverUrl = new URL(`file:///${path.join(mapPath, rawInfo._coverImageFilename)}`).href;
|
||||
@@ -145,14 +141,22 @@ export class LocalMapsManagerService {
|
||||
|
||||
progression.total = levelsPaths.length;
|
||||
|
||||
for(const levelPath of levelsPaths){
|
||||
const promises = levelsPaths.map(async levelPath => {
|
||||
const mapInfo = await this.loadMapInfoFromPath(levelPath);
|
||||
if(mapInfo){
|
||||
progression.maps.push(mapInfo);
|
||||
progression.loaded = progression.maps.length;
|
||||
observer.next({...progression, maps: []});
|
||||
if(!mapInfo){ return null; }
|
||||
progression.loaded++;
|
||||
observer.next(progression);
|
||||
return mapInfo;
|
||||
});
|
||||
|
||||
const mapsInfo = (await Promise.allSettled(promises)).reduce((acc, mapInfo) => {
|
||||
if(mapInfo.status === "fulfilled" && mapInfo.value){
|
||||
acc.push(mapInfo.value);
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
} ,[] as BsmLocalMap[]);
|
||||
|
||||
progression.maps = mapsInfo;
|
||||
|
||||
observer.next(progression);
|
||||
|
||||
@@ -222,7 +226,6 @@ export class LocalMapsManagerService {
|
||||
observer.next(progress);
|
||||
}
|
||||
}catch(e){
|
||||
console.log(e);
|
||||
observer.error(e);
|
||||
}
|
||||
observer.complete();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable prefer-promise-reject-errors */
|
||||
import { BS_APP_ID, BS_DEPOT } from "../constants";
|
||||
import path from "path";
|
||||
import { BSVersion, PartialBSVersion } from 'shared/bs-version.interface';
|
||||
@@ -10,6 +11,7 @@ import { BSLocalVersionService } from "./bs-local-version.service";
|
||||
import isOnline from 'is-online';
|
||||
import { WindowManagerService } from "./window-manager.service";
|
||||
import { copy, copySync } from "fs-extra";
|
||||
import { clean, satisfies } from "semver";
|
||||
|
||||
export class BSInstallerService{
|
||||
|
||||
@@ -38,8 +40,12 @@ export class BSInstallerService{
|
||||
return BSInstallerService.instance;
|
||||
}
|
||||
|
||||
private escapeSpaces(path: string): string{
|
||||
return `\"${path}\"`
|
||||
}
|
||||
|
||||
private getDepotDownloaderExePath(): string{
|
||||
return path.join(this.utils.getAssetsScriptsPath(), 'depot-downloader', 'DepotDownloader.exe');
|
||||
return this.escapeSpaces(path.join(this.utils.getAssetsScriptsPath(), 'depot-downloader', 'DepotDownloader.exe'));
|
||||
}
|
||||
|
||||
private removeSpecialSchar(txt: string): string{ return txt.replaceAll(/\[|\]/g, ""); }
|
||||
@@ -66,16 +72,19 @@ export class BSInstallerService{
|
||||
});
|
||||
}
|
||||
|
||||
public async isDotNet6Installed(): Promise<boolean>{
|
||||
public async isDotNet6Installed(): Promise<boolean>{
|
||||
try{
|
||||
const process = spawnSync(this.getDepotDownloaderExePath());
|
||||
const out = process.output.toString();
|
||||
if(out.includes(".NET runtime can be found at")){ return false; }
|
||||
const process = spawnSync(this.getDepotDownloaderExePath(), {shell: true});
|
||||
if(process.stderr.toString()){
|
||||
log.error("no dotnet", process.stderr.toString());
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
catch(e){
|
||||
log.error("Error while checking .NET 6", e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async downloadBsVersion(downloadInfos: DownloadInfo): Promise<DownloadEvent>{
|
||||
@@ -100,8 +109,7 @@ export class BSInstallerService{
|
||||
`-depot ${BS_DEPOT}`,
|
||||
`-manifest ${bsVersion.BSManifest}`,
|
||||
`-username ${downloadInfos.username}`,
|
||||
`-dir \"${this.localVersionService.getVersionFolder(downloadVersion)}\"`,
|
||||
(downloadInfos.stay || !downloadInfos.password) && "-remember-password"
|
||||
`-dir \"${this.localVersionService.getVersionFolder(downloadVersion)}\"`
|
||||
],
|
||||
{shell: true, cwd: this.installLocationService.versionsDirectory}
|
||||
);
|
||||
@@ -152,16 +160,25 @@ export class BSInstallerService{
|
||||
this.downloadProcess.stdout.on('error', (err) => {
|
||||
log.error("BS-DOWNLOAD ERROR", err.toString());
|
||||
this.killDownloadProcess();
|
||||
if(err.toString().includes(".NET") || err.toString().includes("dotnet") || err.toString().includes(".dll")){
|
||||
reject("dotnet");
|
||||
}
|
||||
reject();
|
||||
});
|
||||
this.downloadProcess.stderr.on('data', (err) => {
|
||||
log.error("BS-DOWNLOAD ERROR", err.toString());
|
||||
this.killDownloadProcess();
|
||||
if(err.toString().includes(".NET") || err.toString().includes("dotnet") || err.toString().includes(".dll")){
|
||||
reject("dotnet");
|
||||
}
|
||||
reject();
|
||||
});
|
||||
this.downloadProcess.stderr.on('error', (err) => {
|
||||
log.error("BS-DOWNLOAD ERROR", err.toString());
|
||||
this.killDownloadProcess();
|
||||
if(err.toString().includes(".NET") || err.toString().includes("dotnet") || err.toString().includes(".dll")){
|
||||
reject("dotnet");
|
||||
}
|
||||
reject();
|
||||
});
|
||||
|
||||
|
||||
@@ -44,12 +44,21 @@ export class BSLauncherService{
|
||||
|
||||
if(!this.utilsService.pathExist(exePath)){ return "EXE_NOT_FINDED"; }
|
||||
|
||||
const launchMods = [launchOptions.oculus && "-vrmode oculus", launchOptions.desktop && "fpfc", launchOptions.debug && "--verbose"];
|
||||
const launchMods = [];
|
||||
|
||||
this.bsProcess = spawn(`\"${exePath}\"`, launchMods, {shell: true, cwd, env: {...process.env, "SteamAppId": BS_APP_ID}});
|
||||
if(launchOptions.oculus){ launchMods.push("-vrmode oculus"); }
|
||||
if(launchOptions.desktop){ launchMods.push("fpfc"); }
|
||||
if(launchOptions.debug){ launchMods.push("--verbose"); }
|
||||
|
||||
this.bsProcess.on('message', msg => { /** EMIT HERE **/ });
|
||||
this.bsProcess.on('error', err => { /** EMIT HERE **/ });
|
||||
if(launchOptions.debug){
|
||||
this.bsProcess = spawn(`\"${exePath}\"`, launchMods, {shell: true, cwd, env: {...process.env, "SteamAppId": BS_APP_ID}, detached: true, windowsVerbatimArguments: true });
|
||||
}
|
||||
else{
|
||||
this.bsProcess = spawn(`\"${exePath}\"`, launchMods, {shell: true, cwd, env: {...process.env, "SteamAppId": BS_APP_ID} });
|
||||
}
|
||||
|
||||
this.bsProcess.on('message', msg => console.log(msg));
|
||||
this.bsProcess.on('error', err => console.log(err));
|
||||
this.bsProcess.on('exit', code => this.utilsService.ipcSend("bs-launch.exit", {data: code, success: code === 0}));
|
||||
|
||||
return "LAUNCHED";
|
||||
|
||||
@@ -35,7 +35,7 @@ export class BSVersionLibService{
|
||||
|
||||
private async getLocalVersions(): Promise<BSVersion[]>{
|
||||
const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE);
|
||||
const rawVersion = (await this.utilsService.readFileAsync(localVersionsPath)).toString();
|
||||
const rawVersion = await this.utilsService.readFileAsync(localVersionsPath);
|
||||
return JSON.parse(rawVersion);
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,6 @@ export class IpcService {
|
||||
observable.subscribe(data => {
|
||||
this.send(channel, window, data);
|
||||
}, error => {
|
||||
console.log("LAAAA 2", error);
|
||||
this.send(this.getErrorChannel(channel), window, error);
|
||||
}, () => {
|
||||
this.send(this.getCompleteChannel(channel), window);
|
||||
|
||||
@@ -43,7 +43,7 @@ export class SupportersService {
|
||||
|
||||
private async getLocalSupporters(): Promise<Supporter[]>{
|
||||
const patreonsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.PATREONS_FILE);
|
||||
const rawPatreons = (await this.utilsService.readFileAsync(patreonsPath)).toString();
|
||||
const rawPatreons = await this.utilsService.readFileAsync(patreonsPath);
|
||||
return JSON.parse(rawPatreons);
|
||||
}
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ export class BeatSaverService {
|
||||
return res;
|
||||
}, [] as BsvMapDetail[]);
|
||||
|
||||
await Promise.all(chunkHash.map(async hashs => {
|
||||
await Promise.allSettled(chunkHash.map(async hashs => {
|
||||
const res = await this.bsaverApi.getMapsDetailsByHashs(hashs);
|
||||
|
||||
if(res.status === 200){
|
||||
|
||||
@@ -9,6 +9,7 @@ import { IpcResponse } from "shared/models/ipc";
|
||||
import log from "electron-log";
|
||||
import { AppWindow } from "shared/models/window-manager/app-window.model";
|
||||
|
||||
|
||||
// TODO : REFACTOR
|
||||
|
||||
export class UtilsService{
|
||||
@@ -64,8 +65,8 @@ export class UtilsService{
|
||||
}
|
||||
|
||||
public readFileAsync(path: string){
|
||||
return new Promise<Buffer>((resolve, reject) => {
|
||||
readFile(path, (err, data) => {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
readFile(path, {encoding: "utf-8", flag: "r"}, (err, data) => {
|
||||
if(err){ reject(err); }
|
||||
else{ resolve(data); }
|
||||
});
|
||||
@@ -73,9 +74,12 @@ export class UtilsService{
|
||||
}
|
||||
|
||||
public listDirsInDir(dirPath: string, fullPath = false): string[]{
|
||||
let files = readdirSync(dirPath, { withFileTypes:true});
|
||||
files = files.filter(f => f.isDirectory())
|
||||
return files.map(f => fullPath ? path.join(dirPath, f.name) : f.name);
|
||||
let files = readdirSync(dirPath, { withFileTypes:true });
|
||||
return files.reduce((acc, f) => {
|
||||
if(!f.isDirectory()){ return acc; }
|
||||
acc.push(fullPath ? path.join(dirPath, f.name) : f.name);
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
|
||||
public async deleteFolder(folderPath: string): Promise<void>{
|
||||
|
||||
@@ -51,7 +51,6 @@ export function FilterPanel({className, ref, playlist = false, filter, onChange,
|
||||
firstRun.current = false;
|
||||
return;
|
||||
}
|
||||
console.log(filter, firstFilter);
|
||||
setHaveChanged(() => !equal(filter, firstFilter));
|
||||
}, [filter])
|
||||
|
||||
@@ -214,7 +213,7 @@ export function FilterPanel({className, ref, playlist = false, filter, onChange,
|
||||
</div>
|
||||
{onApply && (
|
||||
<div className="inline float-right relative w-fit h-fit mt-2">
|
||||
<BsmButton className="inline float-right rounded-md font-bold px-1 py-0.5 text-sm" text="Appliquer" typeColor="primary" withBar={false} onClick={handleApply}/>
|
||||
<BsmButton className="inline float-right rounded-md font-bold px-1 py-0.5 text-sm" text="misc.apply" typeColor="primary" withBar={false} onClick={handleApply}/>
|
||||
<GlowEffect visible={haveChanged}/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+2
-3
@@ -117,7 +117,6 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
|
||||
const loadMapsObs$ = mapsManager.getMaps(version);
|
||||
|
||||
loadMapsObs$.pipe(map(progess => {
|
||||
console.log(progess);
|
||||
return Math.floor(((progess.loaded / progess.total) * 100));
|
||||
})).subscribe(percent => loadPercent$.next(percent));
|
||||
|
||||
@@ -304,7 +303,7 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
|
||||
if(!maps){
|
||||
return (
|
||||
<div ref={ref} className={className}>
|
||||
<div className="h-full flex flex-col items-center justify-center flex-wrap gap-1">
|
||||
<div className="h-full flex flex-col items-center justify-center flex-wrap gap-1 text-gray-800 dark:text-gray-200">
|
||||
<BsmImage className="w-32 h-32 spin-loading" image={BeatWaitingImg}/>
|
||||
<span className="font-bold">{t("modals.download-maps.loading-maps")}</span>
|
||||
<TextProgressBar value$={loadPercent$}/>
|
||||
@@ -316,7 +315,7 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
|
||||
if(!maps.length){
|
||||
return (
|
||||
<div ref={ref} className={className}>
|
||||
<div className="h-full flex flex-col items-center justify-center flex-wrap gap-1">
|
||||
<div className="h-full flex flex-col items-center justify-center flex-wrap gap-1 text-gray-800 dark:text-gray-200">
|
||||
<BsmImage className="h-32" image={BeatConflict}/>
|
||||
<span className="font-bold">{t("pages.version-viewer.maps.tabs.maps.empty-maps.text")}</span>
|
||||
<BsmButton className="font-bold rounded-md p-2" text="pages.version-viewer.maps.tabs.maps.empty-maps.button" typeColor="primary" withBar={false} onClick={e => {e.preventDefault(); mapsDownloader.openDownloadMapModal(version)}}/>
|
||||
|
||||
@@ -93,7 +93,12 @@ export const MapItem = memo(({hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
return diffLabel;
|
||||
}
|
||||
|
||||
const openPreview = () => linkOpener.open(previewUrl, true);
|
||||
const openPreview = () => {
|
||||
if(audioPlayer.playing){
|
||||
audioPlayer.pause();
|
||||
}
|
||||
linkOpener.open(previewUrl, true);
|
||||
}
|
||||
const copyBsr = () => navigator.clipboard.writeText(`!bsr ${mapId}`);
|
||||
const toogleMusic = () => {
|
||||
if(songPlaying){
|
||||
@@ -225,12 +230,12 @@ export const MapItem = memo(({hash, title, autor, songAutor, coverUrl, songUrl,
|
||||
<span className="absolute w-[10px] h-[10px] bottom-0 right-full bg-inherit" style={{clipPath: 'path("M11 11 L11 0 L10 0 A10 10 0 0 1 0 10 L 0 11 Z")'}}/>
|
||||
|
||||
<div className="flex flex-col justify-center items-center gap-1 w-full h-full overflow-hidden opacity-0 group-hover:opacity-100">
|
||||
{onDelete && !downloading && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-200" iconColor={color} icon="trash" withBar={false} onClick={e => {e.stopPropagation(); onDelete(callBackParam)}}/>}
|
||||
{onDownload && !downloading && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-200" iconColor={color} icon="download" withBar={false} onClick={e => {e.stopPropagation(); onDownload(callBackParam)}}/>}
|
||||
{onCancelDownload && !downloading && <BsmButton className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-200" iconColor="red" icon="cross" withBar={false} onClick={e => {e.stopPropagation(); onCancelDownload(callBackParam)}}/>}
|
||||
{downloading && <BsmBasicSpinner className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2 flex items-center justify-center" spinnerClassName="brightness-200" style={{color}} thikness="3px"/>}
|
||||
{previewUrl && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-200" iconColor={color} icon="eye" withBar={false} onClick={e => {e.stopPropagation(); openPreview()}}/>}
|
||||
{mapId && <BsmButton className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-200" iconColor={color} icon="twitch" withBar={false} onClick={e => {e.stopPropagation(); copyBsr()}}/>}
|
||||
{onDelete && !downloading && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-75 dark:brightness-200" iconColor={color} icon="trash" withBar={false} onClick={e => {e.stopPropagation(); onDelete(callBackParam)}}/>}
|
||||
{onDownload && !downloading && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-75 dark:brightness-200" iconColor={color} icon="download" withBar={false} onClick={e => {e.stopPropagation(); onDownload(callBackParam)}}/>}
|
||||
{onCancelDownload && !downloading && <BsmButton className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-75 dark:brightness-200" iconColor="red" icon="cross" withBar={false} onClick={e => {e.stopPropagation(); onCancelDownload(callBackParam)}}/>}
|
||||
{downloading && <BsmBasicSpinner className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2 flex items-center justify-center" spinnerClassName="brightness-75 dark:brightness-200" style={{color}} thikness="3px"/>}
|
||||
{previewUrl && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-75 dark:brightness-200" iconColor={color} icon="eye" withBar={false} onClick={e => {e.stopPropagation(); openPreview()}}/>}
|
||||
{mapId && <BsmButton className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-75 dark:brightness-200" iconColor={color} icon="twitch" withBar={false} onClick={e => {e.stopPropagation(); copyBsr()}}/>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@ export function MapsPlaylistsPanel({version}: Props) {
|
||||
<TabNavBar className="!rounded-none shadow-sm" tabIndex={tabIndex} tabsText={["misc.maps", "misc.playlists"]} onTabChange={setTabIndex} renderTab={renderTab}/>
|
||||
<div className="w-full grow min-h-0 flex flex-row items-center transition-transform duration-300" style={{transform: `translate(${-(tabIndex * 100)}%, 0)`}}>
|
||||
<LocalMapsListPanel ref={mapsRef} className="w-full h-full shrink-0 flex flex-col" version={version} filter={mapFilter} search={mapSearch}/>
|
||||
<div className="w-full h-full shrink-0 flex flex-col justify-center items-center content-center gap-2 overflow-hidden">
|
||||
<div className="w-full h-full shrink-0 flex flex-col justify-center items-center content-center gap-2 overflow-hidden text-gray-800 dark:text-gray-200">
|
||||
<BsmImage className="rounded-md" image={wipGif}/>
|
||||
<span>Coming soon</span>
|
||||
</div>
|
||||
|
||||
@@ -2,14 +2,17 @@ import { useState } from "react";
|
||||
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 { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
|
||||
import { ModalComponent, ModalExitCode, ModalService } from "renderer/services/modale.service";
|
||||
import BeatImpatient from '../../../../../assets/images/apngs/beat-impatient.png'
|
||||
import { WhyCredentialsModal } from "./why-credentials-modal.component";
|
||||
|
||||
export const LoginModal: ModalComponent<{username: string, password: string, stay: boolean}> = ({resolver}) => {
|
||||
|
||||
const modal = ModalService.getInsance();
|
||||
|
||||
const [username, setUsername] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [stay, setStay] = useState(true);
|
||||
const [stay, setStay] = useState(false);
|
||||
const t = useTranslation();
|
||||
|
||||
const loggin = () => {
|
||||
@@ -17,10 +20,15 @@ export const LoginModal: ModalComponent<{username: string, password: string, sta
|
||||
resolver({exitCode: ModalExitCode.COMPLETED, data:{username, password, stay}});
|
||||
}
|
||||
|
||||
const whyCredentials = () => {
|
||||
modal.openModal(WhyCredentialsModal);
|
||||
}
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => {e.preventDefault(); loggin();}}>
|
||||
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.steam-login.title")}</h1>
|
||||
<BsmImage className="mx-auto h-20" image={BeatImpatient} />
|
||||
|
||||
<div className="mb-2">
|
||||
<label className="block font-bold cursor-pointer tracking-wide text-gray-800 dark:text-gray-200" htmlFor="username">{t("modals.steam-login.inputs.username.label")}</label>
|
||||
<input className="w-full bg-light-main-color-1 dark:bg-main-color-1 px-1 py-[2px] rounded-md outline-none" onChange={e => setUsername(e.target.value)} value={username} type="text" name="username" id="username" placeholder={t("modals.steam-login.inputs.username.placeholder")}/>
|
||||
@@ -29,10 +37,9 @@ export const LoginModal: ModalComponent<{username: string, password: string, sta
|
||||
<label className="block font-bold cursor-pointer tracking-wide text-gray-800 dark:text-gray-200" htmlFor="password">{t("modals.steam-login.inputs.password.label")}</label>
|
||||
<input className="w-full bg-light-main-color-1 dark:bg-main-color-1 px-1 py-[2px] rounded-md outline-none" onChange={e => setPassword(e.target.value)} value={password} type="password" name="password" id="password" placeholder={t("modals.steam-login.inputs.password.placeholder")}/>
|
||||
</div>
|
||||
<div className="flex items-center content-center justify-start mb-3">
|
||||
<input onChange={e => setStay(e.target.checked)} checked={stay} className="mr-1" type="checkbox" name="stay" id="stay" />
|
||||
<label className="cursor-pointer text-gray-800 dark:text-gray-200" htmlFor="stay">{t("modals.steam-login.inputs.stay.label")}</label>
|
||||
</div>
|
||||
|
||||
<span onClick={whyCredentials} className="underline my-4 block cursor-pointer">{t("modals.steam-login.why-credentials")}</span>
|
||||
|
||||
<div className="grid grid-flow-col grid-cols-2 gap-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.steam-login.buttons.submit"/>
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { BsmButton } from "renderer/components/shared/bsm-button.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service"
|
||||
|
||||
export const WhyCredentialsModal: ModalComponent<void> = ({resolver}) => {
|
||||
|
||||
const t = useTranslation();
|
||||
const linkOpener = LinkOpenerService.getInstance();
|
||||
|
||||
const openTutorial = () => {
|
||||
linkOpener.open("https://steamcommunity.com/sharedfiles/filedetails/?id=1805934840");
|
||||
}
|
||||
|
||||
return (
|
||||
<form className="w-min">
|
||||
<h1 className="text-3xl mb-2 uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.steam-credentials.title")}</h1>
|
||||
|
||||
<p>{t("modals.steam-credentials.p-1")}</p>
|
||||
<a onClick={e => {e.preventDefault; openTutorial()}} className="underline text-blue-600 hover:text-blue-900 dark:text-blue-400 dark:hover:text-blue-600 mb-2 block cursor-pointer">
|
||||
https://steamcommunity.com/sharedfiles/filedetails/?id=1805934840
|
||||
</a>
|
||||
<p>{t("modals.steam-credentials.p-2")}</p>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export function Modal() {
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{ModalComponent && (
|
||||
{ModalComponent && resolver && (
|
||||
<div className="top-0 absolute w-screen h-screen flex content-center items-center justify-center z-[90]">
|
||||
<motion.span key="modal-overlay" onClick={() => modalSevice.resolve({exitCode: ModalExitCode.NO_CHOICE})} className="absolute top-0 bottom-0 right-0 left-0 bg-black" initial={{opacity: 0}} animate={{opacity: ModalComponent && .60}} exit={{opacity: 0}} transition={{duration: .2}}/>
|
||||
<motion.div key="modal" initial={{y: "100vh"}} animate={{y: 0}} exit={{y: "100vh"}}>
|
||||
|
||||
@@ -26,8 +26,8 @@ export function NavBar() {
|
||||
<nav id='nav-bar' className='z-10 flex flex-col h-full max-h-full items-center p-1 bg-light-main-color-1 dark:bg-main-color-1'>
|
||||
<BsManagerIcon className='relative aspect-square w-16 h-16 mb-3'/>
|
||||
<ol id='versions' className='w-fit max-w-[120px] relative left-[2px] grow overflow-y-hidden scrollbar-track-transparent scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 hover:overflow-y-scroll'>
|
||||
<MapsNavBarItem/>
|
||||
<NavBarSpliter/>
|
||||
<MapsNavBarItem/>
|
||||
<NavBarSpliter/>
|
||||
{installedVersions && installedVersions.map((version) => <BsVersionItem key={JSON.stringify(version)} version={version}/>)}
|
||||
</ol>
|
||||
<NavBarSpliter/>
|
||||
|
||||
@@ -52,7 +52,7 @@ export function BsmButton({className, style, imgClassName, iconClassName, icon,
|
||||
return "";
|
||||
})();
|
||||
|
||||
const handleClick = (e: MouseEvent) => !disabled && onClick(e);
|
||||
const handleClick = (e: MouseEvent) => !disabled && onClick?.(e);
|
||||
|
||||
return (
|
||||
<div ref={ref} onClick={handleClick} title={t(title)} className={`${className} overflow-hidden cursor-pointer group ${(!withBar && !disabled && (!!typeColor || !!color)) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, backgroundColor: primaryColor || color}}>
|
||||
|
||||
@@ -11,9 +11,10 @@ type Props = {
|
||||
max: number,
|
||||
step?: number,
|
||||
renderLabel?: (value: number) => JSX.Element,
|
||||
colors?: string[]
|
||||
}
|
||||
|
||||
export function BsmRange({colorType = "first-color", values, onChange, onFinalChange, min, max, renderLabel, step = 1} : Props) {
|
||||
export function BsmRange({colorType = "first-color", values, onChange, onFinalChange, min, max, renderLabel, step = 1, colors} : Props) {
|
||||
|
||||
const color = useThemeColor(colorType);
|
||||
const labelTextColor = getCorrectTextColor(color);
|
||||
@@ -35,10 +36,10 @@ export function BsmRange({colorType = "first-color", values, onChange, onFinalCh
|
||||
style={{
|
||||
...props.style,
|
||||
background: getTrackBackground({
|
||||
values: values,
|
||||
colors: ["#ccc", color, "#ccc"],
|
||||
min: min,
|
||||
max: max
|
||||
values,
|
||||
colors: colors ?? ["#ccc", color, "#ccc"],
|
||||
min,
|
||||
max
|
||||
}),
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -48,11 +48,14 @@ import { TwitterIcon } from "./icons/twitter-icon.component";
|
||||
import { Mee6Icon } from "./icons/mee6-icon.component";
|
||||
import { InfoIcon } from "./icons/info-icon.component";
|
||||
import { SyncIcon } from "./icons/sync-icon.component";
|
||||
import { VolumeUpIcon } from "./icons/volume-up-icon.component";
|
||||
import { VolumeOffIcon } from "./icons/volume-off-icon.component";
|
||||
import { VolumeDownIcon } from "./icons/volume-down-icon.component";
|
||||
|
||||
export type BsmIconType = BsvMapCharacteristic | (
|
||||
"settings"|"trash"|"favorite"|"folder"|"bsNote"|"check"|"three-dots"|"twitch"|"eye"|"play"|"checkCircleIcon"|"discord"|"info"|
|
||||
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|"github"|"close"|"thumbUpFill"|"timerFill"|"pause"|"twitter"|"sync"|
|
||||
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|"bsMapDifficulty"|"link"|"unlink"|"download"|"filter"|"mee6"|
|
||||
"copy"|"steam"|"edit"|"export"|"patreon"|"search"|"bsMapDifficulty"|"link"|"unlink"|"download"|"filter"|"mee6"|"volume-up"|"volume-off"|"volume-down"|
|
||||
"fr-FR-flag"|"es-ES-flag"|"en-US-flag"|"en-EN-flag"
|
||||
);
|
||||
|
||||
@@ -107,6 +110,9 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
|
||||
if(icon === "mee6"){ return <Mee6Icon className={className} style={style}/> }
|
||||
if(icon === "info"){ return <InfoIcon className={className} style={style}/> }
|
||||
if(icon === "sync"){ return <SyncIcon className={className} style={style}/> }
|
||||
if(icon === "volume-up"){ return <VolumeUpIcon className={className} style={style}/> }
|
||||
if(icon === "volume-down"){ return <VolumeDownIcon className={className} style={style}/> }
|
||||
if(icon === "volume-off"){ return <VolumeOffIcon className={className} style={style}/> }
|
||||
return <TrashIcon className={className} style={style}/>
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ export function TwitterIcon(props: {className?: string, style?: CSSProperties})
|
||||
return (
|
||||
<svg className={props.className} style={props.style} viewBox="0 0 24 24" fill="currentColor">
|
||||
<g>
|
||||
<path d="M23.643 4.937c-.835.37-1.732.62-2.675.733.962-.576 1.7-1.49 2.048-2.578-.9.534-1.897.922-2.958 1.13-.85-.904-2.06-1.47-3.4-1.47-2.572 0-4.658 2.086-4.658 4.66 0 .364.042.718.12 1.06-3.873-.195-7.304-2.05-9.602-4.868-.4.69-.63 1.49-.63 2.342 0 1.616.823 3.043 2.072 3.878-.764-.025-1.482-.234-2.11-.583v.06c0 2.257 1.605 4.14 3.737 4.568-.392.106-.803.162-1.227.162-.3 0-.593-.028-.877-.082.593 1.85 2.313 3.198 4.352 3.234-1.595 1.25-3.604 1.995-5.786 1.995-.376 0-.747-.022-1.112-.065 2.062 1.323 4.51 2.093 7.14 2.093 8.57 0 13.255-7.098 13.255-13.254 0-.2-.005-.402-.014-.602.91-.658 1.7-1.477 2.323-2.41z"></path>
|
||||
<path d="M23.643 4.937c-.835.37-1.732.62-2.675.733.962-.576 1.7-1.49 2.048-2.578-.9.534-1.897.922-2.958 1.13-.85-.904-2.06-1.47-3.4-1.47-2.572 0-4.658 2.086-4.658 4.66 0 .364.042.718.12 1.06-3.873-.195-7.304-2.05-9.602-4.868-.4.69-.63 1.49-.63 2.342 0 1.616.823 3.043 2.072 3.878-.764-.025-1.482-.234-2.11-.583v.06c0 2.257 1.605 4.14 3.737 4.568-.392.106-.803.162-1.227.162-.3 0-.593-.028-.877-.082.593 1.85 2.313 3.198 4.352 3.234-1.595 1.25-3.604 1.995-5.786 1.995-.376 0-.747-.022-1.112-.065 2.062 1.323 4.51 2.093 7.14 2.093 8.57 0 13.255-7.098 13.255-13.254 0-.2-.005-.402-.014-.602.91-.658 1.7-1.477 2.323-2.41z"/>
|
||||
</g>
|
||||
</svg>
|
||||
)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function VolumeDownIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 96 960 960" fill="currentColor">
|
||||
<path d="M251.283 675.999q-13.564 0-22.423-8.859-8.859-8.859-8.859-22.423V507.283q0-13.564 8.859-22.423 8.859-8.859 22.423-8.859h117.179L486.794 357.67q14.769-14.769 33.987-6.91 19.218 7.858 19.218 28.781v392.661q0 21.18-19.218 29.038-19.218 7.859-33.987-6.91L368.462 675.999H251.283Zm374.358 49.949V424.463q43.05 19.102 68.704 60.551 25.654 41.448 25.654 90.986 0 50.205-25.654 90.525-25.654 40.32-68.704 59.423ZM489.744 429.436l-99.129 96.82H270.256v99.488h120.359l99.129 97.23V429.436ZM378.923 576Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function VolumeOffIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 96 960 960" fill="currentColor">
|
||||
<path d="M770.974 963.435 663.179 855.64q-15.436 9.948-31.423 18.512-15.987 8.564-33.244 15-12.845 4.846-23.909-3.397-11.064-8.243-11.064-22.756 0-6.82 4.013-12.064 4.012-5.243 10.243-7.397 12.872-4.487 25.436-10.41t24-13.692L468.153 659.948v112.254q0 21.18-19.218 29.038-19.217 7.859-33.986-6.91L296.617 675.999H179.438q-13.564 0-22.423-8.859-8.859-8.859-8.859-22.423V507.283q0-13.564 8.859-22.423 8.859-8.859 22.423-8.859H283.54L94.155 286.36q-7.23-7.231-7.218-17.705.013-10.474 7.628-18.089 7.616-7.359 17.692-7.359 10.077 0 17.692 7.359l677.485 677.075q7.231 7.231 7.295 17.705.064 10.474-7.295 18.089-7.615 7.615-18.23 7.615t-18.23-7.615ZM598.512 260.591q97.769 38.231 157.551 123.423Q815.845 469.206 815.845 575q0 51.795-15.449 100.09t-44.398 90.217l-35.819-35.819q22.59-34.026 34-73.193Q765.59 617.129 765.59 575q0-91.743-51.308-165.628-51.308-73.885-136.487-103.321-6.82-2.153-10.538-7.692-3.718-5.538-3.718-12.358 0-14.051 11.269-22.025 11.269-7.974 23.704-3.385ZM376.257 568.718Zm257.28 74.129-69.998-69.999V427.847q40.461 22 62.538 61.961Q648.153 529.77 648.153 576q0 17.693-3.654 34.5-3.654 16.808-10.962 32.347ZM468.153 477.463l-86.306-86.692 33.102-33.101q14.769-14.769 33.986-6.782 19.218 7.987 19.218 28.653v97.922Zm-50.255 247.819V610.359l-84.103-84.103H198.411v99.488H318.36l99.538 99.538Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { CSSProperties } from "react";
|
||||
|
||||
export function VolumeUpIcon(props: {className?: string, style?: CSSProperties}) {
|
||||
return (
|
||||
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 96 960 960" fill="currentColor">
|
||||
<path d="M596.512 889.152q-12.846 4.846-23.91-3.416-11.063-8.263-11.063-22.794 0-6.763 4.012-12.007 4.013-5.243 10.244-7.397 84.769-32 136.282-104.936Q763.589 665.667 763.589 575t-51.512-163.808q-51.513-73.141-136.282-104.73-6.821-2.154-10.539-7.965-3.717-5.812-3.717-12.587 0-13.96 11.268-21.934 11.269-7.974 23.705-3.385 97.64 38.231 157.486 123.339 59.846 85.107 59.846 191.07t-59.846 191.07q-59.846 85.108-157.486 123.082ZM177.438 675.999q-13.468 0-22.375-8.907t-8.907-22.375V507.283q0-13.468 8.907-22.375t22.375-8.907h117.178L412.948 357.67q14.769-14.769 33.987-6.722 19.218 8.047 19.218 28.593v392.661q0 20.803-19.218 28.85t-33.987-6.722L294.616 675.999H177.438Zm374.357 49.949V424.463q43.051 19.102 68.704 60.551 25.654 41.448 25.654 90.986 0 50.205-25.654 90.525-25.653 40.32-68.704 59.423ZM415.898 429.436l-99.128 96.82H196.411v99.488H316.77l99.128 97.23V429.436ZM318.872 576Z"/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
@@ -1,37 +1,54 @@
|
||||
import { useState } from 'react';
|
||||
import { useObservable } from 'renderer/hooks/use-observable.hook';
|
||||
import { useThemeColor } from 'renderer/hooks/use-theme-color.hook';
|
||||
import { AudioPlayerService } from 'renderer/services/audio-player.service';
|
||||
import { IpcService } from 'renderer/services/ipc.service';
|
||||
import { WindowManagerService } from 'renderer/services/window-manager.service';
|
||||
import { AppWindow } from 'shared/models/window-manager/app-window.model';
|
||||
import { BsmButton } from '../shared/bsm-button.component';
|
||||
import { BsmRange } from '../shared/bsm-range.component';
|
||||
import { BsmIconType } from '../svgs/bsm-icon.component';
|
||||
import './title-bar.component.css'
|
||||
|
||||
export default function TitleBar({template = "index.html"} : {template: AppWindow}) {
|
||||
|
||||
const ipcService = IpcService.getInstance();
|
||||
const windows = WindowManagerService.getInstance();
|
||||
const ipcService = IpcService.getInstance();
|
||||
const windows = WindowManagerService.getInstance();
|
||||
const audio = AudioPlayerService.getInstance();
|
||||
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
const volume = useObservable(audio.volume$, audio.volume);
|
||||
const playing = useObservable(audio.playing$);
|
||||
const color = useThemeColor("first-color");
|
||||
|
||||
const closeWindow = () => {
|
||||
return windows.close(template);
|
||||
const [maximized, setMaximized] = useState(false);
|
||||
|
||||
const closeWindow = () => {
|
||||
return windows.close(template);
|
||||
}
|
||||
|
||||
const maximizeWindow = () => {
|
||||
ipcService.sendLazy('window.maximize');
|
||||
}
|
||||
|
||||
const maximizeWindow = () => {
|
||||
ipcService.sendLazy('window.maximize');
|
||||
}
|
||||
const minimizeWindow = () => {
|
||||
ipcService.sendLazy('window.minimize');
|
||||
}
|
||||
|
||||
const minimizeWindow = () => {
|
||||
ipcService.sendLazy('window.minimize');
|
||||
}
|
||||
const resetWindow = () => {
|
||||
ipcService.sendLazy('window.reset');
|
||||
}
|
||||
|
||||
const resetWindow = () => {
|
||||
ipcService.sendLazy('window.reset');
|
||||
}
|
||||
const toogleMaximize = () => {
|
||||
if(maximized){ resetWindow(); }
|
||||
else{ maximizeWindow(); }
|
||||
setMaximized(!maximized);
|
||||
}
|
||||
|
||||
const toogleMaximize = () => {
|
||||
if(maximized){ resetWindow(); }
|
||||
else{ maximizeWindow(); }
|
||||
setMaximized(!maximized);
|
||||
}
|
||||
const volumeIcon: BsmIconType = (() => {
|
||||
if(volume.muted || !volume.volume){ return "volume-off"; }
|
||||
if(volume.volume < .5){ return "volume-down"; }
|
||||
return "volume-up";
|
||||
})()
|
||||
|
||||
if(template === "index.html"){
|
||||
|
||||
@@ -42,7 +59,13 @@ export default function TitleBar({template = "index.html"} : {template: AppWindo
|
||||
<span className='text-gray-800 dark:text-gray-100 font-bold text-xs italic'>BSManager</span>
|
||||
</div>
|
||||
</div>
|
||||
<div id="window-controls" className="h-full flex shrink-0">
|
||||
<div id="window-controls" className="h-full flex shrink-0 items-center">
|
||||
<div className='h-full text-gray-800 dark:text-gray-200 pr-1 cursor-pointer flex flex-row justify-end items-center gap-2 group pl-5'>
|
||||
<div className='shrink-0 w-0 overflow-hidden transition-all group-hover:w-16 group-hover:overflow-visible group-active:w-16 group-active:overflow-visible text-main-color-3'>
|
||||
<BsmRange min={0} max={1} step={.01} values={[volume.muted ? 0 : volume.volume]} colors={[color, "currentColor"]} onChange={val => audio.setVolume(val[0])} onFinalChange={val => audio.setFinalVolume(val[0])}/>
|
||||
</div>
|
||||
<BsmButton className='shrink-0 h-[23px] w-[23px] aspect-square !bg-transparent flex items-start' iconClassName={volumeIcon === "volume-down" && "-translate-x-[1.8px]"} icon={volumeIcon} withBar={false} onClick={() => audio.toggleMute()}/>
|
||||
</div>
|
||||
<div onClick={minimizeWindow} className="text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-[#4F545C] cursor-pointer w-11 h-full shrink-0 flex justify-center items-center" id="min-button" >
|
||||
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12"><rect fill="currentColor" width="10" height="1" x="1" y="6"> </rect></svg>
|
||||
</div>
|
||||
|
||||
@@ -20,16 +20,14 @@ export function LaunchSlide({version}: Props) {
|
||||
const launchState = useObservable(bsLauncherService.launchState$);
|
||||
|
||||
const setMode = (mode: LaunchMods, value: boolean) => {
|
||||
if(mode === LaunchMods.DEBUG_MOD){ setDebugMode(value); }
|
||||
if(mode === LaunchMods.DEBUG_MOD){
|
||||
setDebugMode(value);
|
||||
}
|
||||
else if(mode === LaunchMods.OCULUS_MOD){
|
||||
setOculusMode(value);
|
||||
setDesktopMode(false);
|
||||
configService.set(LaunchMods.DESKTOP_MOD, false);
|
||||
setOculusMode(value);
|
||||
}
|
||||
else if(mode === LaunchMods.DESKTOP_MOD){
|
||||
setDesktopMode(value);
|
||||
setOculusMode(false);
|
||||
configService.set(LaunchMods.OCULUS_MOD, false);
|
||||
setDesktopMode(value);
|
||||
}
|
||||
configService.set(mode, value);
|
||||
}
|
||||
|
||||
@@ -69,16 +69,14 @@ export function ModsGrid({modsMap, installed, modsSelected, onModChange, moreInf
|
||||
]}/>
|
||||
</span>
|
||||
|
||||
{
|
||||
Array.from(modsMap.keys()).map(key => modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && (
|
||||
{Array.from(modsMap.keys()).map(key => modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && (
|
||||
<ul key={key} className="contents">
|
||||
<h2 className="col-span-full py-1 font-bold pl-3">{key}</h2>
|
||||
{modsMap.get(key).map(mod => mod.name.toLowerCase().includes(filter) && (
|
||||
<ModItem key={mod.name} className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1 hover:cursor-pointer" mod={mod} installedVersion={installedModVersion(key, mod)} isDependency={isDependency(mod)} isSelected={isSelected(mod)} onChange={(val) => onModChange(val, mod)} onWantInfo={onWantInfos} wantInfo={mod.name === moreInfoMod?.name}/>
|
||||
))}
|
||||
</ul>
|
||||
))
|
||||
}
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export function ModsSlide({version, onDisclamerDecline}: {version: BSVersion, on
|
||||
<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'>
|
||||
{isOnline && modsAvailable ? (
|
||||
<>
|
||||
<div className="overflow-scroll w-full min-h-0 scrollbar-thin scrollbar-thumb-neutral-900 scrollbar-thumb-rounded-full">
|
||||
<div className="grow 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">
|
||||
@@ -148,7 +148,7 @@ export function ModsSlide({version, onDisclamerDecline}: {version: BSVersion, on
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center">
|
||||
<div className="w-full h-full flex flex-col items-center justify-center text-gray-800 dark:text-gray-200">
|
||||
<img className={`w-32 h-32 ${isOnline && "spin-loading"}`} src={isOnline ? BeatWaitingImg : BeatConflictImg} alt=" "/>
|
||||
<span className="text-xl mt-3 h-0 italic">{t(isOnline ? "pages.version-viewer.mods.loading-mods" : "pages.version-viewer.mods.no-internet")}</span>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { MapsPlaylistsPanel } from "renderer/components/maps-mangement-components/maps-playlists-panel.component";
|
||||
import { Slideshow } from "renderer/components/slideshow/slideshow.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
|
||||
export function MapsPage() {
|
||||
|
||||
const t = useTranslation();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex items-center flex-col pt-2">
|
||||
<Slideshow className="absolute w-full h-full top-0"/>
|
||||
<h1 className="text-gray-100 text-2xl z-[1]">Maps partagées</h1>
|
||||
<h1 className="text-gray-100 text-2xl z-[1]">{t("pages.shared-maps.title")}</h1>
|
||||
<div className="w-full min-h-0 grow p-10 flex flex-col items-center justify-start text-white relative">
|
||||
<MapsPlaylistsPanel/>
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,6 @@ export function VersionViewer() {
|
||||
const [currentTabIndex, setCurrentTabIndex] = useState(0);
|
||||
|
||||
const navigateToVersion = (version?: BSVersion) => {
|
||||
console.log(version);
|
||||
if(!version){
|
||||
return navigate("/available-versions");
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { BehaviorSubject } from "rxjs";
|
||||
import { Observable, BehaviorSubject } from "rxjs";
|
||||
import { ConfigurationService } from "./configuration.service";
|
||||
|
||||
export class AudioPlayerService{
|
||||
|
||||
@@ -10,18 +10,38 @@ export class AudioPlayerService{
|
||||
return AudioPlayerService.instance;
|
||||
}
|
||||
|
||||
private readonly config: ConfigurationService;
|
||||
|
||||
private readonly player: HTMLAudioElement;
|
||||
|
||||
private readonly _src$: BehaviorSubject<string> = new BehaviorSubject("");
|
||||
private readonly _playing$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
private readonly _bpm$: BehaviorSubject<number> = new BehaviorSubject(0);
|
||||
private readonly _volume$: BehaviorSubject<PlayerVolume>;
|
||||
|
||||
private lastVolume: number;
|
||||
|
||||
private constructor(){
|
||||
|
||||
this.config = ConfigurationService.getInstance();
|
||||
|
||||
this._volume$ = new BehaviorSubject(
|
||||
this.config.get<PlayerVolume>("audio-level") || { volume: 0.5, muted: false }
|
||||
);
|
||||
|
||||
this.lastVolume = this._volume$.value.volume;
|
||||
|
||||
this.player = new Audio();
|
||||
|
||||
this.player.onplay = () => this._playing$.next(true);
|
||||
this.player.onpause = () => this._playing$.next(false);
|
||||
this.player.onended = () => this._playing$.next(false);
|
||||
|
||||
this._volume$.subscribe(volume => {
|
||||
this.player.volume = volume.volume;
|
||||
this.player.muted = volume.muted;
|
||||
this.config.set("audio-level", volume);
|
||||
});
|
||||
}
|
||||
|
||||
public play(src: string, bpm = 0): Promise<void>{
|
||||
@@ -41,6 +61,31 @@ export class AudioPlayerService{
|
||||
return this.player.play();
|
||||
}
|
||||
|
||||
public setVolume(volume: number): void{
|
||||
const playerVolume: PlayerVolume = {muted: volume <= 0, volume};
|
||||
this._volume$.next(playerVolume);
|
||||
}
|
||||
|
||||
public setFinalVolume(volume: number): void{
|
||||
if(volume > 0){ this.lastVolume = volume; }
|
||||
const playerVolume: PlayerVolume = {muted: volume <= 0, volume: this.lastVolume};
|
||||
this._volume$.next(playerVolume);
|
||||
}
|
||||
|
||||
public mute(): void{
|
||||
const playerVolume = {...this._volume$.value, muted: true};
|
||||
this._volume$.next(playerVolume);
|
||||
}
|
||||
|
||||
public unmute(): void{
|
||||
const playerVolume = {...this._volume$.value, muted: false};
|
||||
this._volume$.next(playerVolume);
|
||||
}
|
||||
|
||||
public toggleMute(): void{
|
||||
this.muted ? this.unmute() : this.mute();
|
||||
}
|
||||
|
||||
public get src$(): Observable<string>{
|
||||
return this._src$.asObservable();
|
||||
}
|
||||
@@ -50,9 +95,19 @@ export class AudioPlayerService{
|
||||
public get bpm$(): Observable<number>{
|
||||
return this._bpm$.asObservable();
|
||||
}
|
||||
public get volume$(): Observable<PlayerVolume>{
|
||||
return this._volume$.asObservable();
|
||||
}
|
||||
|
||||
public get src(): string{ return this._src$.value; }
|
||||
public get playing(): boolean{ return this._playing$.value; }
|
||||
public get bpm(): number{ return this._bpm$.value; }
|
||||
public get volume(): PlayerVolume{ return this._volume$.value; }
|
||||
public get muted(): boolean{ return this.player.muted; }
|
||||
|
||||
}
|
||||
|
||||
interface PlayerVolume{
|
||||
volume: number;
|
||||
muted: boolean;
|
||||
}
|
||||
@@ -96,11 +96,7 @@ export class BsDownloaderService{
|
||||
return this.ipcService.send<boolean>("is-dotnet-6-installed").then(res => res.success && res.data)
|
||||
}
|
||||
|
||||
public async download(bsVersion: BSVersion, isVerification?: boolean, isFirstCall = true): Promise<IpcResponse<DownloadEvent>>{
|
||||
if(isFirstCall && !this.progressBarService.require()){ return {success: false}; }
|
||||
|
||||
if(isFirstCall && !(await this.isDotNet6Installed())){
|
||||
|
||||
private async showDotNetNotInstalledError(): Promise<void>{
|
||||
const choice = await this.notificationService.notifyError({
|
||||
duration: 11_000,
|
||||
title: "notifications.bs-download.errors.titles.dotnet-required",
|
||||
@@ -111,7 +107,16 @@ export class BsDownloaderService{
|
||||
if(choice === "0"){
|
||||
this.linkOpener.open("https://dotnet.microsoft.com/en-us/download/dotnet/thank-you/runtime-6.0.12-windows-x64-installer");
|
||||
}
|
||||
}
|
||||
|
||||
public async download(bsVersion: BSVersion, isVerification?: boolean, isFirstCall = true): Promise<IpcResponse<DownloadEvent>>{
|
||||
|
||||
// TODO : to remake cause we don't need recursion anymore
|
||||
|
||||
if(isFirstCall && !this.progressBarService.require()){ return {success: false}; }
|
||||
|
||||
if(isFirstCall && !(await this.isDotNet6Installed())){
|
||||
await this.showDotNetNotInstalledError();
|
||||
return {success: false};
|
||||
}
|
||||
|
||||
@@ -142,7 +147,13 @@ 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){ this.notificationService.notifyError({title: `notifications.types.error`, desc: `notifications.bs-download.errors.msg.${res.data}`, duration: 3000}); }
|
||||
else if(res.data === "dotnet" && isFirstCall){
|
||||
await this.showDotNetNotInstalledError();
|
||||
return res;
|
||||
}
|
||||
else if(res.data && isFirstCall){
|
||||
this.notificationService.notifyError({title: `notifications.types.error`, desc: `notifications.bs-download.errors.msg.${res.data}`, duration: 3000});
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
@@ -115,8 +115,6 @@ export class MapsManagerService {
|
||||
|
||||
const progress$ = this.ipcService.sendV2<DeleteMapsProgress>("delete-maps", {args: maps}).pipe(map(progress => (progress.deleted / progress.total) * 100));
|
||||
|
||||
progress$.subscribe(console.log);
|
||||
|
||||
showProgressBar && this.progressBar.show(progress$, true);
|
||||
|
||||
progress$.toPromise().finally(() => this.progressBar.hide(true));
|
||||
|
||||
Reference in New Issue
Block a user