Compare commits

...

35 Commits

Author SHA1 Message Date
MathieuG-P c264684ec6 bumb 1.2.0-alpha.3 2023-06-05 14:44:27 +02:00
MathieuG-P aa32f4e2f4 Merge pull request #235 from Zagrios/hotfix/tweak-model-images-for-better-viewing
[hotfix] tweak models ui to improve images visibility
2023-06-05 14:29:50 +02:00
MathieuG-P bfafe0e4b1 [hotfix] tweak models ui to improve images visibility 2023-06-05 14:28:10 +02:00
MathieuG-P a8c36a5e7c nothing 2023-06-04 17:46:12 +02:00
MathieuG-P de3284922a Merge pull request #233 from Zagrios/hotfix/make-supporter-service-use-request-service
[hotfix] Supporter service now use Request service
2023-06-04 17:44:11 +02:00
MathieuG-P d018ac5c9a [hotfix] Supporter service now use Request service 2023-06-04 17:43:53 +02:00
MathieuG-P e69983d6a7 Merge pull request #232 from Zagrios/hotfix/faster-inital-mod-loading
[hotfix] Faster inital loading of mods
2023-06-04 17:19:53 +02:00
MathieuG-P 2a8f4feea8 [hotfix] Faster inital loading of mods 2023-06-04 17:19:30 +02:00
MathieuG-P b08aa4c1d9 Add supporter 2023-06-04 12:52:35 +02:00
MathieuG-P c0e169cead Merge pull request #231 from Zagrios/hotfix/some-mods-not-marked-as-installed-but-installed
[hotfix] Not filter out "declined" mod when detecting installed mods
2023-06-04 00:57:34 +02:00
MathieuG-P 99f087a89d [hotfix] Not filter out "declined" mod when detecting installed mods + add logs 2023-06-04 00:52:49 +02:00
MathieuG-P e0fd024314 Merge pull request #229 from Zagrios/hotfix/some-mods-not-update-after-install
[hotfix] Fix some mods not update after install
2023-06-03 21:54:03 +02:00
MathieuG-P 599e04a96f [hotfix] Fix some mods not update after install 2023-06-03 21:53:35 +02:00
MathieuG-P 77b838187c Merge pull request #225 from Zagrios/hotfix/add-warning-for-steam-username-max-length
[hotfix] Add warning if the password exceed 64 characters
2023-05-29 21:00:38 +02:00
MathieuG-P 0ce2b9493a [hotfix] Add warning if the password exceed 64 characters 2023-05-29 20:59:27 +02:00
MathieuG-P 8124577728 Merge branch 'master' of https://github.com/Zagrios/bs-manager 2023-05-27 18:21:40 +02:00
MathieuG-P 2e5929c74c add supporter 2023-05-27 18:21:38 +02:00
Iluhadesu a4908ef850 Add v1.30.0 to bs_versions.json 2023-05-25 04:05:50 +00:00
MathieuG-P cf0fa2b787 bumb 1.2.0-alpha.2 2023-05-24 23:57:48 +02:00
MathieuG-P 03d4d14edd Merge pull request #223 from Zagrios/hotfix/double-click-on-model-to-download
[hotfix] add double click to download model
2023-05-24 23:03:44 +02:00
MathieuG-P 3bdbcfcfae [hotfix] add double click to download model 2023-05-24 23:03:04 +02:00
MathieuG-P cebf83be7d Merge pull request #222 from Zagrios/hotfix/add-indication-to-indicate-models-details-can-expand
Hotfix/add indication to indicate models details can expand
2023-05-24 22:09:35 +02:00
MathieuG-P 2c89a8a44c Merge branch 'master' into hotfix/add-indication-to-indicate-models-details-can-expand 2023-05-24 22:03:52 +02:00
MathieuG-P e0060973b6 Merge pull request #221 from Zagrios/hotfix/make-models-slightly-bigger
[hotfix] Make model item slightly bigger
2023-05-24 22:03:12 +02:00
MathieuG-P 0c0dd34026 [hotfix] Add indicator to indicate model details can expand 2023-05-24 22:00:54 +02:00
MathieuG-P 5f63aaac45 [hotfix] Make model item slightly bigger 2023-05-24 21:32:33 +02:00
MathieuG-P 52cdc50fa8 Merge pull request #220 from Zagrios/bugfix/maps-loading-conflict-on-version-change/157
Bugfix/maps loading conflict on version change/157
2023-05-23 22:29:38 +02:00
MathieuG-P e1bfcd7bc0 [bugfix-157] no need of cancellable observable 2023-05-23 22:29:11 +02:00
MathieuG-P 1009a4c02a [bugfix-157] Fix loading maps percent collision 2023-05-23 22:25:19 +02:00
MathieuG-P d1176ccabe Merge branch 'master' into bugfix/maps-loading-conflict-on-version-change/157 2023-05-23 21:51:19 +02:00
MathieuG-P c20e10ea0a Merge pull request #219 from Zagrios/hotfix/sometimes-eprem-error-when-extract-mod-zip
[Hotfix] Make ERPERM error not possible when extracting mod zip
2023-05-23 21:44:28 +02:00
MathieuG-P 3f4d58a3ed [hotfix] fix async mishandled on extract mod zip 2023-05-23 21:31:23 +02:00
MathieuG-P ba3de199fa [hotfix] download mod zip in buffer to avoid eprem error 2023-05-22 22:33:48 +02:00
MathieuG-P 1d8a68f665 bumb to 1.2.0-alpha.1 2023-05-22 21:14:38 +02:00
MathieuG-P 4abf04c12a [bugfix-157] create cancellable observable 2023-04-17 01:17:19 +02:00
23 changed files with 197 additions and 120 deletions
+8
View File
@@ -541,5 +541,13 @@
"ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans/32055887/9c3d476c03e4a2ac72ca5df016b7c94d242b41cb.png",
"ReleaseDate": "1683644991",
"year": "2023"
},
{
"BSVersion": "1.30.0",
"BSManifest": "7304715857122259127",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/3725085394628677636",
"ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans/32055887/cd56688ad716b544af14a61b2fbace9088478887.png",
"ReleaseDate": "1684965390",
"year": "2023"
}
]
+3 -1
View File
@@ -6,5 +6,7 @@
{"username": "Carbon", "type": "gold"},
{"username": "Naysy"},
{"username": "Falmil"},
{"username": "K1Lc4m"}
{"username": "K1Lc4m"},
{"username": "Adam"},
{"username": "Cabalia"}
]
+2 -1
View File
@@ -472,7 +472,8 @@
},
"password": {
"label": "Passwort",
"placeholder": "Gebe dein Passwort ein"
"placeholder": "Gebe dein Passwort ein",
"max-length-warning": "Das Passwort überschreitet 64 Zeichen! Wenn das Passwort ungültig ist, versuchen Sie nur die ersten 64 Zeichen einzugeben."
},
"stay": {
"label": "Verbunden bleiben"
+2 -1
View File
@@ -472,7 +472,8 @@
},
"password": {
"label": "Password",
"placeholder": "Enter your password"
"placeholder": "Enter your password",
"max-length-warning": "The password exceeds 64 characters! If the password is invalid, try to put its first 64 characters."
},
"stay": {
"label": "Stay connected"
+2 -1
View File
@@ -471,7 +471,8 @@
},
"password": {
"label": "Contraseña",
"placeholder": "Introduzca su contraseña"
"placeholder": "Introduzca su contraseña",
"max-length-warning": "¡La contraseña supera los 64 caracteres! Si la contraseña es inválida, intenta ingresar solo sus primeros 64 caracteres."
},
"stay": {
"label": "Manténgase conectado"
+2 -1
View File
@@ -471,7 +471,8 @@
},
"password": {
"label": "Mot de passe",
"placeholder": "Entre ton mot de passe"
"placeholder": "Entre ton mot de passe",
"max-length-warning": "Le mot de passe dépasse 64 caractères ! Si le mot de passe est invalide, tente d'entrer seulement ses 64 premiers caractères."
},
"stay": {
"label": "Rester connecté"
+7 -18
View File
@@ -24,6 +24,7 @@
"fs-extra": "^11.1.1",
"history": "^5.3.0",
"is-online": "^9.0.1",
"jszip": "^3.10.1",
"md5-file": "^5.0.0",
"node-fetch": "^2.6.7",
"node-stream-zip": "^1.15.0",
@@ -9872,8 +9873,7 @@
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"dev": true
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"node_modules/immutable": {
"version": "4.1.0",
@@ -12158,7 +12158,6 @@
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dev": true,
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
@@ -12170,7 +12169,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -12297,7 +12295,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"dependencies": {
"immediate": "~3.0.5"
}
@@ -13831,8 +13828,7 @@
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"node_modules/param-case": {
"version": "3.0.4",
@@ -16213,8 +16209,7 @@
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"dev": true
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"node_modules/setprototypeof": {
"version": "1.2.0",
@@ -26235,8 +26230,7 @@
"immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"dev": true
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ=="
},
"immutable": {
"version": "4.1.0",
@@ -28053,7 +28047,6 @@
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"dev": true,
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",
@@ -28065,7 +28058,6 @@
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dev": true,
"requires": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
@@ -28178,7 +28170,6 @@
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"dev": true,
"requires": {
"immediate": "~3.0.5"
}
@@ -29308,8 +29299,7 @@
"pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"dev": true
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="
},
"param-case": {
"version": "3.0.4",
@@ -31014,8 +31004,7 @@
"setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"dev": true
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA=="
},
"setprototypeof": {
"version": "1.2.0",
+1
View File
@@ -244,6 +244,7 @@
"fs-extra": "^11.1.1",
"history": "^5.3.0",
"is-online": "^9.0.1",
"jszip": "^3.10.1",
"md5-file": "^5.0.0",
"node-fetch": "^2.6.7",
"node-stream-zip": "^1.15.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bs-manager",
"version": "1.1.1",
"version": "1.2.0-alpha.3",
"description": "A foundation for scalable desktop apps",
"main": "./dist/main/main.js",
"author": {
+24
View File
@@ -0,0 +1,24 @@
import JSZip from 'jszip';
import { pathExist } from './fs.helpers';
import path from 'path';
import { mkdir, writeFile } from 'fs/promises';
export async function extractZip(zip: JSZip, dest: string): Promise<string[]>{
if(!await pathExist(dest)){ throw new Error(`Path ${dest} does not exist`); }
const files: string[] = [];
for(const [relativePath, entry] of Object.entries(zip.files)){
if(entry.dir){ continue; }
const content = await entry.async("nodebuffer");
const outPath = path.join(dest, relativePath);
const outDir = path.dirname(outPath);
await mkdir(outDir, { recursive: true });
await writeFile(outPath, content);
files.push(outPath);
}
return files;
}
@@ -90,6 +90,8 @@ export class BSInstallerService{
public async downloadBsVersion(downloadInfos: DownloadInfo): Promise<DownloadEvent>{
// TODO : Can be a lot improved by using ipcV2 and Observable
if(this.downloadProcess && this.downloadProcess.connected){ throw "AlreadyDownloading"; }
const {bsVersion} = downloadInfos;
if(!bsVersion){ return {type: "[Error]"}; }
+1 -1
View File
@@ -30,7 +30,7 @@ export class BSVersionLibService{
}
private getRemoteVersions(): Promise<BSVersion[]>{
return this.requestService.get<BSVersion[]>(this.REMOTE_BS_VERSIONS_URL);
return this.requestService.getJSON<BSVersion[]>(this.REMOTE_BS_VERSIONS_URL);
}
private async getLocalVersions(): Promise<BSVersion[]>{
@@ -38,7 +38,7 @@ export class BeatModsApiService {
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 => {
return this.requestService.getJSON<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)));
});
@@ -64,7 +64,7 @@ export class BeatModsApiService {
const alias = await this.getAliasOfVersion(version);
return this.requestService.get<Mod[]>(this.getVersionModsUrl(alias)).then(mods => {
return this.requestService.getJSON<Mod[]>(this.getVersionModsUrl(alias)).then(mods => {
mods = mods.map(mod => this.asignDependencies(mod, mods));
this.versionModsCache.set(version.BSVersion, mods);
return mods;
@@ -73,16 +73,10 @@ export class BeatModsApiService {
public async getAllMods(): Promise<Mod[]>{
if(!!this.allModsCache){ return this.allModsCache; }
return this.requestService.get<Mod[]>(this.getAllModsUrl()).then(mods => {
return this.requestService.getJSON<Mod[]>(this.getAllModsUrl()).then(mods => {
this.allModsCache = mods;
return this.allModsCache;
});
}
public loadAllMods(){ return this.getAllMods(); }
}
@@ -6,13 +6,15 @@ 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";
import { deleteFolder, ensureFolderExist, pathExist, unlinkPath } from "../../helpers/fs.helpers";
import { deleteFolder, pathExist, unlinkPath } from "../../helpers/fs.helpers";
import { lastValueFrom } from "rxjs";
import JSZip from "jszip"
import { extractZip } from "../../helpers/zip.helpers";
import { ensureFolderExist } from "../../helpers/fs.helpers";
export class BsModsManagerService {
@@ -46,7 +48,7 @@ export class BsModsManagerService {
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; }
if(mod.name.toLowerCase() === "bsipa"){ return false; }
return mod.downloads.some(download => download.hashMd5.some(md5 => md5.hash === hash));
})
}
@@ -96,17 +98,20 @@ export class BsModsManagerService {
return this.getIpaFromHash(injectorMd5);
}
private async downloadZip(zipUrl: string): Promise<{zip: StreamZip.StreamZipAsync, zipPath: string}>{
private async downloadZip(zipUrl: string): Promise<JSZip>{
zipUrl = path.join(this.beatModsApi.BEAT_MODS_URL, zipUrl);
const fileName = path.basename(zipUrl);
const tempPath = this.utilsService.getTempPath();
await ensureFolderExist(this.utilsService.getTempPath());
const dest = path.join(tempPath, fileName);
const zipPath = (await lastValueFrom(this.requestService.downloadFile(zipUrl, dest))).data;
const zip = new StreamZip.async({file : zipPath});
const buffer = await lastValueFrom(this.requestService.downloadBuffer(zipUrl)).then(progress => progress.data).catch(e => {
log.error("ZIP", "Error while downloading zip", e);
return undefined
});
return {zip, zipPath};
if(!buffer){ return null; }
return JSZip.loadAsync(buffer).catch(e => {
log.error("ZIP", "Error while loading zip", e);
return null
});
}
private async executeBSIPA(version: BSVersion, args: string[]): Promise<boolean>{
@@ -135,23 +140,22 @@ export class BsModsManagerService {
}
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);
const zip = await this.downloadZip(download.url);
if(!zip){ return false; }
const crypto = require('crypto');
const entries = await zip.entries();
const files = await zip.files;
const checkedEntries = (await Promise.all(Object.values(entries).map(async (entry) => {
if(!entry.isFile){ return undefined; }
const data = await zip.entryData(entry);
const checkedEntries = (await Promise.all(Object.values(files).map(async (entry) => {
const data = await entry.async("nodebuffer");
const entryMd5 = crypto.createHash('md5').update(data).digest('hex')
return download.hashMd5.some(md5 => md5.hash === entryMd5) ? entry : undefined;
}))).filter(entry => !!entry);
@@ -162,12 +166,16 @@ export class BsModsManagerService {
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 ensureFolderExist(destDir);
const extracted = await extractZip(zip, destDir).then(() => true).catch(e => {
log.error("EXTRACT MOD ZIP", e);
return false;
})
await zip.close();
await unlinkPath(zipPath);
const res = isBSIPA ? (extracted && (await this.executeBSIPA(version, ["-n"]))) : extracted;
const res = isBSIPA ? (extracted && (await this.executeBSIPA(version, ["-n"]).catch(e => {
log.error(e);
return false;
}))) : extracted;
res && this.nbInstalledMods++;
@@ -237,7 +245,7 @@ export class BsModsManagerService {
public async getInstalledMods(version: BSVersion): Promise<Mod[]>{
this.manifestMatches = [];
await this.beatModsApi.loadAllMods();
await this.beatModsApi.getAllMods();
const bsipa = await this.getBsipaInstalled(version);
return Promise.all([
this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING),
@@ -245,11 +253,16 @@ export class BsModsManagerService {
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;
const modsDict = new Map<string, Mod>();
if(!!bsipa){ modsDict.set(bsipa.name, bsipa); }
for(const mod of dirMods.flat()){
if(modsDict.has(mod.name)){ continue; }
modsDict.set(mod.name, mod);
}
return Array.from(modsDict.values());
});
}
+33 -17
View File
@@ -1,8 +1,9 @@
import { RequestOptions, get } from "https";
import { get } from "https";
import { createWriteStream, unlink } from "fs";
import { Progression, unlinkPath } from "main/helpers/fs.helpers";
import { Observable, buffer, shareReplay, tap } from "rxjs";
import { Progression } from "main/helpers/fs.helpers";
import { Observable, shareReplay, tap } from "rxjs";
import log from "electron-log";
import fetch, { RequestInfo, RequestInit } from "node-fetch";
export class RequestService {
@@ -15,18 +16,20 @@ export class RequestService {
private constructor(){}
public get<T = any>(options: string|RequestOptions): Promise<T>{
public async getJSON<T = unknown>(url: RequestInfo, options?: RequestInit): Promise<T>{
try {
const response = await fetch(url, options);
return new Promise((resolve, reject) => {
let body = ''
get(options, (res) => {
res.on('data', chunk => body += chunk);
res.on('end', () => {
resolve(JSON.parse(body));
});
res.on('error', (err) => reject(err))
}).on("error", err => reject(err));
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status} ${response}`);
}
return await response.json();
}
catch (err) {
log.error(err);
throw err;
}
}
public downloadFile(url: string, dest: string): Observable<Progression<string>>{
@@ -61,17 +64,30 @@ export class RequestService {
}
public downloadBuffer(url: string): Observable<Buffer>{
public downloadBuffer(url: string): Observable<Progression<Buffer>>{
return new Observable<Progression<Buffer>>(subscriber => {
const progress: Progression<Buffer> = {
current: 0,
total: 0,
data: null
};
return new Observable<Buffer>(subscriber => {
const allChunks: Buffer[] = [];
const req = get(url, res => {
progress.total = parseInt(res.headers?.["content-length"] || "0", 10);
res.on("data", chunk => {
allChunks.push(chunk);
progress.current += chunk.length;
subscriber.next(progress);
});
res.on('end', () => {
subscriber.next(Buffer.concat(allChunks));
progress.data = Buffer.concat(allChunks);
subscriber.next(progress);
subscriber.complete();
});
res.on('error', (err) => subscriber.error(err))
+10 -16
View File
@@ -1,10 +1,10 @@
import { writeFileSync } from "fs";
import { readJSON } from "fs-extra";
import { get } from "https";
import isOnline from "is-online";
import path from "path";
import { Supporter } from "shared/models/supporters/supporter.interface";
import { UtilsService } from "./utils.service";
import { RequestService } from "./request.service";
import { allSettled } from "../../shared/helpers/promise.helpers";
export class SupportersService {
@@ -16,6 +16,8 @@ export class SupportersService {
private cache: Supporter[];
private readonly utilsService: UtilsService;
private readonly requestService: RequestService;
public static getInstance(): SupportersService{
if(!SupportersService.instance){ SupportersService.instance = new SupportersService(); }
@@ -24,6 +26,7 @@ export class SupportersService {
private constructor(){
this.utilsService = UtilsService.getInstance();
this.requestService = RequestService.getInstance();
}
private async updateLocalSupporters(supporters: Supporter[]): Promise<void>{
@@ -31,15 +34,8 @@ export class SupportersService {
writeFileSync(patreonsPath, JSON.stringify(supporters, null, "\t"), {encoding: 'utf-8', flag: 'w'});
}
private async getRemoteSupporters(): Promise<Supporter[]>{
return new Promise<Supporter[]>((resolve, reject) => {
let body = ''
get(this.PATREONS_URL, (res) => {
res.on('data', chunk => body += chunk);
res.on('end', () => resolve(JSON.parse(body)));
res.on('error', () => reject(null))
})
})
private getRemoteSupporters(): Promise<Supporter[]>{
return this.requestService.getJSON(this.PATREONS_URL);
}
private async getLocalSupporters(): Promise<Supporter[]>{
@@ -50,11 +46,9 @@ export class SupportersService {
private async loadSupporters(): Promise<Supporter[]>{
if(!!this.cache && this.cache.length){ return this.cache; }
const isOnlineRes = await isOnline({timeout: 1500});
const [localVersions, remoteVersions] = await Promise.all([
this.getLocalSupporters(), (isOnlineRes && this.getRemoteSupporters())
]);
const [localVersions, remoteVersions] = await allSettled([
this.getLocalSupporters(), this.getRemoteSupporters()
], {keepStructure: true});
const supporters = remoteVersions?.length ? remoteVersions : localVersions;
@@ -9,7 +9,7 @@ import { MapsDownloaderService } from "renderer/services/maps-downloader.service
import { VariableSizeList } from "react-window"
import { MapsRow } from "./maps-row.component"
import { BehaviorSubject } from "rxjs"
import { debounceTime, last, map, mergeMap } from "rxjs/operators"
import { debounceTime, last, mergeMap, tap } from "rxjs/operators"
import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service"
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"
import { useTranslation } from "renderer/hooks/use-translation.hook"
@@ -115,11 +115,8 @@ export const LocalMapsListPanel = forwardRef(({version, className, filter, searc
const loadMapsObs$ = mapsManager.getMaps(version);
loadMapsObs$.pipe(map(progess => {
return Math.floor(((progess.loaded / progess.total) * 100));
})).subscribe(percent => loadPercent$.next(percent));
subs.push(loadMapsObs$.pipe(
tap(progress => loadPercent$.next(Math.floor(((progress.loaded / progress.total) * 100)))),
last(),
mergeMap(async progress => {
if(os.isOffline){ return progress.maps; }
@@ -25,7 +25,7 @@ export const LoginModal: ModalComponent<{username: string, password: string, sta
}
return (
<form onSubmit={(e) => {e.preventDefault(); loggin();}}>
<form className="max-w-xs" 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} />
@@ -33,9 +33,12 @@ 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="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")}/>
</div>
<div className="mb-2">
<div className="mb-2 flex flex-col w-full">
<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")}/>
{password?.length > 64 && (
<span className="text-orange-700 dark:text-orange-400 text-xs whitespace-normal max-w-full w-full overflow-hidden">{t("modals.steam-login.inputs.password.max-length-warning")}</span>
)}
</div>
<span onClick={whyCredentials} className="underline my-4 block cursor-pointer">{t("modals.steam-login.why-credentials")}</span>
@@ -182,6 +182,7 @@ export const DownloadModelsModal: ModalComponent<void, {version: BSVersion, type
callbackValue={model}
isDownloading={equal(currentDownload, {model, version} as ModelDownload)}
onDownload={!isModelOwned(model) ? handleDownloadModel: undefined}
onDoubleClick={(!isModelOwned(model)) ? handleDownloadModel: undefined}
onCancelDownload={modelPendingDownload(model) ? handleCancelDownload : undefined}
/>
))}
@@ -1,5 +1,5 @@
import equal from "fast-deep-equal";
import { memo, useState } from "react";
import { memo, useRef, useState } from "react";
import { MSModel, MSModelType } from "shared/models/models/model-saber.model";
import { BsmImage } from "../shared/bsm-image.component";
import { motion } from "framer-motion";
@@ -11,11 +11,12 @@ import Tippy from "@tippyjs/react";
import { followCursor } from "tippy.js"
import defaultImage from '../../../../assets/images/default-version-img.jpg'
import { BsmLocalModel } from "shared/models/models/bsm-local-model.interface";
import { BsmIconType } from "../svgs/bsm-icon.component";
import { BsmIcon, BsmIconType } from "../svgs/bsm-icon.component";
import { BsmButton } from "../shared/bsm-button.component";
import { BsmBasicSpinner } from "../shared/bsm-basic-spinner/bsm-basic-spinner.component";
import { isValidUrl } from "shared/helpers/url.helpers";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import useDoubleClick from "use-double-click";
type Props<T> = {
selected?: boolean,
@@ -26,7 +27,8 @@ type Props<T> = {
onDelete?: (value: T) => void,
onDownload?: (value: T) => void,
onCancelDownload?: (value: T) => void
} & Partial<MSModel> & Partial<BsmLocalModel> & Omit<React.ComponentProps<"li">, "id">
onDoubleClick?: (value: T) => void
} & Partial<MSModel> & Partial<BsmLocalModel> & Omit<React.ComponentProps<"li">, "id" | "onDoubleClick">
function modelItem<T = unknown>(props: Props<T>) {
@@ -34,6 +36,14 @@ function modelItem<T = unknown>(props: Props<T>) {
const color = useThemeColor("first-color");
const [hovered, setHovered] = useState(false);
const [idContentCopied, setIdContentCopied] = useState<string>(null);
const ref = useRef();
useDoubleClick({
ref,
latency: props.onDoubleClick ? 200 : 0,
onSingleClick: (e) => props?.onClick?.(e as unknown as React.MouseEvent<HTMLLIElement>),
onDoubleClick: () => props?.onDoubleClick?.(props.callbackValue)
});
const modelPageUrl = (() => {
if(!props.id){ return null; }
@@ -88,21 +98,27 @@ function modelItem<T = unknown>(props: Props<T>) {
}
return (
<motion.li className={`relative flex-grow min-w-[14rem] h-56 cursor-pointer ${props.className ?? ""}`} onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)} onClick={props.onClick}>
<motion.li className={`relative flex-grow min-w-[16rem] h-64 cursor-pointer ${props.className ?? ""}`} onHoverStart={() => setHovered(() => true)} onHoverEnd={() => setHovered(() => false)}>
<GlowEffect visible={props.selected || hovered}/>
<div className="absolute top-0 left-0 w-full h-full rounded-lg overflow-hidden blur-none bg-black shadow-sm shadow-black">
<BsmImage className={`absolute top-0 left-0 w-full h-full object-cover ${props.type === MSModelType.Avatar ? "object-top" : ""}`} image={thumbnailUrl} placeholder={defaultImage} loading="lazy"/>
<div className="absolute top-0 right-0 h-full w-0 flex flex-col items-end gap-1 pt-1.5 pr-1.5">
{!props.isDownloading ? (
actionButtons().map((button, index) => (
<BsmButton key={index} className="w-7 h-7 p-1 rounded-md transition-transform duration-150 shadow-black shadow-sm" style={{transitionDelay: `${index * 50}ms`, transform: hovered ? "translate(0%)" : "translate(150%)"}} icon={button.icon} iconColor={button.iconColor} onClick={e => {e.stopPropagation(); e.preventDefault(); button.action();}} withBar={false}/>
))
): (
<BsmBasicSpinner className="w-7 h-7 p-1 rounded-md bg-main-color-2 flex items-center justify-center shadow-black shadow-sm" spinnerClassName="brightness-200" thikness="3.5px" style={{color}}/>
)}
<div ref={ref} className="contents">
<BsmImage className={`absolute top-0 left-0 w-full h-full object-cover brightness-50 scale-[200%] blur-md`} image={thumbnailUrl} placeholder={defaultImage} loading="lazy"/>
<BsmImage className={`absolute top-0 left-1/2 -translate-x-1/2 max-w-[20rem] w-full h-full object-cover`} image={thumbnailUrl} placeholder={defaultImage} loading="lazy"/>
<div className="absolute top-0 right-0 h-full w-0 flex flex-col items-end gap-1 pt-1.5 pr-1.5">
{!props.isDownloading ? (
actionButtons().map((button, index) => (
<BsmButton key={index} className="w-8 h-8 p-1 rounded-md transition-transform duration-150 shadow-black shadow-sm" style={{transitionDelay: `${index * 50}ms`, transform: hovered ? "translate(0%)" : "translate(150%)"}} icon={button.icon} iconColor={button.iconColor} onClick={e => {e.stopPropagation(); e.preventDefault(); button.action();}} withBar={false}/>
))
): (
<BsmBasicSpinner className="w-7 h-7 p-1 rounded-md bg-main-color-2 flex items-center justify-center shadow-black shadow-sm" spinnerClassName="brightness-200" thikness="3.5px" style={{color}}/>
)}
</div>
</div>
<motion.div className="absolute cursor-default top-[80%] left-0 w-full h-full px-2 pt-2.5 flex flex-col gap-1.5 bg-main-color-3 bg-opacity-60 backdrop-blur-md transition-all delay-150 hover:top-0" onClick={e =>{e.stopPropagation(); e.preventDefault()}}>
<BsmLink className={`block w-fit max-w-full overflow-hidden font-bold whitespace-nowrap text-ellipsis ${props.id ? "cursor-pointer hover:underline" : ""}`} href={modelPageUrl}>{props.name}</BsmLink>
<motion.div className="absolute top-[83%] left-0 w-full h-full px-2 flex flex-col gap-1.5 bg-main-color-3 bg-opacity-60 backdrop-blur-md transition-all delay-150 hover:top-0 group" onClick={e =>{e.stopPropagation(); e.preventDefault()}}>
<div className="w-full flex justify-center items-center mt-1.5">
<BsmLink className={`block grow overflow-hidden font-bold whitespace-nowrap text-ellipsis ${props.id ? "cursor-pointer hover:underline" : ""}`} href={modelPageUrl}>{props.name}</BsmLink>
<BsmIcon className="shrink-0 h-8 group-hover:rotate-180 transition-transform w-fit" icon="chevron-top"/>
</div>
<BsmLink className={`block w-fit max-w-full overflow-hidden whitespace-nowrap text-ellipsis brightness-200 ${authorPageUrl ? "cursor-pointer hover:underline" : ""}`} style={{color}} href={authorPageUrl}>{props.author ?? ''}</BsmLink>
<ul className="flex flex-row flex-wrap gap-1">
{modelTags?.map(tag => (
@@ -57,10 +57,11 @@ import { ModelTypeAvatarIcon } from "./icons/model-type-avatar-icon.component";
import { ModelTypeSaberIcon } from "./icons/model-type-saber-icon.component";
import { ModelTypePlatformIcon } from "./icons/model-type-platform-icon.component";
import { ModelTypeBloqIcon } from "./icons/model-type-bloq-icon.component";
import { ChevronTopIcon } from "./icons/chevron-top-icon.component";
export type BsmIconType = BsvMapCharacteristic | MSModelType | (
"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"|
"terminal"|"desktop"|"oculus"|"add"|"cross"|"task"|"github"|"close"|"thumbUpFill"|"timerFill"|"pause"|"twitter"|"sync"|"chevron-top"|
"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"|"de-DE-flag"
);
@@ -122,6 +123,8 @@ export const BsmIcon = memo(({className, icon, style}: {className?: string, icon
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}/> }
if(icon === "chevron-top"){ return <ChevronTopIcon className={className} style={style}/> }
if(icon === MSModelType.Avatar){ return <ModelTypeAvatarIcon className={className} style={style}/> }
if(icon === MSModelType.Saber){ return <ModelTypeSaberIcon className={className} style={style}/> }
if(icon === MSModelType.Platfrom){ return <ModelTypePlatformIcon className={className} style={style}/> }
@@ -0,0 +1,9 @@
import { CSSProperties } from "react";
export function ChevronTopIcon(props: {className?: string, style?: CSSProperties}) {
return (
<svg className={props.className} style={props.style} fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960">
<path d="M260.811-366.051q-11.021-11.021-11.355-26.521-.333-15.5 10.689-26.522l193.333-193.333q5.892-5.892 12.475-8.457 6.583-2.565 14.047-2.565t14.047 2.565q6.583 2.565 12.475 8.457l193.667 193q11.021 10.514 10.981 26.054-.039 15.54-11.315 26.656-11.021 11.021-26.855 11.021-15.833 0-26.855-11.021L480-532.196 313.521-365.051q-10.355 11.022-25.894 10.649-15.54-.373-26.816-11.649Z"/>
</svg>
)
}
@@ -8,6 +8,7 @@ import { DownloadModelsModal } from "renderer/components/modal/modal-types/model
import { ProgressBarService } from "../progress-bar.service";
import { Progression } from "main/helpers/fs.helpers";
import { ProgressionInterface } from "shared/models/progress-bar";
import equal from "fast-deep-equal";
export class ModelsDownloaderService {
@@ -59,11 +60,11 @@ export class ModelsDownloaderService {
}
public addModelToDownload(model: ModelDownload): ModelDownload{
public addModelToDownload(model: ModelDownload): void{
if(this.queue$.value.some(m => equal(m, model))){ return null; }
const queue = this.queue$.value;
queue.push(model);
this.queue$.next([...queue]);
return model;
}
public removeFromDownloadQueue(download: ModelDownload){