Compare commits

...

59 Commits

Author SHA1 Message Date
MathieuG-P eb30400b7c update to 1.5.0-alpha.3 2024-08-01 01:06:38 +02:00
MathieuG-P 0fd1e211ba Merge pull request #541 from Zagrios/chore/filter-sensitive-data-in-logs
[chore] filter sensitive data in logs
2024-08-01 00:55:39 +02:00
MathieuG-P 64484bfd4e Merge branch 'master' into chore/filter-sensitive-data-in-logs 2024-08-01 00:51:27 +02:00
MathieuG-P 499956d3b3 update to 1.4.8 2024-08-01 00:23:32 +02:00
MathieuG-P c313685f6f [chore] filter potential oculus token in logs 2024-08-01 00:20:02 +02:00
MathieuG-P 2e750f3d5a chore : use name instead of "_id" beacause BeatMods is broken 2024-07-30 22:29:58 +02:00
MathieuG-P e1c3115668 Merge pull request #540 from Zagrios/qol/dont-reinstall-all-mods-by-default
[qol] dont reinstall all mods by default
2024-07-29 21:32:57 +02:00
MathieuG-P a6462530bb [qol] reinstall all translations + fix buttons sizing 2024-07-29 21:11:14 +02:00
MathieuG-P 8316a9ef80 [qol] fix transform style not working 2024-07-29 20:31:06 +02:00
MathieuG-P 152313289c [qol] install only mods not already installed (not finished) 2024-07-28 21:40:09 +02:00
MathieuG-P c96ad6f7cd Merge branch 'master' into qol/dont-reinstall-all-mods-by-default 2024-07-28 19:46:43 +02:00
MathieuG-P b7cf3c5d20 Merge pull request #539 from Zagrios/chore/update-to-electron-31
[chore] update to electron 31 + update deps
2024-07-28 16:51:24 +02:00
MathieuG-P 97d3c2a3ae [chore] update to electron 31 + update deps 2024-07-28 16:41:16 +02:00
MathieuG-P a20ebe2075 Merge pull request #537 from Zagrios/bugfix/536/bug-no-maps-found-for-tag-dance
[bugfix-536] Fix dance-style filter
2024-07-27 16:12:00 +02:00
MathieuG-P 2f5a74b155 [bugfix-536] Fix dance-style filter 2024-07-27 16:06:07 +02:00
MathieuG-P a20c73d0e9 [qol] dont reinstall all mods by default (not finnished) 2024-07-27 15:51:06 +02:00
MathieuG-P aeca1c1e9e Merge pull request #535 from Zagrios/qol/dont-select-default-mods-when-mods-are-installed
[QOL] Do not reselect default mods when mods are already installed
2024-07-25 20:50:12 +02:00
MathieuG-P b9ce95968c Merge pull request #530 from Zagrios/feature/feature/small-addition-to-the-version-list
[bugfix] fix incorrect active state in shared nav bar item
2024-07-25 20:39:38 +02:00
MathieuG-P 490ccfdc1e [qol] Do not reselect default mods when mods are already installed 2024-07-24 15:45:35 +02:00
Shidorien 6d0c6b4f64 remove useless console.log 2024-07-23 21:13:22 +02:00
Shidorien 89c3b0906b fix little issue 2024-07-23 21:06:22 +02:00
Shidorien 8cd74c3b0b fix incorrect active state in shared nav bar item 2024-07-23 20:31:33 +02:00
MathieuG-P 769dab2896 Add changelog for 1.5.0-alpha.2 2024-07-22 18:26:13 +02:00
MathieuG-P 8f1f998c5a Update to 1.5.0-alpha.2 2024-07-22 17:59:22 +02:00
MathieuG-P 48d03cccbb Merge pull request #524 from Zagrios/bugfix/fix-timeout-when-downloading-maps-from-bsv
[bugfix] Downloading maps sometimes times out
2024-07-21 15:44:30 +02:00
MathieuG-P 7d54ae79f7 Merge pull request #523 from Zagrios/bugfix/unable-to-install-bsipa-when-its-path-contain-spaces
[bugfix] unable to install BSIPA if BSIPA path was containing spaces
2024-07-21 15:41:23 +02:00
MathieuG-P c46fdd56f6 Merge pull request #522 from Zagrios/bugfix/495/bug-custom-map-runtime-not-displaying-correctly-in-bsmanager
[bugfix] fix sometimes maps duration were wrong
2024-07-21 15:41:10 +02:00
MathieuG-P 22dc1ca511 [bugfix] fix timeout when downloading maps 2024-07-21 15:36:33 +02:00
MathieuG-P 709105b9f4 [bugfix] fixed : unable to install BSIPA if BSIPA path was containing spaces + wrong translation when bsipa was throwing errors 2024-07-21 14:53:49 +02:00
MathieuG-P 1c0cfb6b89 [bugfix] playlist duraction was always NaN 2024-07-21 14:10:30 +02:00
MathieuG-P 7ebc1bff16 [bugfix] fix sometimes maps duration were wrong 2024-07-21 14:09:14 +02:00
MathieuG-P 0e1cda5050 Merge pull request #517 from Zagrios/bugfix/playlist-maps-with-malformed-hash-doesnt-get-loaded
[bugfix] fix playlist maps with malformed hash doesnt get loaded + improve playlist maps reading reliability
2024-07-17 20:42:12 +02:00
MathieuG-P 331626bd1e Merge pull request #518 from Zagrios/bugfix/crash-when-changing-audio-volume
[bugfix] fix crash when changing audio volume
2024-07-17 20:41:31 +02:00
MathieuG-P ea62f2443f [bugfix-517] use regex.exec instead of .match 2024-07-17 20:35:31 +02:00
MathieuG-P 532ab08e46 [bugfix] fix crash when changing audio volume 2024-07-17 20:32:53 +02:00
MathieuG-P a6fa55719d [bugfix] fix playlist maps with malformed hash doesnt get loaded + improve playlist maps reading reliability 2024-07-17 20:26:54 +02:00
MathieuG-P 163857a14f Merge pull request #513 from Zagrios/bugfix/bsm-crashs-when-loading-playlists-with-an-invalid-playlist-file
[bugfix] Bsm crashs when loading playlists with an invalid playlist file
2024-07-15 21:54:53 +02:00
MathieuG-P 854e0e2761 Merge branch 'master' into bugfix/bsm-crashs-when-loading-playlists-with-an-invalid-playlist-file 2024-07-15 21:50:13 +02:00
MathieuG-P eba6f916a3 Merge pull request #514 from Zagrios/bugfix/read-playlists-in-sub-folders
[bugfix] Playlists in sub folders doesn't get loaded
2024-07-15 21:43:03 +02:00
MathieuG-P 9790f35749 [bugfix] Playlists in sub folders doesn't get loaded 2024-07-15 21:35:26 +02:00
MathieuG-P 93d3cbc8bf [bugfix] Bsm crashs when loading playlists with an invalid playlist file 2024-07-15 20:59:26 +02:00
MathieuG-P 51ea1b130b Merge pull request #512 from Zagrios/chore/update-changelog
Chore/update changelog
2024-07-15 15:58:10 +02:00
shidorien d020b72f20 Add web site button to ChangelogModal component 2024-07-15 15:51:19 +02:00
shidorien f2ac952cd9 Update changelogs.json with new features, fixes, and improvements 2024-07-15 15:23:38 +02:00
shidorien 189d2388fb add missing scrollbar 2024-07-15 15:16:42 +02:00
shidorien 1c989a2402 Update changelog for BSManager 1.5.0-alpha.1 release 2024-07-15 15:11:17 +02:00
shidorien 959e60127d Update changelog for BSManager 1.5.0-alpha.1 2024-07-15 15:08:22 +02:00
shidorien 0faa0f1288 Update changelog for BSManager 1.5.0-alpha.1 2024-07-15 15:00:53 +02:00
shidorien 5ad25f58d5 Update changelogs.json with BSManager 1.5.0-alpha.1 release 2024-07-15 14:57:39 +02:00
shidorien 8ace5e6a7a Add changelog fort 1.5.0-alpha.1 2024-07-15 14:46:14 +02:00
MathieuG-P 185d01e7b4 version to 1.5.0-alpha.1 2024-07-14 22:34:29 +02:00
MathieuG-P 755e407372 Merge pull request #511 from Zagrios/bugfix/bsipa-installation-never-throw-error-even-if-bsipa-process-errors
[bugfix] During the BSIPA installation, no errors are displayed, even if BSIPA generates errors
2024-07-14 21:27:01 +02:00
MathieuG-P 4f123b69ef [bugfix] bsipa installation never show error even if bsipa throw errors 2024-07-14 21:22:25 +02:00
MathieuG-P 7e2334f8fc Merge pull request #509 from Insprill/fix/linux-build
Fix Linux build & crash on startup
2024-07-14 17:46:13 +02:00
MathieuG-P 5be533ccfb chore: Update Linux publishing command in package.json 2024-07-14 17:32:45 +02:00
MathieuG-P b8eafd5f9b chore: Update Linux publishing command in package.json 2024-07-14 17:23:19 +02:00
MathieuG-P 3f3bb959c4 Merge branch 'github-action/build-linux' 2024-07-14 17:17:21 +02:00
MathieuG-P c4d5db809b build for linux 2024-07-14 15:52:51 +02:00
Pierce Thompson 8ecc9d30b3 Fix Linux build 2024-07-13 15:13:15 -04:00
56 changed files with 1157 additions and 800 deletions
+64
View File
@@ -0,0 +1,64 @@
/**
* Webpack config for development electron main process
*/
import path from 'path';
import webpack from 'webpack';
import { BundleAnalyzerPlugin } from 'webpack-bundle-analyzer';
import { merge } from 'webpack-merge';
import checkNodeEnv from '../scripts/check-node-env';
import baseConfig from './webpack.config.base';
import webpackPaths from './webpack.paths';
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
// at the dev webpack config is not accidentally run in a production environment
if (process.env.NODE_ENV === 'production') {
checkNodeEnv('development');
}
const configuration: webpack.Configuration = {
devtool: 'inline-source-map',
mode: 'development',
target: 'electron-main',
entry: {
main: path.join(webpackPaths.srcMainPath, 'main.ts'),
preload: path.join(webpackPaths.srcMainPath, 'preload.ts'),
},
output: {
path: webpackPaths.dllPath,
filename: '[name].bundle.dev.js',
library: {
type: 'umd',
},
},
plugins: [
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
new BundleAnalyzerPlugin({
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
analyzerPort: 8888,
}),
new webpack.DefinePlugin({
'process.type': '"browser"',
}),
],
/**
* Disables webpack processing of __dirname and __filename.
* If you run the bundle in node.js it falls back to these values of node.js.
* https://github.com/webpack/webpack/issues/2010
*/
node: {
__dirname: false,
__filename: false,
},
};
export default merge(baseConfig, configuration);
+4
View File
@@ -2,6 +2,9 @@ const path = require("path");
const rootPath = path.join(__dirname, "../..");
const erbPath = path.join(__dirname, '..');
const erbNodeModulesPath = path.join(erbPath, 'node_modules');
const dllPath = path.join(__dirname, "../dll");
const srcPath = path.join(rootPath, "src");
@@ -22,6 +25,7 @@ const buildPath = path.join(releasePath, "build");
export default {
rootPath,
erbNodeModulesPath,
dllPath,
srcPath,
srcMainPath,
+8 -5
View File
@@ -1,9 +1,12 @@
import fs from "fs";
import webpackPaths from "../configs/webpack.paths";
import fs from 'fs';
import webpackPaths from '../configs/webpack.paths';
const { srcNodeModulesPath } = webpackPaths;
const { appNodeModulesPath } = webpackPaths;
const { srcNodeModulesPath, appNodeModulesPath, erbNodeModulesPath } = webpackPaths;
if (!fs.existsSync(srcNodeModulesPath) && fs.existsSync(appNodeModulesPath)) {
fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, "junction");
fs.symlinkSync(appNodeModulesPath, srcNodeModulesPath, 'junction');
}
if (!fs.existsSync(erbNodeModulesPath) && fs.existsSync(appNodeModulesPath)) {
fs.symlinkSync(appNodeModulesPath, erbNodeModulesPath, 'junction');
}
+2 -2
View File
@@ -13,7 +13,7 @@ jobs:
strategy:
matrix:
os: [windows-latest]
os: [windows-latest, ubuntu-latest]
steps:
- name: Check out Git repository
@@ -30,5 +30,5 @@ jobs:
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: release
name: release-${{ matrix.os }}
path: release
+4 -1
View File
@@ -2,6 +2,9 @@
name: Realease Linux
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
on:
workflow_dispatch:
@@ -23,7 +26,7 @@ jobs:
node-version: 18.x
cache: "npm"
- run: npm ci
- run: npm run build && electron-builder --linux --x64 --publish always
- run: npm run publish:linux
- name: Upload artifact
uses: actions/upload-artifact@v4
+14 -1
View File
@@ -1 +1,14 @@
{}
{
"1.5.0-alpha.1":{
"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><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><h2>Fixes</h2><ul><li>Fixed an issue where switching between Beat Saber versions too quickly displayed mods for the wrong version</li><li>Fixed an issue where the launch window from a shortcut would never close</li><li>Advanced launch arguments are used even when the input field is closed</li><li>Fixed an issue where having many maps caused some maps to not have their information loaded</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" : 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&#39;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"
}
}
+3 -2
View File
@@ -73,7 +73,8 @@
"mods-not-available": "Für diese Version von Beat Saber sind noch keine Mods verfügbar",
"buttons": {
"more-infos": "Mehr Infos",
"install-or-update": "Installieren oder aktualisieren"
"install-or-update": "Installieren oder aktualisieren",
"reinstall-all": "Alles neu installieren"
},
"mods-grid": {
"header-bar": {
@@ -738,7 +739,7 @@
"accuracy": "Präzision",
"balanced": "Ausgeglichen",
"challenge": "Herausforderung",
"dancestyle": "Tanz",
"dance-style": "Tanz",
"fitness": "Fitness",
"speed": "Geschwindigkeit",
"tech": "Tech"
+3 -2
View File
@@ -73,7 +73,8 @@
"mods-not-available": "No mods are available yet for this version of Beat Saber",
"buttons": {
"more-infos": "More info",
"install-or-update": "Install or update"
"install-or-update": "Install or update",
"reinstall-all": "Reinstall all"
},
"mods-grid": {
"header-bar": {
@@ -745,7 +746,7 @@
"accuracy": "accuracy",
"balanced": "balanced",
"challenge": "challenge",
"dancestyle": "dance",
"dance-style": "dance",
"fitness": "fitness",
"speed": "speed",
"tech": "tech"
+3 -2
View File
@@ -73,7 +73,8 @@
"mods-not-available": "Aún no hay mods disponibles para esta versión de Beat Saber",
"buttons": {
"more-infos": "Más información",
"install-or-update": "Instalar o actualizar"
"install-or-update": "Instalar o actualizar",
"reinstall-all": "Reinstalar todo"
},
"mods-grid": {
"header-bar": {
@@ -738,7 +739,7 @@
"accuracy": "precisión",
"balanced": "equilibrado",
"challenge": "desafío",
"dancestyle": "baile",
"dance-style": "baile",
"fitness": "fitness",
"speed": "speed",
"tech": "tech"
+3 -2
View File
@@ -73,7 +73,8 @@
"mods-not-available": "Aucun mod n'est encore disponible pour cette version de Beat Saber",
"buttons": {
"more-infos": "Plus d'infos",
"install-or-update": "Installer ou mettre à jour"
"install-or-update": "Installer ou mettre à jour",
"reinstall-all": "Tout réinstaller"
},
"mods-grid": {
"header-bar": {
@@ -738,7 +739,7 @@
"accuracy": "précision",
"balanced": "équilibrée",
"challenge": "challenge",
"dancestyle": "dance",
"dance-style": "dance",
"fitness": "fitness",
"speed": "vitesse",
"tech": "tech"
+3 -2
View File
@@ -73,7 +73,8 @@
"mods-not-available": "このバージョンで使用できるMODはまだありません。",
"buttons": {
"more-infos": "詳細情報",
"install-or-update": "インストールとアップデート"
"install-or-update": "インストールとアップデート",
"reinstall-all": "すべて再インストール"
},
"mods-grid": {
"header-bar": {
@@ -738,7 +739,7 @@
"accuracy": "正確",
"balanced": "バランス",
"challenge": "挑戦",
"dancestyle": "ダンス",
"dance-style": "ダンス",
"fitness": "フットネス",
"speed": "スピード",
"tech": "技術的"
+3 -2
View File
@@ -73,7 +73,8 @@
"mods-not-available": "Не найдены моды для этой версии Beat Saber",
"buttons": {
"more-infos": "Подробнее",
"install-or-update": "Установить или обновить"
"install-or-update": "Установить или обновить",
"reinstall-all": "Переустановить все"
},
"mods-grid": {
"header-bar": {
@@ -737,7 +738,7 @@
"accuracy": "точность",
"balanced": "баланс",
"challenge": "испытание",
"dancestyle": "танец",
"dance-style": "танец",
"fitness": "фитнес",
"speed": "скорость",
"tech": "техника"
+3 -2
View File
@@ -73,7 +73,8 @@
"mods-not-available": "該版本 BeatSaber 暫無可用 Mod",
"buttons": {
"more-infos": "更多資訊",
"install-or-update": "安裝或更新"
"install-or-update": "安裝或更新",
"reinstall-all": "重新安裝全部"
},
"mods-grid": {
"header-bar": {
@@ -738,7 +739,7 @@
"accuracy": "精確度",
"balanced": "平衡",
"challenge": "挑戰",
"dancestyle": "舞蹈",
"dance-style": "舞蹈",
"fitness": "健身",
"speed": "速度",
"tech": "技術"
+3 -2
View File
@@ -73,7 +73,8 @@
"mods-not-available": "该版本 BeatSaber 暂无可用 Mod",
"buttons": {
"more-infos": "更多信息",
"install-or-update": "安装或更新"
"install-or-update": "安装或更新",
"reinstall-all": "重新安装全部"
},
"mods-grid": {
"header-bar": {
@@ -738,7 +739,7 @@
"accuracy": "精确度",
"balanced": "平衡",
"challenge": "挑战",
"dancestyle": "舞蹈",
"dance-style": "舞蹈",
"fitness": "健身",
"speed": "速度",
"tech": "技术"
+531 -570
View File
File diff suppressed because it is too large Load Diff
+22 -24
View File
@@ -1,24 +1,26 @@
{
"name": "bs-manager",
"description": "Manage maps, mods and more for Beat Saber",
"main": "./src/main/main.ts",
"version": "1.5.0",
"main": "./.erb/dll/main.bundle.dev.js",
"version": "1.5.0-alpha.3",
"scripts": {
"build-rust-scripts": "node -r esbuild-register ./.erb/scripts/build-rust-scripts.js",
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
"build:dll": "cross-env NODE_ENV=development webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
"build:main": "cross-env NODE_ENV=production webpack --config ./.erb/configs/webpack.config.main.prod.ts",
"build:renderer": "cross-env NODE_ENV=production webpack --config ./.erb/configs/webpack.config.renderer.prod.ts",
"postinstall": "node -r esbuild-register .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll",
"postinstall": "ts-node .erb/scripts/check-native-dep.js && electron-builder install-app-deps && npm run build:dll",
"rebuild": "electron-rebuild --parallel --types prod,dev,optional --module-dir release/app",
"prestart": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.dev.ts",
"lint": "cross-env NODE_ENV=development eslint . --ext .js,.jsx,.ts,.tsx",
"package": "node -r esbuild-register ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never && npm run build:dll",
"start": "node -r esbuild-register ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
"start:main": "cross-env NODE_ENV=development NODE_OPTIONS=\"--loader esbuild-register/loader -r esbuild-register\" electronmon .",
"start:preload": "cross-env NODE_ENV=development webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
"start:renderer": "cross-env NODE_ENV=development webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
"package": "ts-node ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never && npm run build:dll",
"start": "ts-node ./.erb/scripts/check-port-in-use.js && npm run prestart && npm run start:renderer",
"start:main": "concurrently -k \"cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --watch --config ./.erb/configs/webpack.config.main.dev.ts\" \"electronmon .\"",
"start:preload": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.preload.dev.ts",
"start:renderer": "cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack serve --config ./.erb/configs/webpack.config.renderer.dev.ts",
"test": "jest",
"publish": "npm run build && electron-builder -c.win.certificateSha1=842a817a51e2a1d360fcd62f54bf5f9193e919e1 --publish always --win --x64"
"publish": "npm run build && electron-builder -c.win.certificateSha1=842a817a51e2a1d360fcd62f54bf5f9193e919e1 --publish always --win --x64",
"publish:linux": "npm run build && electron-builder --publish always --linux --x64"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
@@ -181,12 +183,10 @@
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
"detect-port": "^1.5.1",
"electron": "^28.2.4",
"electron": "^31.3.0",
"electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0",
"electronmon": "^2.0.2",
"esbuild": "^0.20.1",
"esbuild-register": "^3.5.0",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-erb": "^4.1.0",
@@ -209,15 +209,13 @@
"postcss": "^8.4.33",
"postcss-loader": "^8.1.0",
"prettier": "^3.2.4",
"ps-scrollbar-tailwind": "0.0.1",
"react-refresh": "^0.14.0",
"react-test-renderer": "^18.2.0",
"rimraf": "^5.0.5",
"sass": "^1.70.0",
"sass-loader": "^14.1.0",
"style-loader": "^3.3.4",
"tailwind-scrollbar": "^3.1.0",
"tailwindcss": "^3.4.1",
"tailwindcss": "^3.4.7",
"terser-webpack-plugin": "^5.3.10",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
@@ -235,7 +233,7 @@
"@nextui-org/react": "^2.3.6",
"@node-steam/vdf": "^2.2.0",
"@tippyjs/react": "^4.2.6",
"archiver": "^6.0.1",
"archiver": "^7.0.1",
"clsx": "^2.1.1",
"color": "^4.2.3",
"crypto-js": "^4.2.0",
@@ -247,9 +245,10 @@
"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",
"got": "^14.4.2",
"history": "^5.3.0",
"is-elevated": "^4.0.0",
"jszip": "^3.10.1",
@@ -270,14 +269,13 @@
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
"recursive-readdir": "^2.2.3",
"rfdc": "^1.3.1",
"rfdc": "^1.4.1",
"rxjs": "^7.8.1",
"sanitize-filename": "^1.6.3",
"semver": "^7.6.2",
"semver": "^7.6.3",
"serialize-error": "^11.0.3",
"striptags": "^4.0.0-alpha.4",
"tailwind-merge": "^2.4.0",
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss-scoped-groups": "^2.0.0",
"tippy.js": "^6.3.7",
"to-ico": "^1.1.5",
@@ -315,9 +313,9 @@
},
"electronmon": {
"patterns": [
"!src/__tests__/**",
"!release/**",
"!assets/**"
"!**/**",
"src/main/**",
".erb/dll/**"
],
"logLevel": "quiet"
},
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "bs-manager",
"version": "1.5.0",
"version": "1.5.0-alpha.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.5.0",
"version": "1.5.0-alpha.2",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "bs-manager",
"version": "1.5.0",
"version": "1.5.0-alpha.3",
"description": "BSManager",
"main": "./dist/main/main.js",
"author": {
@@ -9,9 +9,9 @@
"url": "https://github.com/Zagrios/bs-manager"
},
"scripts": {
"electron-rebuild": "node -r esbuild-register ../../.erb/scripts/electron-rebuild.js",
"link-modules": "node -r esbuild-register ../../.erb/scripts/link-modules.ts",
"postinstall": "npm run electron-rebuild && npm run link-modules"
"rebuild": "node -r ts-node/register ../../.erb/scripts/electron-rebuild.js",
"link-modules": "node -r ts-node/register ../../.erb/scripts/link-modules.ts",
"postinstall": "npm run rebuild && npm run link-modules"
},
"dependencies": {
"@resvg/resvg-js": "2.6.2",
@@ -16,7 +16,7 @@ ipc.on("import-version", (args, reply) => {
ipc.on("bs-download.installation-folder", (_, reply) => {
const installLocation = InstallationLocationService.getInstance();
reply(from(installLocation.installationDirectory()));
reply(of(installLocation.installationDirectory()));
});
ipc.on("bs-download.set-installation-folder", (args, reply) => {
+129 -6
View File
@@ -23,16 +23,20 @@ import { LivShortcut } from "./services/liv/liv-shortcut.service";
import { SteamLauncherService } from "./services/bs-launcher/steam-launcher.service";
import { FileAssociationService } from "./services/file-association.service";
import { SongDetailsCacheService } from "./services/additional-content/maps/song-details-cache.service";
import { readdirSync, statSync, unlinkSync } from "fs-extra";
const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true";
log.transports.file.level = "info";
log.transports.file.resolvePath = () => {
const now = new Date();
return path.join(app.getPath("logs"), `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}-v${app.getVersion()}.log`);
};
export const filterStrings = new Set<string>();
export const filterPatterns = new Set<RegExp>();
// Filter all occulus tokens
filterPatterns.add(/FRL\S{10,}/g);
initLogger();
deleteOlestLogs();
deleteOldLogs();
log.catchErrors();
if (process.env.NODE_ENV === "production") {
const sourceMapSupport = require("source-map-support");
@@ -128,5 +132,124 @@ if (!gotTheLock) {
log.error(args?.args);
});
ipcMain.on("add-filter-string", (_, args: IpcRequest<string>) => {
filterStrings.add(args?.args);
});
ipcMain.on("add-filter-pattern", (_, args: IpcRequest<string>) => {
filterPatterns.add(new RegExp(args?.args));
});
}).catch(log.error);
}
function initLogger(){
log.transports.file.level = "info";
log.transports.file.resolvePath = () => {
const now = new Date();
return path.join(app.getPath("logs"), `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}-v${app.getVersion()}.log`);
};
log.hooks.push((message) => {
const filterMessage = (filter: string|RegExp, ...param: unknown[]): unknown[] => {
return param.map(data => {
if(typeof data === "string"){
return data.replaceAll(filter, "****");
}
if(data instanceof Error){
data.message = data.message?.replaceAll(filter, "****");
data.stack = data.stack?.replaceAll(filter, "****");
}
if(data instanceof Array){
return filterMessage(filter, ...data);
}
return data;
});
}
filterStrings.forEach(filter => {
if(filter && message.data.length){
message.data = filterMessage(filter, ...message.data);
}
});
filterPatterns.forEach(filter => {
if(filter && message.data.length){
message.data = filterMessage(filter, ...message.data);
}
});
return message;
});
log.catchErrors();
}
function getLogFilesEntries() {
try {
const logsFolder = app.getPath("logs");
let logs = readdirSync(logsFolder, { withFileTypes: true });
logs = logs.filter(file => file.isFile() && path.extname(file.name) === ".log");
logs.sort((a, b) => {
const aStat = statSync(path.join(logsFolder, a.name));
const bStat = statSync(path.join(logsFolder, b.name));
return bStat.mtime.getTime() - aStat.mtime.getTime();
});
return logs.map(file => {
const filePath = path.join(logsFolder, file.name);
const stat = statSync(filePath);
return {
path: filePath,
name: file.name,
stats: stat
};
});
} catch (err) {
log.error('Error while retrieving log files entries:', err);
return [];
}
}
// keep only the last 5 logs
function deleteOldLogs(): void{
try {
let logs = getLogFilesEntries();
logs = logs.slice(5);
logs.forEach(file => {
try {
unlinkSync(file.path);
log.info(`Deleted log file: ${file.path}`);
} catch (err) {
log.error(`Error deleting file ${file.path}:`, err);
}
});
} catch (err) {
log.error("Error while deleting old logs:", err);
}
}
// Temporary function to delete logs before 2024-07-31
function deleteOlestLogs(): void{
// delete all logs before 2024-07-31
const date = new Date(2024, 6, 31); // month is 0-based
const logs = getLogFilesEntries().filter(file => file.stats.mtime.getTime() < date.getTime());
logs.forEach(file => {
try {
unlinkSync(file.path);
log.info(`Deleted log file: ${file.path}`);
} catch (err) {
log.error(`Error deleting file ${file.path}:`, err);
}
});
}
+7 -3
View File
@@ -1,4 +1,4 @@
import { writeFileSync } from "fs-extra";
import { pathExistsSync, readFileSync, writeFileSync } from "fs-extra";
import { tryit } from "shared/helpers/error.helpers";
import log from "electron-log";
import { Subject, debounceTime } from "rxjs";
@@ -22,9 +22,13 @@ export class JsonCache<T = unknown> {
private load(): void {
try {
this._cache = require(this.jsonPath);
if(pathExistsSync(this.jsonPath)){
this._cache = JSON.parse(readFileSync(this.jsonPath).toString());
} else {
log.warn("File cache not exist yet", this.jsonPath);
}
} catch (error) {
log.warn("Failed to load cache or file cache not exist yet", this.jsonPath, error);
log.warn("Failed to load cache file", this.jsonPath, error);
} finally {
this._cache ??= {};
}
@@ -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;
@@ -14,8 +14,9 @@ import { lastValueFrom } from "rxjs";
import JSZip from "jszip";
import { extractZip } from "../../helpers/zip.helpers";
import recursiveReadDir from "recursive-readdir";
import { minToMs } from "../../../shared/helpers/time.helpers";
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,12 +148,22 @@ 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.
: `start /wait /min "" "${ipaPath}" ${args.join(" ")}`;
: `"${ipaPath}" ${args.join(" ")}`;
log.info("START IPA PROCESS", cmd);
const processIPA = spawn(cmd, { cwd: versionPath, detached: true, shell: true });
const timemout = setTimeout(() => {
log.info("Ipa process timeout");
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) {
log.info("Ipa process exist with code 0");
return resolve(true);
@@ -161,10 +172,6 @@ export class BsModsManagerService {
resolve(false);
});
setTimeout(() => {
log.info("Ipa process timeout");
resolve(false)
}, minToMs(1));
});
}
@@ -336,18 +343,15 @@ 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);
mods.push(...deps);
const bsipa = mods.find(mod => mod.name.toLowerCase() === "bsipa");
if (bsipa) {
mods = mods.filter(mod => mod.name.toLowerCase() !== "bsipa");
}
this.nbModsToInstall = mods.length + (bsipa && 1);
this.nbModsToInstall = mods.length + (bsipa ? 1 : 0);
this.nbInstalledMods = 0;
if (bsipa) {
@@ -356,7 +360,7 @@ export class BsModsManagerService {
return false;
});
if (!installed) {
throw "cannot-install-bsipa";
throw CustomError.fromError(new Error("Unable to install BSIPA"), "cannot-install-bsipa");
}
}
@@ -372,7 +376,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;
@@ -392,7 +396,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;
+22 -18
View File
@@ -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(
+1 -1
View File
@@ -8,7 +8,7 @@ import log from "electron-log";
export class UtilsService {
private static instance: UtilsService;
private assetsPath: string = app.isPackaged ? path.join(process.resourcesPath, "assets") : path.join(__dirname, "../../../assets");
private assetsPath: string = app.isPackaged ? path.join(process.resourcesPath, "assets") : path.join(__dirname, "../../assets");
private constructor() {}
+1 -1
View File
@@ -9,7 +9,7 @@ import { isValidUrl } from "../../shared/helpers/url.helpers";
export class WindowManagerService {
private static instance: WindowManagerService;
private readonly PRELOAD_PATH = app.isPackaged ? path.join(__dirname, "preload.js") : path.join(__dirname, "../../../.erb/dll/preload.js");
private readonly PRELOAD_PATH = app.isPackaged ? path.join(__dirname, "preload.js") : path.join(__dirname, "../../.erb/dll/preload.js");
private readonly IS_DEBUG = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true"
private readonly utilsService: UtilsService = UtilsService.getInstance();
@@ -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,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);
@@ -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 (
@@ -30,7 +30,7 @@ export const EnterMetaTokenModal: ModalComponent<string> = ({resolver}) => {
const cancel = () => {
resolver({exitCode: ModalExitCode.CANCELED});
}
return (
<form className="flex flex-col w-80 gap-4">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("modals.enter-meta-token.title")}</h1>
@@ -205,7 +205,7 @@ const PasswordInput = ({onChange, value}: {onChange: (value : {password: string,
<input className="grow px-1 py-[2px] outline-none bg-transparent" onChange={e => onChange({password: e.target.value, valid: isPasswordValid(e.target.value)})} value={value} type={showPassword ? "text" : "password"} name="password" id="password" placeholder={t("modals.enter-meta-token.body.password")} />
<BsmButton className="shrink-0 m-1 rounded-md p-0.5 !bg-light-main-color-3 dark:!bg-main-color-3" icon={showPassword ? "eye-cross" : "eye"} withBar={false} onClick={() => setShowPassword(prev => !prev)} />
</div>
</>
</>
)
}
@@ -14,12 +14,13 @@ export const ChangelogModal: ModalComponent<void, ChangelogVersion> = ({ options
const openTwitter = () => linkOpener.open("https://twitter.com/BSManager_");
const openSupportPage = () => linkOpener.open("https://www.patreon.com/bsmanager");
const openDiscord = () => linkOpener.open("https://discord.gg/uSqbHVpKdV");
const openWebSite = () => linkOpener.open("https://bsmanager.io/");
const date = changelog?.timestamp ? new Date(changelog.timestamp * 1000).toLocaleDateString() : '';
return (
<form className="w-[350px] text-gray-800 dark:text-gray-200 h-[70vh] flex flex-col justify-between">
<h1 className=" p-4 pt-1 text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200 font-bold">{changelog?.title}</h1>
<div className=" overflow-y-scroll h-full content grow" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(changelog?.htmlBody) }}/>
<div className=" overflow-y-scroll h-full content grow scrollbar-default" dangerouslySetInnerHTML={{ __html: DOMPurify.sanitize(changelog?.htmlBody) }}/>
<span className="block w-[100%] mx-auto mt-0 mb-4 h-1 rounded-full bg-main-color-1" />
<div className="flex flex-row justify-between">
<div className="my-auto flex flex-col text-sm">
@@ -39,6 +40,9 @@ export const ChangelogModal: ModalComponent<void, ChangelogVersion> = ({ options
<Tippy content="Patreon" placement="top" className="font-bold !bg-neutral-900" duration={[200, 0]} arrow={false}>
<div><BsmButton onClick={openSupportPage} className="rounded-md p-1 w-7 h-7 " icon="patreon" withBar={false} iconColor="#fff" color="#000"/></div>
</Tippy>
<Tippy content="Web Site" placement="top" className="font-bold !bg-neutral-900" duration={[200, 0]} arrow={false}>
<div><BsmButton onClick={openWebSite} className="rounded-md p-1 w-7 h-7 " icon="web-site" withBar={false} iconColor="#fff" color="#000"/></div>
</Tippy>
</div>
</div>
</form>
@@ -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(() => {
@@ -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 (
@@ -1,28 +1,20 @@
import { Link } from "react-router-dom";
import { BsmIcon } from "renderer/components/svgs/bsm-icon.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { PageStateService } from "renderer/services/page-state.service";
import { NavBarItem } from "./nav-bar-item.component";
import { useService } from "renderer/hooks/use-service.hook";
import Tippy from "@tippyjs/react";
import { useTranslation } from "renderer/hooks/use-translation.hook";
export function SharedNavBarItem() {
const pageState = useService(PageStateService);
const t = useTranslation();
const route = useObservable(() => pageState.route$);
const color = useThemeColor("first-color");
return (
<NavBarItem isActive={route === "/shared"}>
<Tippy content={t("nav-bar.shared.tooltip")} className="font-bold !bg-neutral-900" placement="right-end" arrow={false} duration={[100, 0]} animation="shift-away-subtle">
<Link to="shared" className="w-full flex items-center justify-start content-center max-w-full h-[30px]">
<BsmIcon className="w-[19px] h-[19px] mr-[5px] shrink-0 brightness-125" icon="link" style={{ color }} />
<span className="dark:text-gray-200 text-gray-800 font-bold tracking-wide">{t("nav-bar.shared.text")}</span>
</Link>
</Tippy>
</NavBarItem>
);
}
@@ -14,10 +14,13 @@ import { useService } from "renderer/hooks/use-service.hook";
import { distinctUntilChanged } from "rxjs";
import equal from "fast-deep-equal";
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
import { PageStateService } from "renderer/services/page-state.service";
import { NavBarItem } from "./nav-bar-items/nav-bar-item.component";
export function NavBar() {
const versionManager = useService(BSVersionManagerService);
const versionDownloader = useService(BsDownloaderService);
const pageState = useService(PageStateService);
const downloadingVersion = useObservable(() => versionDownloader.downloadingVersion$.pipe(distinctUntilChanged(equal)));
const installedVersions = useObservable(() => versionManager.installedVersions$);
@@ -25,6 +28,9 @@ export function NavBar() {
const color = useThemeColor("first-color");
const t = useTranslation();
const route = useObservable(() => pageState.route$);
function listVersions(){
const versions = Array.isArray(installedVersions) ? [...installedVersions] : [];
@@ -38,26 +44,30 @@ export function NavBar() {
return (
<nav id="nav-bar" className="z-10 flex flex-col h-full max-h-full items-center p-1">
<BsManagerIcon className="relative aspect-square w-16 h-16 mb-3" />
<ol id="versions" className="w-fit max-w-[150px] relative left-[2px] grow overflow-y-hidden scrollbar-track-transparent scrollbar-default hover:overflow-y-scroll">
<SharedNavBarItem />
<ol id="versions" className="w-fit max-w-[150px] relative left-[2px] grow overflow-y-hidden scrollbar-default hover:overflow-y-scroll">
<NavBarItem isActive={route === "/shared"}>
<SharedNavBarItem />
</NavBarItem>
<NavBarSpliter />
{listVersions().map(version => (
<BsVersionItem key={JSON.stringify(version)} version={version} />
))}
</ol>
<NavBarSpliter />
<div className="w-full pb-2 flex flex-col items-center content-center justify-start gap-1">
<NavBarItem isActive={["/blah","/"].includes(route)}>
<Tippy placement="right" content={t("nav-bar.add-version")} className="!bg-neutral-900" arrow={false}>
<Link className="rounded-md w-9 h-9 flex justify-center items-center hover:bg-light-main-color-3 dark:hover:bg-main-color-3" to="blah">
<BsmIcon icon="add" className="text-blue-500 h-[34px]" style={{ color }} />
</Link>
</Tippy>
<Tippy placement="right" content={t("nav-bar.settings")} className="!bg-neutral-900" arrow={false}>
</NavBarItem>
<NavBarItem isActive={route === "/settings"}>
<Tippy placement="right" content={t("nav-bar.settings")} className="!bg-neutral-900" arrow={false}>
<Link className="rounded-md w-9 h-9 flex justify-center items-center hover:bg-light-main-color-3 dark:hover:bg-main-color-3" to="settings">
<BsmIcon icon="settings" className="text-blue-500 h-7" style={{ color }} />
</Link>
</Tippy>
</div>
</NavBarItem>
</nav>
);
}
@@ -6,11 +6,12 @@ import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
type BsmButtonType = "primary" | "secondary" | "success" | "cancel" | "error" | "none";
export type BsmButtonType = "primary" | "secondary" | "success" | "cancel" | "error" | "none";
type Props = {
className?: string;
style?: CSSProperties;
iconStyle?: CSSProperties;
imgClassName?: string;
iconClassName?: string;
icon?: BsmIconType;
@@ -29,7 +30,7 @@ type Props = {
textClassName?: string;
};
export const BsmButton = forwardRef<unknown, Props>(({ className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor, textClassName }, forwardedRef) => {
export const BsmButton = forwardRef<unknown, Props>(({ className, style, iconStyle, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor, textClassName }, forwardedRef) => {
const t = useTranslation();
const { firstColor, secondColor } = useThemeColor();
const ref = useRef<HTMLDivElement>(null);
@@ -86,10 +87,10 @@ export const BsmButton = forwardRef<unknown, Props>(({ className, style, imgClas
return (
<div ref={setRef} onClick={handleClick} title={t(title)} className={`${className} overflow-hidden group ${!withBar && !disabled && (!!typeColor || !!color) && "hover:brightness-[1.15]"} ${disabled ? "brightness-75 cursor-not-allowed" : "cursor-pointer"} ${renderTypeColor}`} style={{ ...style, backgroundColor: primaryColor || color }}>
{image && <BsmImage image={image} className={imgClassName} />}
{icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"} style={{ color: iconColor || textColor }} />}
{icon && <BsmIcon icon={icon} className={iconClassName ?? "size-full text-gray-800 dark:text-white"} style={{ ...(iconStyle ?? {}), color: (iconColor || textColor) }} />}
{text &&
(type === "submit" ? (
<button type="submit" className={textClassName || "h-full w-full"} style={{ ...(!!textColor && { color: textColor }) }}>
<button type="submit" className={textClassName || "size-full"} style={{ ...(!!textColor && { color: textColor }) }}>
{t(text)}
</button>
) : (
@@ -99,8 +100,8 @@ export const BsmButton = forwardRef<unknown, Props>(({ className, style, imgClas
))}
{withBar && (
<div className="absolute bottom-0 left-0 w-full h-1 bg-current" style={{ color: secondColor }}>
<div className="absolute top-0 left-0 h-full w-full bg-current brightness-50" />
<div className={`absolute top-0 left-0 h-full w-full bg-inherit -translate-x-full group-hover:translate-x-0 transition-transform shadow-center shadow-current ${active && "translate-x-0"}`} />
<div className="absolute top-0 left-0 size-full bg-current brightness-50" />
<div className={`absolute top-0 left-0 size-full bg-inherit -translate-x-full group-hover:translate-x-0 transition-transform shadow-center shadow-current ${active && "translate-x-0"}`} />
</div>
)}
</div>
@@ -1,9 +1,10 @@
import { forwardRef, LegacyRef, useImperativeHandle, useRef, useState } from "react";
import { BsmIconType, BsmIcon } from "../svgs/bsm-icon.component";
import { BsmButton } from "./bsm-button.component";
import { BsmButton, BsmButtonType } from "./bsm-button.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { AnimatePresence } from "framer-motion";
import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
import { cn } from "renderer/helpers/css-class.helpers";
export interface DropDownItem {
text: string;
@@ -11,8 +12,17 @@ export interface DropDownItem {
onClick?: () => void;
}
type ClassNames = {
mainContainer?: string;
button?: string;
itemsContainer?: string;
iconClassName?: string;
}
type Props = {
className?: string;
classNames?: ClassNames;
buttonColor?: BsmButtonType;
items?: DropDownItem[];
align?: "left" | "right" | "center";
withBar?: boolean;
@@ -24,7 +34,7 @@ type Props = {
textClassName?: string;
};
export const BsmDropdownButton = forwardRef(({ className, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY, children, text, textClassName }: Props, fowardRed) => {
export const BsmDropdownButton = forwardRef(({ className, classNames, buttonColor, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY, children, text, textClassName }: Props, fowardRed) => {
const [expanded, setExpanded] = useState(false);
const t = useTranslation();
const ref = useRef(fowardRed);
@@ -63,9 +73,9 @@ export const BsmDropdownButton = forwardRef(({ className, items, align, withBar
})();
return (
<div ref={ref as unknown as LegacyRef<HTMLDivElement>} className={className}>
<BsmButton onClick={() => setExpanded(!expanded)} className={buttonClassName ?? defaultButtonClassName} icon={icon} active={expanded} textClassName={textClassName} onClickOutside={handleClickOutside} withBar={withBar} text={text} />
<div className={`py-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] duration-150 ease-in-out ${alignClass}`} style={{ scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}` }}>
<div ref={ref as unknown as LegacyRef<HTMLDivElement>} className={cn(className, classNames?.mainContainer)}>
<BsmButton onClick={() => setExpanded(!expanded)} className={cn(buttonClassName ?? defaultButtonClassName, classNames?.button)} icon={icon} active={expanded} textClassName={textClassName} onClickOutside={handleClickOutside} withBar={withBar} text={text} typeColor={buttonColor} iconClassName={classNames?.iconClassName}/>
<div className={cn(`py-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] duration-150 ease-in-out ${alignClass}`, classNames?.itemsContainer)} style={{ scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}` }}>
{items?.map(
i =>
i && (
@@ -50,6 +50,7 @@ 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";
import { WebSiteIcon } from "./icons/web-site-icon.component";
import { GermanIcon } from "./flags/german-icon.component";
import { RussianIcon } from "./flags/russian-icon.component";
import { ChineseIcon } from "./flags/chinese-icon.component";
@@ -61,7 +62,8 @@ import { ShortcutIcon } from "./icons/shortcut-icon.component";
import { BackupRestoreIcon } from "./icons/backup-restore-icon.component";
import { SongDetailDiffCharactertistic } from "shared/models/maps/song-details-cache/song-details-cache.model";
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "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" | "shortcut" | "backup-restore" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
export type BsmIconType = SongDetailDiffCharactertistic | ("settings" | "trash" | "favorite" | "folder" | "bsNote" | "check" | "three-dots" | "twitch" | "eye" | "play" | "checkCircleIcon" | "discord" | "info" | "eye-cross" | "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" | "shortcut" | "backup-restore" | "web-site" | "fr-FR-flag" | "es-ES-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag");
export const BsmIcon = memo(({ className, icon, style }: { className?: string; icon: BsmIconType; style?: CSSProperties }) => {
// TODO : Very ugly very messy, need to find a better way to do this
@@ -250,7 +252,13 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
if (icon === "backup-restore") {
return <BackupRestoreIcon className={className} style={style} />;
}
if(icon === "web-site") {
return <WebSiteIcon className={className} style={style} />;
}
return <TrashIcon className={className} style={style} />;
};
return <>{renderIcon()}</>;
@@ -0,0 +1,9 @@
import { CSSProperties } from "react";
export function WebSiteIcon(props: { className?: string; style?: CSSProperties }) {
return (
<svg className={props.className} style={props.style} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M480-80q-82 0-155-31.5t-127.5-86Q143-252 111.5-325T80-480q0-83 31.5-155.5t86-127Q252-817 325-848.5T480-880q83 0 155.5 31.5t127 86q54.5 54.5 86 127T880-480q0 82-31.5 155t-86 127.5q-54.5 54.5-127 86T480-80Zm0-82q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/>
</svg>
);
}
@@ -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>
@@ -1,7 +1,7 @@
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { Mod } from "shared/models/mods/mod.interface";
import { CSSProperties, MouseEvent, useRef } from "react";
import { CSSProperties, MouseEvent, useMemo, useRef } from "react";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { useObservable } from "renderer/hooks/use-observable.hook";
@@ -9,6 +9,7 @@ import { PageStateService } from "renderer/services/page-state.service";
import useDoubleClick from "use-double-click";
import { gt } from "semver";
import { useService } from "renderer/hooks/use-service.hook";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
type Props = { className?: string; mod: Mod; installedVersion: string; isDependency?: boolean; isSelected?: boolean; onChange?: (val: boolean) => void; wantInfo?: boolean; onWantInfo?: (mod: Mod) => void };
@@ -20,6 +21,8 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
const uninstalling = useObservable(() => modsManager.isUninstalling$);
const clickRef = useRef();
const isChecked = useMemo(() => isDependency || isSelected || mod.required, [isDependency, isSelected, mod.required]);
useDoubleClick({
onSingleClick: e => handleWantInfo(e),
onDoubleClick: e => handleOnChange(e),
@@ -27,6 +30,10 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
latency: 175,
});
useOnUpdate(() => {
onChange(isChecked);
}, [isChecked]);
const wantInfoStyle: CSSProperties = wantInfo ? { borderColor: themeColor } : { borderColor: "transparent" };
const isOutDated = installedVersion ? gt(mod.version, installedVersion) : false;
@@ -34,21 +41,19 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
modsManager.uninstallMod(mod, pageState.getState());
};
const handleWantInfo = (e: MouseEvent<Element, MouseEvent>) => {
const handleWantInfo = (e: MouseEvent) => {
e.preventDefault();
onWantInfo(mod);
};
const handleOnChange = (e: MouseEvent<Element, MouseEvent>) => {
const handleOnChange = (e: MouseEvent) => {
e.preventDefault();
onChange(!isChecked);
};
const isChecked = isDependency || isSelected || mod.required;
return (
<li ref={clickRef} className={`${className} group`}>
<div className="h-full aspect-square flex items-center justify-center p-[7px] rounded-l-md bg-inherit ml-3 border-2 border-r-0 z-[1] group-hover:brightness-90" style={wantInfoStyle}>
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={onChange} disabled={mod.required || isDependency} checked={isChecked} />
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.required || isDependency} checked={isChecked} />
</div>
<span className="bg-inherit py-2 pl-3 font-bold text-sm whitespace-nowrap border-t-2 border-b-2 blur-none group-hover:brightness-90" style={wantInfoStyle}>
{mod.name}
@@ -12,7 +12,7 @@ import { useService } from "renderer/hooks/use-service.hook";
type Props = { modsMap: Map<string, Mod[]>; installed: Map<string, Mod[]>; modsSelected: Mod[]; onModChange: (selected: boolean, mod: Mod) => void; moreInfoMod?: Mod; onWantInfos: (mod: Mod) => void };
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos }: Props) {
const pageState = useService(PageStateService);
const modsManager = useService(BsModsManagerService);
@@ -61,8 +61,8 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
return (
modsMap && (
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_1fr_min-content] bg-light-main-color-2 dark:bg-main-color-2 text-main-color-1 dark:text-light-main-color-1">
<span className="absolute z-10 top-0 w-full h-8 bg-inherit" />
<span className="z-10 sticky flex items-center justify-end top-0 bg-inherit border-b-2 border-main-color-1">
<span className="absolute z-10 top-0 w-full h-8 bg-inherit rounded-tl-md" />
<span className="z-10 sticky flex items-center justify-end top-0 bg-inherit border-b-2 border-main-color-1 rounded-tl-md">
<BsmButton className="rounded-full h-6 w-6 p-[2px]" withBar={false} icon="search" onClick={handleToogleFilter} />
</span>
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1 whitespace-nowrap">{filterEnabled ? <motion.input autoFocus className="bg-main-color-1 rounded-md h-6 px-2" initial={{ width: 0 }} animate={{ width: "250px" }} transition={{ ease: "easeInOut", duration: 0.15 }} onChange={e => handleInput(e.target.value)} /> : <span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>}</span>
@@ -19,12 +19,14 @@ import { ModsDisclaimerModal } from "renderer/components/modal/modal-types/mods-
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
import { lt } from "semver";
import { useService } from "renderer/hooks/use-service.hook";
import { NotificationService } from "renderer/services/notification.service";
export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; onDisclamerDecline: () => void }) {
const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer";
const modsManager = useService(BsModsManagerService);
const configService = useService(ConfigurationService);
const notification = useService(NotificationService);
const linkOpener = useService(LinkOpenerService);
const modals = useService(ModalService);
const os = useService(OsDiagnosticService);
@@ -35,6 +37,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
const [modsInstalled, setModsInstalled] = useState(null as Map<string, Mod[]>);
const [modsSelected, setModsSelected] = useState([] as Mod[]);
const [moreInfoMod, setMoreInfoMod] = useState(null as Mod);
const [reinstallAllMods, setReinstallAllMods] = useState(false);
const isOnline = useObservable(() => os.isOnline$);
const installing = useObservable(() => modsManager.isInstalling$);
@@ -51,15 +54,17 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
};
const handleModChange = (selected: boolean, mod: Mod) => {
if (selected) {
return setModsSelected([...modsSelected, mod]);
return setModsSelected(mods => {
if (mods.some(m => m.name === mod.name)) {
return mods;
}
return [...mods, mod];
});
}
const mods = [...modsSelected];
mods.splice(
mods.findIndex(m => m.name === mod.name),
1
);
setModsSelected(mods);
setModsSelected(mods => mods.filter(m => m.name !== mod.name));
};
const handleMoreInfo = (mod: Mod) => {
@@ -76,25 +81,28 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
linkOpener.open(moreInfoMod.link);
};
const installMods = () => {
const installMods = (reinstallAll: boolean): void => {
setReinstallAllMods(() => false);
if (installing) {
return;
}
const modsToInstall = modsSelected.filter(mod => {
const corespondingMod = modsAvailable.get(mod.category).find(availabeMod => availabeMod._id === mod._id);
const installedMod = modsInstalled.get(mod.category)?.find(installedMod => installedMod.name === mod.name);
if (corespondingMod?.version && lt(corespondingMod.version, mod.version)) {
return false;
}
if(reinstallAll || !installedMod){ return true; }
if(installedMod?.version && lt(mod.version, installedMod?.version)){
return false;
}
return true;
return lt(installedMod.version, mod.version);
});
if (!modsToInstall.length) {
notification.notifyInfo({ title: "Mods déjà installées", desc: "Tous les mods séléctionnées sont déjà installées" });
loadMods();
return;
}
modsManager.installMods(modsToInstall, version).then(() => {
loadMods();
});
@@ -112,7 +120,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
}
return promise().then(([available, installed]) => {
const defaultMods = configService.get<string[]>("default_mods" as DefaultConfigKey);
const defaultMods = installed?.length ? [] : configService.get<string[]>("default_mods" as DefaultConfigKey);
setModsAvailable(() => modsToCategoryMap(available));
setModsSelected(() => available.filter(m => m.required || defaultMods.some(d => m.name.toLowerCase() === d.toLowerCase()) || installed.some(i => m.name === i.name)));
setModsInstalled(() => modsToCategoryMap(installed));
@@ -186,13 +194,17 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
}
return (
<>
<div className="grow overflow-scroll w-full min-h-0 scrollbar-thin scrollbar-thumb-neutral-900 scrollbar-thumb-rounded-full">
<div className="grow overflow-y-scroll w-full min-h-0 scrollbar-default p-0 m-0">
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo} />
</div>
<div className="h-10 shrink-0 flex items-center justify-between px-3">
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{ width: downloadWith }} />
<div ref={downloadRef}>
<BsmButton className="text-center rounded-md px-2 py-[2px]" text="pages.version-viewer.mods.buttons.install-or-update" withBar={false} disabled={installing} typeColor="primary" onClick={installMods} />
<div className="shrink-0 flex items-center justify-between px-3 py-2">
<BsmButton className="flex items-center justify-center rounded-md px-1 h-8" text="pages.version-viewer.mods.buttons.more-infos" typeColor="cancel" withBar={false} disabled={!moreInfoMod} onClick={handleOpenMoreInfo} style={{ width: downloadWith }}/>
<div ref={downloadRef} className="flex h-8 justify-center items-center gap-px overflow-hidden rounded-md">
<div className="grow h-full relative">
<BsmButton className="relative left-0 flex items-center justify-center px-2 size-full transition-[top] duration-200 ease-in-out" text="pages.version-viewer.mods.buttons.install-or-update" typeColor="primary" withBar={false} onClick={() => installMods(false)} style={{ top: reinstallAllMods ? "-100%" : "0" }} />
<BsmButton className="relative left-0 flex items-center justify-center px-2 size-full transition-[top] duration-200 ease-in-out " text="pages.version-viewer.mods.buttons.reinstall-all" typeColor="primary" withBar={false} onClick={() => installMods(true)} style={{ top: reinstallAllMods ? "-100%" : "0" }}/>
</div>
<BsmButton className="flex items-center justify-center shrink-0 h-full" iconClassName="transition-transform size-full ease-in-out duration-200" iconStyle={{ transform: reinstallAllMods ? "rotate(360deg)" : "rotate(180deg)" }} icon="chevron-top" typeColor="primary" withBar={false} onClick={() => setReinstallAllMods(prev => !prev)}/>
</div>
</div>
</>
@@ -201,7 +213,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
return (
<div ref={ref} className="shrink-0 w-full h-full px-8 pb-7 flex justify-center">
<div className="relative flex flex-col grow-0 bg-light-main-color-2 dark:bg-main-color-2 h-full w-full rounded-md shadow-black shadow-center overflow-hidden">{renderContent()}</div>
<div className="relative flex flex-col grow-0 bg-light-main-color-2 dark:bg-main-color-2 size-full rounded-md shadow-black shadow-center overflow-hidden">{renderContent()}</div>
</div>
);
}
+15 -4
View File
@@ -79,10 +79,21 @@
}
.scrollbar-default {
/* scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 */
@apply scrollbar-thin;
@apply scrollbar-thumb-rounded-full;
@apply scrollbar-thumb-neutral-900;
&::-webkit-scrollbar {
width: 4px;
height: 4px;
}
&::-webkit-scrollbar-track {
background-color: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgb(23 23 23 / var(--tw-bg-opacity));
border-radius: 999px;
}
}
.tippy-box[data-theme~='default'] {
+8
View File
@@ -62,4 +62,12 @@ export function logRenderError(...params: unknown[]){
ipc.sendLazy("log-error", { args: params });
}
export function addFilterStringLog(str: string){
ipc.sendLazy("add-filter-string", { args: str });
}
export function addFilterPatternLog(pattern: string){
ipc.sendLazy("add-filter-pattern", { args: pattern });
}
@@ -113,7 +113,10 @@ export function SettingsPage() {
};
const loadInstallationFolder = () => {
steamDownloader.getInstallationFolder().then(res => setInstallationFolder(res));
steamDownloader.getInstallationFolder().then(res => {
console.log("AAAAA", res);
setInstallationFolder(res);
});
};
const loadDownloadersSession = () => {
@@ -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();
@@ -11,6 +11,7 @@ import { AbstractBsDownloaderService } from "./abstract-bs-downloader.service";
import { DownloadInfo } from "main/services/bs-version-download/bs-steam-downloader.service";
import { MetaAuthErrorCodes, OculusDownloaderErrorCodes } from "shared/models/bs-version-download/oculus-download.model";
import { EnterMetaTokenModal } from "renderer/components/modal/modal-types/bs-downgrade/enter-meta-token-modal.component";
import { addFilterStringLog } from "renderer";
export class OculusDownloaderService extends AbstractBsDownloaderService implements DownloaderServiceInterface{
@@ -88,6 +89,8 @@ export class OculusDownloaderService extends AbstractBsDownloaderService impleme
return false;
}
addFilterStringLog(tokenRes.data);
return lastValueFrom(this.startDownloadBsVersion({ bsVersion, isVerification, token: tokenRes.data })).then(() => true);
})().then(res => {
@@ -13,6 +13,7 @@ import { DepotDownloaderErrorEvent, DepotDownloaderEvent, DepotDownloaderEventTy
import { SteamMobileApproveModal } from "renderer/components/modal/modal-types/bs-downgrade/steam-mobile-approve-modal.component";
import { DownloaderServiceInterface } from "./bs-store-downloader.interface";
import { AbstractBsDownloaderService } from "./abstract-bs-downloader.service";
import { addFilterStringLog } from "renderer";
export class SteamDownloaderService extends AbstractBsDownloaderService implements DownloaderServiceInterface{
@@ -219,6 +220,10 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
return Promise.resolve();
}
if(loginRes?.data?.password){
addFilterStringLog(loginRes.data.password);
}
if(loginRes.data.stay){
this.setSteamSession(loginRes.data.username);
}
@@ -62,7 +62,7 @@ export default function OneClickDownloadPlaylist() {
<TitleBar template="oneclick-download-playlist.html" />
<BsmImage className="mt-2 aspect-square w-1/2 object-cover rounded-md shadow-black shadow-lg" placeholder={defaultImage} image={playlistImage()} errorImage={defaultImage} />
<h1 className="mt-4 overflow-hidden font-bold italic text-xl text-gray-200 tracking-wide w-full text-center whitespace-nowrap text-ellipsis px-2">{playlistInfos?.playlistTitle}</h1>
<div className="w-full py-3 flex items-center justify-center max-w-full overflow-x-scroll overflow-y-hidden scrollbar scrollbar-thin scrollbar-track-transparent scrollbar-thumb-neutral-900" ref={mapsContainer}>
<div className="w-full py-3 flex items-center justify-center max-w-full overflow-x-scroll overflow-y-hidden scrollbar-default" ref={mapsContainer}>
<div className="flex justify-start items-start gap-2.5">{downloadedMaps?.map(map => map?.coverUrl &&
<motion.img layout="position" key={map.hash} className="block aspect-square w-14 object-cover rounded-md shadow-black shadow-md" src={map?.coverUrl} initial={{ scale: 0 }} animate={{ scale: 1 }} whileHover={{ rotate: 5 }} />
)}</div>
+13
View File
@@ -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;
}
+1 -1
View File
@@ -190,7 +190,7 @@ export enum MapType {
Accuracy = "accuracy",
Balanced = "balanced",
Challenge = "challenge",
Dancestyle = "dancestyle",
Dancestyle = "dance-style",
Fitness = "fitness",
Speed = "speed",
Tech = "tech"
+1 -1
View File
@@ -12,7 +12,7 @@ module.exports = {
darkMode: "class",
content: ["./src/renderer/**/*.{js,jsx,ts,tsx,ejs}", "./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}"],
mode: "jit",
plugins: [require("tailwind-scrollbar-hide"), require("tailwind-scrollbar")({ nocompatible: true }), require("tailwindcss-scoped-groups")({ groups: ["one"] }), require("ps-scrollbar-tailwind"), nextui()],
plugins: [require("tailwindcss-scoped-groups")({ groups: ["one"] }), nextui()],
theme: {
colors: {
...colors,