mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 769dab2896 | |||
| 8f1f998c5a | |||
| 48d03cccbb | |||
| 7d54ae79f7 | |||
| c46fdd56f6 | |||
| 22dc1ca511 | |||
| 709105b9f4 | |||
| 1c0cfb6b89 | |||
| 7ebc1bff16 | |||
| 0e1cda5050 | |||
| 331626bd1e | |||
| ea62f2443f | |||
| 532ab08e46 | |||
| a6fa55719d | |||
| 163857a14f | |||
| 854e0e2761 | |||
| eba6f916a3 | |||
| 9790f35749 | |||
| 93d3cbc8bf |
@@ -4,5 +4,11 @@
|
||||
"title" : "What's new ?",
|
||||
"timestamp" : 1721048400,
|
||||
"version" : "1.5.0-alpha.1"
|
||||
},
|
||||
"1.5.0-alpha.2":{
|
||||
"htmlBody" :"<strong>BSManager continues to get better, thanks to your continued support and contributions!</strong><center>🎈🎉🎈</center><p>Note: This is an alpha release, encountering bugs is expected. Please file new issues for any bugs you find.</p><h4>Alpha changes</h4><ul><li>Fixed an issue where BSIPA could not be installed if the BS version's path contained spaces #523</li><li>Fixed a crash when changing audio volume #518</li><li>Fixed an issue where playlist maps with a malformed hash were not loaded #517</li><li>Improved reliability of reading maps from a playlist file #517</li><li>Playlists in sub-folders are now loaded #514</li><li>Fixed a crash when loading an invalid playlist file #513</li></ul><h2>Features</h2><ul> <li>Added playlist support</li> <li>Added Linux support (Thanks to @Insprill)</li> <li>Added reset button to reset values when editing a Beat Saber version (Thanks to @Liborsaf)</li> <li>Added a changelog modal</li></ul><h3>Fixes</h3><ul><li>Fixed an issue where switching between Beat Saber versions too quickly displayed mods for the wrong version #472</li><li>Fixed an issue where the launch window from a shortcut would never close #485</li><li>Advanced launch arguments are used even when the input field is closed #496</li><li>Fixed an issue where having many maps caused some maps to not have their information loaded #503</li><li>Fixed an issue where downloading maps sometimes resulted in a timeout error #524</li><li>Fixed an issue where the duration of maps could be wrong in some cases #522</li></ul><h2>Other changes</h2><ul> <li>.NET Framework is no longer required</li> <li>Clear English translations for the Steam credentials popup (Thanks to @Aeywoo)</li> <li>NSFW models are now blurred when browsing for models</li> <li>When no mods are available for a Beat Saber version, the version number is now shown</li> <li>Mods load faster</li> <li>After a first full maps loading, maps load almost instantly</li> <li>Improved performance in lists with lot of contents</li></ul>",
|
||||
"title" : "What's new ?",
|
||||
"timestamp" : 1721665382,
|
||||
"version" : "1.5.0-alpha.2"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+8
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.0-alpha.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.0",
|
||||
"version": "1.5.0-alpha.1",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -27,6 +27,7 @@
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.2.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"format-duration": "^3.0.2",
|
||||
"framer-motion": "^11.2.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"got": "^14.4.1",
|
||||
@@ -12763,6 +12764,11 @@
|
||||
"node": ">= 18"
|
||||
}
|
||||
},
|
||||
"node_modules/format-duration": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/format-duration/-/format-duration-3.0.2.tgz",
|
||||
"integrity": "sha512-pKzJDSRgK2lqAiPW3uizDaIJaJnataZclsahz25UMwfdryBGDa+1HlbXGjzpMvX/2kMh4O0sNevFXKaEfCjHsA=="
|
||||
},
|
||||
"node_modules/formdata-polyfill": {
|
||||
"version": "4.0.10",
|
||||
"resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
|
||||
|
||||
+2
-1
@@ -2,7 +2,7 @@
|
||||
"name": "bs-manager",
|
||||
"description": "Manage maps, mods and more for Beat Saber",
|
||||
"main": "./src/main/main.ts",
|
||||
"version": "1.5.0-alpha.1",
|
||||
"version": "1.5.0-alpha.2",
|
||||
"scripts": {
|
||||
"build-rust-scripts": "node -r esbuild-register ./.erb/scripts/build-rust-scripts.js",
|
||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
|
||||
@@ -248,6 +248,7 @@
|
||||
"electron-store": "^8.1.0",
|
||||
"electron-updater": "^6.2.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"format-duration": "^3.0.2",
|
||||
"framer-motion": "^11.2.6",
|
||||
"fs-extra": "^11.2.0",
|
||||
"got": "^14.4.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "bs-manager",
|
||||
"version": "1.5.0-alpha.1",
|
||||
"version": "1.5.0-alpha.2",
|
||||
"description": "BSManager",
|
||||
"main": "./dist/main/main.js",
|
||||
"author": {
|
||||
|
||||
@@ -7,10 +7,10 @@ import { RequestService } from "../request.service";
|
||||
import { LocalMapsManagerService } from "./maps/local-maps-manager.service";
|
||||
import log from "electron-log";
|
||||
import { WindowManagerService } from "../window-manager.service";
|
||||
import { BPList, DownloadPlaylistProgressionData } from "shared/models/playlists/playlist.interface";
|
||||
import { readFileSync } from "fs";
|
||||
import { BPList, DownloadPlaylistProgressionData, PlaylistSong } from "shared/models/playlists/playlist.interface";
|
||||
import { readFileSync, Stats } from "fs";
|
||||
import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service";
|
||||
import { copy, ensureDir, pathExists, pathExistsSync, readdirSync, realpath, writeFileSync } from "fs-extra";
|
||||
import { copy, ensureDir, pathExists, pathExistsSync, realpath, writeFileSync } from "fs-extra";
|
||||
import { Progression, getUniqueFileNamePath, unlinkPath } from "../../helpers/fs.helpers";
|
||||
import { FileAssociationService } from "../file-association.service";
|
||||
import { SongDetailsCacheService } from "./maps/song-details-cache.service";
|
||||
@@ -23,6 +23,10 @@ import { isValidUrl } from "shared/helpers/url.helpers";
|
||||
import { Archive } from "main/models/archive.class";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
import recursiveReadDir from "recursive-readdir";
|
||||
import { BsvMapDetail, SongDetails } from "shared/models/maps";
|
||||
import { findHashInString } from "shared/helpers/string.helpers";
|
||||
|
||||
export class LocalPlaylistsManagerService {
|
||||
private static instance: LocalPlaylistsManagerService;
|
||||
@@ -136,6 +140,14 @@ export class LocalPlaylistsManagerService {
|
||||
|
||||
const bpList: BPList = isLocalFile ? JSON.parse(readFileSync(source).toString()) : await this.request.getJSON<BPList>(source);
|
||||
|
||||
if(!bpList?.playlistTitle) {
|
||||
throw new Error(`Invalid playlist file ${source}`);
|
||||
}
|
||||
|
||||
bpList.songs = (bpList.songs ?? []).map(s => s.hash ? (
|
||||
{ ...s, hash: findHashInString(s.hash) ?? s.hash }
|
||||
) : s).filter(Boolean);
|
||||
|
||||
return bpList;
|
||||
}
|
||||
|
||||
@@ -155,13 +167,22 @@ export class LocalPlaylistsManagerService {
|
||||
throw new Error(`Playlists folder not found ${folerPath}`);
|
||||
}
|
||||
|
||||
const playlists = readdirSync(folerPath).filter(file => path.extname(file) === ".bplist");
|
||||
progress.total = playlists.length;
|
||||
const ignoreFunc = (file: string, stats: Stats): boolean => {
|
||||
if(stats.isFile() && path.extname(file) !== ".bplist") { return true; }
|
||||
return false;
|
||||
}
|
||||
|
||||
for (const playlist of playlists) {
|
||||
const playlistPath = path.join(folerPath, playlist);
|
||||
const bpList = await this.readPlaylistFromSource(playlistPath);
|
||||
const playlistPaths = (await recursiveReadDir(folerPath, [ignoreFunc]));
|
||||
|
||||
progress.total = playlistPaths.length;
|
||||
|
||||
for (const playlistPath of playlistPaths) {
|
||||
const {result: bpList, error} = await tryit(() => this.readPlaylistFromSource(playlistPath));
|
||||
|
||||
if(error) {
|
||||
log.error(error);
|
||||
continue;
|
||||
}
|
||||
|
||||
const localBpList: LocalBPList = { ...bpList, path: playlistPath };
|
||||
bpLists.push(localBpList);
|
||||
@@ -175,6 +196,25 @@ export class LocalPlaylistsManagerService {
|
||||
});
|
||||
}
|
||||
|
||||
private getSongDetailsFromPlaylistSong(song: PlaylistSong): SongDetails | undefined {
|
||||
let songDetails: SongDetails;
|
||||
|
||||
const songHash = findHashInString(song.hash);
|
||||
if(songHash){
|
||||
songDetails = this.songDetails.getSongDetails(song.hash);
|
||||
}
|
||||
|
||||
if(song.key && !songDetails){
|
||||
songDetails = this.songDetails.getSongDetailsById(song.key);
|
||||
}
|
||||
|
||||
const levelIdHash = findHashInString(song.levelid);
|
||||
if(levelIdHash && !songDetails){
|
||||
songDetails = this.songDetails.getSongDetails(levelIdHash);
|
||||
}
|
||||
return songDetails;
|
||||
}
|
||||
|
||||
public getLocalBPListDetails(localBPList: LocalBPList): LocalBPListsDetails {
|
||||
|
||||
const tryExtractPlaylistId = (url: string) => {
|
||||
@@ -185,27 +225,28 @@ export class LocalPlaylistsManagerService {
|
||||
|
||||
const bpListDetails: LocalBPListsDetails = {
|
||||
...localBPList,
|
||||
duration: 0,
|
||||
nbMaps: localBPList.songs?.length ?? 0,
|
||||
id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined
|
||||
}
|
||||
|
||||
const songsDetails = localBPList.songs?.map(s => {
|
||||
if(s.hash){
|
||||
return this.songDetails.getSongDetails(s.hash);
|
||||
}
|
||||
if(s.key){
|
||||
return this.songDetails.getSongDetailsById(s.key);
|
||||
}
|
||||
return undefined;
|
||||
}).filter(Boolean);
|
||||
const mappers = new Set<number>();
|
||||
|
||||
if(songsDetails?.length){
|
||||
bpListDetails.duration = songsDetails.reduce((acc, song) => acc + song.duration, 0);
|
||||
bpListDetails.nbMappers = new Set(songsDetails.map(s => s.uploader.id)).size;
|
||||
bpListDetails.minNps = Math.min(...songsDetails.map(s => Math.min(...s.difficulties.map(d => d.nps || 0))));
|
||||
bpListDetails.maxNps = Math.max(...songsDetails.map(s => Math.max(...s.difficulties.map(d => d.nps || 0))));
|
||||
for(const song of localBPList.songs){
|
||||
const songDetails = this.getSongDetailsFromPlaylistSong(song);
|
||||
|
||||
if(!songDetails) { continue; }
|
||||
|
||||
bpListDetails.duration += songDetails?.duration ? +songDetails.duration : 0;
|
||||
mappers.add(songDetails.uploader?.id);
|
||||
bpListDetails.minNps = Math.min(bpListDetails?.minNps ?? 0, Math.min(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0]));
|
||||
bpListDetails.maxNps = Math.max(bpListDetails?.maxNps ?? 0, Math.max(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0]));
|
||||
|
||||
song.songDetails = songDetails;
|
||||
}
|
||||
|
||||
bpListDetails.nbMappers = mappers.size;
|
||||
|
||||
return bpListDetails;
|
||||
}
|
||||
|
||||
@@ -261,7 +302,28 @@ export class LocalPlaylistsManagerService {
|
||||
continue;
|
||||
}
|
||||
|
||||
const [ mapDetail ] = await this.bsaver.getMapDetailsFromHashs([song.hash]);
|
||||
const mapDetail = await (async () => {
|
||||
let mapDetail: BsvMapDetail;
|
||||
|
||||
const mapHash = findHashInString(song?.hash);
|
||||
if(mapHash) {
|
||||
mapDetail = (await this.bsaver.getMapDetailsFromHashs([findHashInString(mapHash)])).at(0);
|
||||
}
|
||||
|
||||
if(song.key && !mapDetail) {
|
||||
mapDetail = await this.bsaver.getMapDetailsById(song.key);
|
||||
}
|
||||
|
||||
const levelIdHash = findHashInString(song?.levelid);
|
||||
if(levelIdHash && !mapDetail) {
|
||||
mapDetail = (await this.bsaver.getMapDetailsFromHashs([levelIdHash])).at(0);
|
||||
}
|
||||
|
||||
return mapDetail;
|
||||
})().catch(e => {
|
||||
log.error(e);
|
||||
return undefined as BsvMapDetail;
|
||||
});
|
||||
|
||||
if(!mapDetail) {
|
||||
continue;
|
||||
|
||||
@@ -16,6 +16,7 @@ import { extractZip } from "../../helpers/zip.helpers";
|
||||
import recursiveReadDir from "recursive-readdir";
|
||||
import { sToMs } from "../../../shared/helpers/time.helpers";
|
||||
import { ensureDir } from "fs-extra";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
@@ -147,7 +148,7 @@ export class BsModsManagerService {
|
||||
return new Promise<boolean>(resolve => {
|
||||
const cmd = process.platform === 'linux'
|
||||
? `screen -dmS "BSIPA" dotnet ${ipaPath} ${args.join(" ")}` // Must run through screen, otherwise BSIPA tries to move console cursor and crashes.
|
||||
: `${ipaPath} ${args.join(" ")}`;
|
||||
: `"${ipaPath}" ${args.join(" ")}`;
|
||||
|
||||
log.info("START IPA PROCESS", cmd);
|
||||
const processIPA = spawn(cmd, { cwd: versionPath, detached: true, shell: true });
|
||||
@@ -157,6 +158,10 @@ export class BsModsManagerService {
|
||||
resolve(false)
|
||||
}, sToMs(30));
|
||||
|
||||
processIPA.stderr.on("data", data => {
|
||||
log.error("IPA process stderr", data.toString());
|
||||
})
|
||||
|
||||
processIPA.once("exit", code => {
|
||||
clearTimeout(timemout);
|
||||
if (code === 0) {
|
||||
@@ -167,7 +172,6 @@ export class BsModsManagerService {
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
@@ -339,7 +343,7 @@ export class BsModsManagerService {
|
||||
|
||||
public async installMods(mods: Mod[], version: BSVersion): Promise<InstallModsResult> {
|
||||
if (!mods?.length) {
|
||||
throw "no-mods";
|
||||
throw CustomError.fromError(new Error("No mods to install"), "no-mods");
|
||||
}
|
||||
|
||||
const deps = await this.resolveDependencies(mods, version);
|
||||
@@ -359,7 +363,7 @@ export class BsModsManagerService {
|
||||
return false;
|
||||
});
|
||||
if (!installed) {
|
||||
throw "cannot-install-bsipa";
|
||||
throw CustomError.fromError(new Error("Unable to install BSIPA"), "cannot-install-bsipa");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -375,7 +379,7 @@ export class BsModsManagerService {
|
||||
|
||||
public async uninstallMods(mods: Mod[], version: BSVersion): Promise<UninstallModsResult> {
|
||||
if (!mods?.length) {
|
||||
throw "no-mods";
|
||||
throw CustomError.fromError(new Error("No mods to uninstall"), "no-mods");
|
||||
}
|
||||
|
||||
this.nbModsToUninstall = mods.length;
|
||||
@@ -395,7 +399,7 @@ export class BsModsManagerService {
|
||||
const mods = await this.getInstalledMods(version);
|
||||
|
||||
if (!mods?.length) {
|
||||
throw "no-mods";
|
||||
throw CustomError.fromError(new Error("This version has to mods to uninstall"), "no-mods");
|
||||
}
|
||||
|
||||
this.nbModsToUninstall = mods.length;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Agent, RequestOptions, get } from "https";
|
||||
import { createWriteStream, unlink } from "fs";
|
||||
import { Agent, RequestOptions } from "https";
|
||||
import { createWriteStream } from "fs";
|
||||
import { Progression } from "main/helpers/fs.helpers";
|
||||
import { Observable, shareReplay, tap } from "rxjs";
|
||||
import log from "electron-log";
|
||||
@@ -8,6 +8,8 @@ import got, { Options } from "got";
|
||||
import { IncomingMessage } from "http";
|
||||
import { app } from "electron";
|
||||
import os from "os";
|
||||
import { unlinkSync } from "fs-extra";
|
||||
import { tryit } from "shared/helpers/error.helpers";
|
||||
|
||||
export class RequestService {
|
||||
private static instance: RequestService;
|
||||
@@ -60,31 +62,33 @@ export class RequestService {
|
||||
|
||||
public downloadFile(url: string, dest: string): Observable<Progression<string>> {
|
||||
return new Observable<Progression<string>>(subscriber => {
|
||||
const progress: Progression<string> = { current: 0, total: 0 };
|
||||
const progress: Progression<string> = { current: 0, total: 0, data: dest };
|
||||
|
||||
const stream = got.stream(url)
|
||||
const file = createWriteStream(dest);
|
||||
|
||||
file.on("close", () => {
|
||||
progress.data = dest;
|
||||
stream.on("downloadProgress", ({ transferred, total }) => {
|
||||
progress.current = transferred;
|
||||
progress.total = total;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
|
||||
stream.on("error", err => {
|
||||
tryit(() => unlinkSync(dest));
|
||||
subscriber.error(err);
|
||||
});
|
||||
|
||||
stream.on("end", () => {
|
||||
subscriber.next(progress);
|
||||
subscriber.complete();
|
||||
});
|
||||
file.on("error", err => unlink(dest, () => subscriber.error(err)));
|
||||
|
||||
const req = get(url, this.requestOptionsFromDefaultInit(), res => {
|
||||
progress.total = parseInt(res.headers?.["content-length"] || "0", 10);
|
||||
stream.pipe(file);
|
||||
|
||||
res.on("data", chunk => {
|
||||
progress.current += chunk.length;
|
||||
subscriber.next(progress);
|
||||
});
|
||||
return () => {
|
||||
stream.destroy();
|
||||
}
|
||||
|
||||
res.pipe(file);
|
||||
});
|
||||
|
||||
req.on("error", err => {
|
||||
subscriber.error(err);
|
||||
});
|
||||
}).pipe(tap({ error: e => log.error(e, url, dest) }), shareReplay(1));
|
||||
}
|
||||
|
||||
|
||||
@@ -23,14 +23,15 @@ export class BeatSaverService {
|
||||
}
|
||||
|
||||
public async getMapDetailsFromHashs(hashs: string[]): Promise<BsvMapDetail[]> {
|
||||
const filtredHashs = hashs.map(h => h.toLowerCase()).filter(hash => !Array.from(this.cachedMapsDetails.keys()).includes(hash));
|
||||
const filtredHashs = hashs.map(h => h.toLowerCase()).filter(hash => !this.cachedMapsDetails.has(hash));
|
||||
const chunkHash = splitIntoChunk(filtredHashs, 50);
|
||||
|
||||
const mapDetails = Array.from(this.cachedMapsDetails.entries()).reduce((res, [hash, details]) => {
|
||||
if (hashs.includes(hash)) {
|
||||
res.push(details);
|
||||
const mapDetails = hashs.reduce((acc, hash) => {
|
||||
const detail = this.cachedMapsDetails.get(hash.toLowerCase());
|
||||
if (detail) {
|
||||
acc.push(detail);
|
||||
}
|
||||
return res;
|
||||
return acc;
|
||||
}, [] as BsvMapDetail[]);
|
||||
|
||||
await Promise.allSettled(
|
||||
|
||||
@@ -2,8 +2,7 @@ import { BsvMapDetail, MapFilter, MapRequirement, MapSpecificity, MapStyle, MapT
|
||||
import { motion } from "framer-motion";
|
||||
import { MutableRefObject, useEffect, useRef, useState } from "react";
|
||||
import { BsmCheckbox } from "../../shared/bsm-checkbox.component";
|
||||
import { minToS } from "../../../../shared/helpers/time.helpers";
|
||||
import dateFormat from "dateformat";
|
||||
import { minToS, sToMs } from "../../../../shared/helpers/time.helpers";
|
||||
import { BsmRange } from "../../shared/bsm-range.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { MAP_DIFFICULTIES_COLORS } from "shared/models/maps/difficulties-colors";
|
||||
@@ -13,6 +12,7 @@ import clone from "rfdc";
|
||||
import { GlowEffect } from "../../shared/glow-effect.component";
|
||||
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
|
||||
import { SongDetails } from "shared/models/maps";
|
||||
import formatDuration from "format-duration";
|
||||
|
||||
export type Props = {
|
||||
className?: string;
|
||||
@@ -60,9 +60,8 @@ export function FilterPanel({ className, ref, playlist = false, filter, localDat
|
||||
if (sec === MAX_DURATION) {
|
||||
return "∞";
|
||||
}
|
||||
const date = new Date(0);
|
||||
date.setSeconds(sec);
|
||||
return sec > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss");
|
||||
const ms = sToMs(sec);
|
||||
return formatDuration(ms, { leading: true });
|
||||
})();
|
||||
|
||||
return renderLabel(textValue, sec === MAX_DURATION);
|
||||
|
||||
@@ -4,7 +4,7 @@ import { BsmLink } from "../../shared/bsm-link.component";
|
||||
import { BsmIcon } from "../../svgs/bsm-icon.component";
|
||||
import { BsmButton } from "../../shared/bsm-button.component";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
import { useState, Fragment, useRef } from "react";
|
||||
import { useState, Fragment, useRef, useMemo } from "react";
|
||||
import { LinkOpenerService } from "renderer/services/link-opener.service";
|
||||
import dateFormat from "dateformat";
|
||||
import { AudioPlayerService } from "renderer/services/audio-player.service";
|
||||
@@ -30,6 +30,8 @@ import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
|
||||
import { BPListDifficulty } from "shared/models/playlists/playlist.interface";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import { cn } from "renderer/helpers/css-class.helpers";
|
||||
import { sToMs } from "shared/helpers/time.helpers";
|
||||
import formatDuration from "format-duration";
|
||||
|
||||
export type MapItemComponentProps<T = unknown> = {
|
||||
hash: string;
|
||||
@@ -106,14 +108,13 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
|
||||
return dateFormat(date, "d mmm yyyy");
|
||||
});
|
||||
|
||||
const durationText = (() => {
|
||||
const durationText = useMemo(() => {
|
||||
if (!duration) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(0);
|
||||
date.setSeconds(duration);
|
||||
return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss");
|
||||
})();
|
||||
const durationMs = sToMs(duration);
|
||||
return formatDuration(durationMs, { leading: true });
|
||||
}, [duration]);
|
||||
|
||||
const parseDiffLabel = (diffLabel: string) => {
|
||||
if (MAP_DIFFICULTIES.includes(diffLabel as SongDiffName)) {
|
||||
|
||||
+3
-2
@@ -3,9 +3,9 @@ import { Dispatch, SetStateAction, useState } from "react";
|
||||
import { BsmRange } from "renderer/components/shared/bsm-range.component";
|
||||
import { cn } from "renderer/helpers/css-class.helpers"
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import dateFormat from "dateformat";
|
||||
import { hourToS, sToMs } from "shared/helpers/time.helpers";
|
||||
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
|
||||
import formatDuration from "format-duration";
|
||||
|
||||
type Props = {
|
||||
className?: string;
|
||||
@@ -81,7 +81,8 @@ export function LocalPlaylistFilterPanel({ className, filter, onChange }: Props)
|
||||
return "∞";
|
||||
}
|
||||
|
||||
return sec > 3600 ? dateFormat(sToMs(sec), "h:MM:ss") : dateFormat(sToMs(sec), "MM:ss");
|
||||
const ms = sToMs(sec);
|
||||
return formatDuration(ms, { leading: true });
|
||||
})();
|
||||
|
||||
return renderLabel(textValue, sec === MAX_DURATION);
|
||||
|
||||
+5
-4
@@ -282,8 +282,9 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
|
||||
let map: BsmLocalMap;
|
||||
|
||||
if(playlistSong.hash){
|
||||
map = maps.find(m => m.hash.toLowerCase() === playlistSong.hash.toLowerCase());
|
||||
if(playlistSong.hash || playlistSong?.songDetails?.hash){
|
||||
const hash = (playlistSong.hash || playlistSong.songDetails.hash).toLowerCase();
|
||||
map = maps.find(m => m.hash.toLowerCase() === hash);
|
||||
}
|
||||
else if(playlistSong.key){
|
||||
map = maps.find(m => m?.songDetails?.id === playlistSong.key);
|
||||
@@ -379,8 +380,8 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
|
||||
if(!playlists){ return []; }
|
||||
|
||||
return playlists.filter(p => {
|
||||
if(!p.playlistTitle.toLowerCase().includes(search.toLowerCase())){ return false; }
|
||||
if(!p.playlistAuthor.toLowerCase().includes(search.toLowerCase())){ return false; }
|
||||
if(!p.playlistTitle?.toLowerCase().includes(search.toLowerCase())){ return false; }
|
||||
if(!p.playlistAuthor?.toLowerCase().includes(search.toLowerCase())){ return false; }
|
||||
|
||||
if(typeof p.nbMaps === "number" && (typeof playlistFiler?.minNbMaps === "number" || typeof playlistFiler?.maxNbMaps === "number")){
|
||||
if(playlistFiler?.minNbMaps && p.nbMaps < playlistFiler.minNbMaps){ return false; }
|
||||
|
||||
@@ -5,7 +5,6 @@ import { ClockIcon } from 'renderer/components/svgs/icons/clock-icon.component';
|
||||
import { MapIcon } from 'renderer/components/svgs/icons/map-icon.component';
|
||||
import { PersonIcon } from 'renderer/components/svgs/icons/person-icon.component';
|
||||
import { useThemeColor } from 'renderer/hooks/use-theme-color.hook';
|
||||
import dateFormat from 'dateformat';
|
||||
import { NpsIcon } from 'renderer/components/svgs/icons/nps-icon.component';
|
||||
import { GlowEffect } from 'renderer/components/shared/glow-effect.component';
|
||||
import { memo, useState } from 'react';
|
||||
@@ -18,6 +17,8 @@ import { BsmBasicSpinner } from 'renderer/components/shared/bsm-basic-spinner/bs
|
||||
import defaultImage from "../../../../../assets/images/default-version-img.jpg";
|
||||
import equal from 'fast-deep-equal';
|
||||
import { useTranslation } from 'renderer/hooks/use-translation.hook';
|
||||
import { sToMs } from 'shared/helpers/time.helpers';
|
||||
import formatDuration from 'format-duration';
|
||||
|
||||
export type PlaylistItemComponentProps = {
|
||||
title?: string;
|
||||
@@ -79,10 +80,14 @@ export const PlaylistItem = memo(({ title,
|
||||
const showNps = minNps !== undefined && maxNps !== undefined;
|
||||
|
||||
const durationText = (() => {
|
||||
|
||||
console.log("DURATION", duration);
|
||||
|
||||
if (!duration) {
|
||||
return null;
|
||||
}
|
||||
return duration > 3600 ? dateFormat(duration * 1000, "h:MM:ss") : dateFormat(duration * 1000, "MM:ss");
|
||||
const durationMs = sToMs(duration);
|
||||
return formatDuration(durationMs, { leading: true });
|
||||
})();
|
||||
|
||||
return (
|
||||
|
||||
+7
-4
@@ -27,7 +27,6 @@ import { MapIcon } from "renderer/components/svgs/icons/map-icon.component";
|
||||
import { PersonIcon } from "renderer/components/svgs/icons/person-icon.component";
|
||||
import { ClockIcon } from "renderer/components/svgs/icons/clock-icon.component";
|
||||
import { NpsIcon } from "renderer/components/svgs/icons/nps-icon.component";
|
||||
import dateFormat from 'dateformat';
|
||||
import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
|
||||
import { BPList, BPListDifficulty, PlaylistSong } from "shared/models/playlists/playlist.interface";
|
||||
import { EditPlaylistInfosModal } from "./edit-playlist-infos-modal.component";
|
||||
@@ -36,6 +35,9 @@ import { DraggableVirtualScroll } from "renderer/components/shared/virtual-scrol
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import BeatWaiting from "../../../../../../../assets/images/apngs/beat-waiting.png";
|
||||
import BeatConflict from "../../../../../../../assets/images/apngs/beat-conflict.png";
|
||||
import { findHashInString } from "shared/helpers/string.helpers";
|
||||
import { sToMs } from "shared/helpers/time.helpers";
|
||||
import formatDuration from "format-duration";
|
||||
|
||||
type Props = {
|
||||
maps$: Observable<BsmLocalMap[]>;
|
||||
@@ -115,7 +117,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
|
||||
const maps = await lastValueFrom(maps$.pipe(take(1)));
|
||||
|
||||
const playlistMapsRes = (playlist?.songs ?? []).reduce((acc, song) => {
|
||||
const songHash = song.hash?.toLowerCase();
|
||||
const songHash = song?.hash?.toLowerCase() ?? song?.songDetails?.hash?.toLowerCase() ?? findHashInString(song.levelid)?.toLowerCase();
|
||||
const map = maps.find(map => map.hash === songHash);
|
||||
|
||||
if(map){
|
||||
@@ -361,6 +363,7 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
|
||||
|
||||
const playlistDuration = useMemo(() => {
|
||||
if(!playlistMaps || Object.keys(playlistMaps).length === 0){ return "0:00"; }
|
||||
|
||||
const durations = Object.values(playlistMaps ?? {}).map(playlistMap => {
|
||||
|
||||
if(!playlistMap?.map){ return 0; }
|
||||
@@ -379,8 +382,8 @@ export const EditPlaylistModal: ModalComponent<BPList, Props> = ({ resolver, opt
|
||||
return 0;
|
||||
}).filter(duration => !Number.isNaN(duration));
|
||||
|
||||
const totalDuration = durations.reduce((acc, duration) => acc + duration, 0);
|
||||
return totalDuration > 3600 ? dateFormat(totalDuration * 1000, "H:MM:ss") : dateFormat(totalDuration * 1000, "MM:ss");
|
||||
const totalDuration = sToMs(durations.reduce((acc, duration) => acc + duration, 0));
|
||||
return formatDuration(totalDuration, { leading: true });
|
||||
}, [playlistMaps]);
|
||||
|
||||
const [playlistMinNps, playlistMaxNps] = useMemo(() => {
|
||||
|
||||
+4
-4
@@ -1,6 +1,5 @@
|
||||
import { ReactNode } from "react"
|
||||
import { BsmImage } from "renderer/components/shared/bsm-image.component";
|
||||
import dateFormat from "dateformat";
|
||||
import { MapIcon } from "renderer/components/svgs/icons/map-icon.component";
|
||||
import { PersonIcon } from "renderer/components/svgs/icons/person-icon.component";
|
||||
import { ClockIcon } from "renderer/components/svgs/icons/clock-icon.component";
|
||||
@@ -9,6 +8,8 @@ import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
|
||||
import { ThemeColorGradientSpliter } from "renderer/components/shared/theme-color-gradient-spliter.component";
|
||||
import { CrossIcon } from "renderer/components/svgs/icons/cross-icon.component";
|
||||
import { useTranslation } from "renderer/hooks/use-translation.hook";
|
||||
import { sToMs } from "shared/helpers/time.helpers";
|
||||
import formatDuration from "format-duration";
|
||||
|
||||
export type PlaylistDetailsTemplateProps = {
|
||||
title: string;
|
||||
@@ -40,9 +41,8 @@ export function PlaylistDetailsTemplate({title, imagebase64, imageUrl, author, d
|
||||
if (!duration) {
|
||||
return null;
|
||||
}
|
||||
const date = new Date(0);
|
||||
date.setSeconds(duration);
|
||||
return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss");
|
||||
const durationMs = sToMs(duration);
|
||||
return formatDuration(durationMs, { leading: true });
|
||||
})();
|
||||
|
||||
return (
|
||||
|
||||
@@ -83,7 +83,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
|
||||
<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={0.01} values={[volume.muted ? 0 : volume.volume]} colors={[color, "currentColor"]} onChange={val => audio.setVolume(val[0])} onFinalChange={val => audio.setFinalVolume(val[0])} />
|
||||
<BsmRange min={0} max={1} step={0.01} values={[volume.muted ? 0 : volume.volume]} colors={[color, "currentColor"]} onChange={val => audio.setVolume(val[0])} onFinalChange={val => audio.setVolume(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]" : null} icon={volumeIcon} withBar={false} onClick={() => audio.toggleMute()} />
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ import { NotificationType } from "../../shared/models/notification/notification.
|
||||
import { OsDiagnosticService } from "./os-diagnostic.service";
|
||||
import { ProgressBarService } from "./progress-bar.service";
|
||||
import { NotificationService } from "./notification.service";
|
||||
import { CustomError } from "shared/models/exceptions/custom-error.class";
|
||||
|
||||
export class BsModsManagerService {
|
||||
private static instance: BsModsManagerService;
|
||||
@@ -79,8 +80,8 @@ export class BsModsManagerService {
|
||||
const desc = `notifications.mods.install-mods.msg.${isFullyInstalled ? "success" : "warning"}`;
|
||||
|
||||
this.notifications.notify({ type: isFullyInstalled ? NotificationType.SUCCESS : NotificationType.WARNING, title, desc, duration: this.NOTIFICATION_DURATION });
|
||||
}).catch(e => {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.install-mods.msg.errors.${e}`, duration: this.NOTIFICATION_DURATION });
|
||||
}).catch((e: CustomError) => {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.install-mods.msg.errors.${e?.code}`, duration: this.NOTIFICATION_DURATION });
|
||||
}).finally(() => {
|
||||
this.isInstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
@@ -104,8 +105,8 @@ export class BsModsManagerService {
|
||||
|
||||
return lastValueFrom(this.ipcService.sendV2("uninstall-mods", { mods: [mod], version })).then(() => {
|
||||
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-mod.titles.success", duration: this.NOTIFICATION_DURATION });
|
||||
}).catch(e => {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-mod.msg.errors.${e}`, duration: this.NOTIFICATION_DURATION });
|
||||
}).catch((e: CustomError) => {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-mod.msg.errors.${e?.code}`, duration: this.NOTIFICATION_DURATION });
|
||||
}).finally(() => {
|
||||
this.isUninstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
@@ -129,8 +130,8 @@ export class BsModsManagerService {
|
||||
this.isUninstalling$.next(true);
|
||||
return lastValueFrom(this.ipcService.sendV2("uninstall-all-mods", version)).then(() => {
|
||||
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-all-mods.titles.success", desc: "notifications.mods.uninstall-all-mods.msg.success", duration: this.NOTIFICATION_DURATION });
|
||||
}).catch(e => {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-all-mods.msg.errors.${e}`, duration: this.NOTIFICATION_DURATION });
|
||||
}).catch((e: CustomError) => {
|
||||
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-all-mods.msg.errors.${e?.code}`, duration: this.NOTIFICATION_DURATION });
|
||||
}).finally(() => {
|
||||
this.isUninstalling$.next(false);
|
||||
this.progressBar.hide();
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
const HashAlgorithmsLengths = {
|
||||
sha1: 40,
|
||||
} as const;
|
||||
|
||||
export function findHashInString(str: string, algorithm: keyof typeof HashAlgorithmsLengths = 'sha1'): string | undefined {
|
||||
|
||||
if(!str) { return undefined; }
|
||||
|
||||
const hashLength = HashAlgorithmsLengths[algorithm];
|
||||
const regex = new RegExp(`[a-fA-F0-9]{${hashLength}}`, "g");
|
||||
const match = regex.exec(str);
|
||||
return match ? match[0] : undefined;
|
||||
}
|
||||
Reference in New Issue
Block a user