mirror of
https://github.com/Zagrios/bs-manager.git
synced 2026-07-03 14:08:25 +02:00
Compare commits
59 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb30400b7c | |||
| 0fd1e211ba | |||
| 64484bfd4e | |||
| 499956d3b3 | |||
| c313685f6f | |||
| 2e750f3d5a | |||
| e1c3115668 | |||
| a6462530bb | |||
| 8316a9ef80 | |||
| 152313289c | |||
| c96ad6f7cd | |||
| b7cf3c5d20 | |||
| 97d3c2a3ae | |||
| a20ebe2075 | |||
| 2f5a74b155 | |||
| a20c73d0e9 | |||
| aeca1c1e9e | |||
| b9ce95968c | |||
| 490ccfdc1e | |||
| 6d0c6b4f64 | |||
| 89c3b0906b | |||
| 8cd74c3b0b | |||
| 769dab2896 | |||
| 8f1f998c5a | |||
| 48d03cccbb | |||
| 7d54ae79f7 | |||
| c46fdd56f6 | |||
| 22dc1ca511 | |||
| 709105b9f4 | |||
| 1c0cfb6b89 | |||
| 7ebc1bff16 | |||
| 0e1cda5050 | |||
| 331626bd1e | |||
| ea62f2443f | |||
| 532ab08e46 | |||
| a6fa55719d | |||
| 163857a14f | |||
| 854e0e2761 | |||
| eba6f916a3 | |||
| 9790f35749 | |||
| 93d3cbc8bf | |||
| 51ea1b130b | |||
| d020b72f20 | |||
| f2ac952cd9 | |||
| 189d2388fb | |||
| 1c989a2402 | |||
| 959e60127d | |||
| 0faa0f1288 | |||
| 5ad25f58d5 | |||
| 8ace5e6a7a | |||
| 185d01e7b4 | |||
| 755e407372 | |||
| 4f123b69ef | |||
| 7e2334f8fc | |||
| 5be533ccfb | |||
| b8eafd5f9b | |||
| 3f3bb959c4 | |||
| c4d5db809b | |||
| 8ecc9d30b3 |
@@ -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);
|
||||
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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": "技術的"
|
||||
|
||||
@@ -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": "техника"
|
||||
|
||||
@@ -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": "技術"
|
||||
|
||||
@@ -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": "技术"
|
||||
|
||||
Generated
+531
-570
File diff suppressed because it is too large
Load Diff
+22
-24
@@ -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"
|
||||
},
|
||||
|
||||
Generated
+2
-2
@@ -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": {
|
||||
|
||||
@@ -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
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
|
||||
@@ -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
-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 (
|
||||
|
||||
+2
-2
@@ -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>
|
||||
</>
|
||||
</>
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
+5
-1
@@ -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>
|
||||
|
||||
+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 (
|
||||
|
||||
@@ -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
@@ -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'] {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user