Compare commits

...

122 Commits

Author SHA1 Message Date
MathieuG-P 87f134ebb5 Merge pull request #609 from Zagrios/bugfix/update-depot-downloader
[bugfix] update DepotDownloader to fix connection to steam errors
2024-10-14 13:04:50 +02:00
MathieuG-P 8fe346b1a7 [bugfix] update depotdownloader to fix connection to steam errors 2024-10-13 19:57:43 +02:00
MathieuG-P e9ca4bb7ac For certificate verification 2024-10-13 11:19:27 +02:00
MathieuG-P eac6192afb nothing 2024-10-12 02:04:04 +02:00
MathieuG-P 679b28e44f [chore] new code sign certificate 2024-10-12 02:02:29 +02:00
MathieuG-P 36e95d0d96 1.38.0 as recommended 2024-10-11 20:33:53 +02:00
Iluhadesu 59bd480445 Add v1.38.0 to bs-versions.json 2024-10-08 15:24:05 +00:00
MathieuG-P d38a3b5720 Merge pull request #605 from Zagrios/feature/593/feat-cleanup-duplicate-songs
[feature-593] We can now delete duplicates maps
2024-10-06 18:42:28 +02:00
MathieuG-P 9d59bfb956 [feature-593] We can now delete duplicates maps 2024-10-06 18:22:28 +02:00
MathieuG-P b97a58c278 Merge pull request #604 from Zagrios/bugfix/602/bug-error-linking-playlist-folder
[bugfix-602] fix link playlists error
2024-10-05 17:38:36 +02:00
MathieuG-P 6f55f4283d Merge pull request #598 from silentrald/chore/improve-feature-request
[chore] improved user experience for creating feature request
2024-10-05 17:38:23 +02:00
MathieuG-P 9ee33f2c7d Merge pull request #599 from silentrald/chore/linux-labeler
[chore] properly add correct label for linux
2024-10-05 17:37:47 +02:00
MathieuG-P 55f45e438c [bugfix-602] fix link playlists error 2024-10-05 17:29:34 +02:00
silentrald cce6b71993 [chore] improved user experience for creating feature request 2024-10-05 08:52:25 +08:00
silentrald 3d24e97abd [chore] properly add correct label for linux 2024-10-02 08:50:18 +08:00
MathieuG-P 1eea031438 Merge pull request #596 from silentrald/chore/improve-bug-reporting
[chore] improved user experience for bug reporting
2024-10-01 21:10:44 +02:00
MathieuG-P dcb45927a1 Merge pull request #597 from Zagrios/chore/remove-unnecessary-code-from-progress-bar-service
[chore] remove unnecessary code from progress bar service
2024-10-01 21:01:37 +02:00
MathieuG-P d234a504b5 Merge pull request #595 from silentrald/chore/discord-support-link
[chore] added discord link in issue template
2024-10-01 20:57:39 +02:00
MathieuG-P cac09cf032 [chore] remove unnecessary code from progress bar service 2024-10-01 20:51:53 +02:00
silentrald 6d6f7d492e [chore] improved user experience for bug reporting 2024-09-30 09:13:08 +08:00
silentrald 01bae7c59b [chore] added discord link in issue template 2024-09-30 08:25:48 +08:00
MathieuG-P db5af48250 [chore] remove unused functions 2024-09-29 19:46:53 +02:00
MathieuG-P 0576ea0159 Merge pull request #594 from Zagrios/chore/some-reworks-to-remove-watch-functions
[chore] Some reworks to remove watch functions
2024-09-29 19:41:59 +02:00
MathieuG-P f73ab4a6a4 [chore-594] remove unused import 2024-09-29 18:37:54 +02:00
MathieuG-P 501c0d9ae1 [chore] Some reworks to remove watch functions 2024-09-29 18:29:59 +02:00
MathieuG-P cc810ae84e Merge pull request #590 from silentrald/bugfix/585
[bugfix-585] fixed unicode song titles not showing in game
2024-09-28 13:09:26 +02:00
MathieuG-P 3533402197 Merge pull request #589 from silentrald/bugfix/broken-app-icon
[bugfix] show bsm icon properly
2024-09-28 12:22:53 +02:00
silentrald 03a8377773 [bugfix-585] fixed unicode song titles not showing in game
Added SteamEnv=1 in environment variable when launching the "Beat
Saber.exe" file
2024-09-25 21:46:09 +08:00
silentrald 138002d7a0 [bugfix] show bsm icon properly 2024-09-23 20:58:20 +08:00
MathieuG-P 18854650ab Merge pull request #588 from kevin-wijnen/master
[feature] Automating the wiki via GitHub Actions
2024-09-22 15:50:36 +02:00
MathieuG-P e51d47ac73 Merge pull request #587 from silentrald/feat/543
[feat-543] added notification when logging out
2024-09-22 15:42:27 +02:00
MathieuG-P 32bf20a55f [feature-543] add translations 2024-09-22 15:36:23 +02:00
Kevin Wijnen b702b33def Merge branch 'Zagrios:master' into master 2024-09-22 14:55:02 +02:00
MathieuG-P d74a730a04 [chore] update deps 2024-09-22 14:54:02 +02:00
Kevin Wijnen 793af822ce [docs] Specify how to contribute to the wiki 2024-09-22 14:33:42 +02:00
silentrald c05b9a4b8e [feat-543] added notification when logging out 2024-09-22 20:32:58 +08:00
Kevin Wijnen 883f7594b0 [bugfix] Trigger path for wiki automation 2024-09-22 14:32:23 +02:00
Kevin Wijnen 7161f0861e [bugfix] Target path for wiki automation 2024-09-22 14:30:28 +02:00
Kevin Wijnen 5f35a43098 [chore] Trigger wiki automation 2024-09-22 14:22:48 +02:00
Kevin Wijnen 63ef3acc72 [bugfix] Fix target branch wiki automation 2024-09-22 14:21:28 +02:00
Kevin Wijnen 3ff3c5af67 [docs] Add explainer for wiki automation 2024-09-22 14:19:23 +02:00
Kevin Wijnen 50ebef30fb Merge pull request #1 from kevin-wijnen/github-actions-docs
GitHub actions docs
2024-09-22 14:16:54 +02:00
Kevin Wijnen 6d6da765a6 [chore] wiki article migration 2024-09-22 14:14:44 +02:00
Kevin Wijnen 031312e90e Merge branch 'Zagrios:master' into github-actions-docs 2024-09-22 14:07:16 +02:00
Kevin Wijnen f0e32bdae1 [chore] Fix spelling 2024-09-22 14:06:00 +02:00
Kevin Wijnen 81071d920e [feature] GitHub Actions wiki automation
Initial concept for automating the BSManager Wiki via PR's to the docs/wiki folder.

Requires locking the Wiki's writing to none of the public, to prevent vandalism, desyncs and losing wiki-only information, with what is on the repository.
2024-09-22 14:05:03 +02:00
MathieuG-P ea320c0e08 remove lowerCase 2024-09-22 14:02:47 +02:00
MathieuG-P 8335494a9f Merge branch 'master' of https://github.com/Zagrios/bs-manager 2024-09-22 13:59:31 +02:00
MathieuG-P 96fc2b4efb Add labeler workflow for Linux issues 2024-09-22 13:59:28 +02:00
MathieuG-P 3280ca1984 Merge pull request #584 from silentrald/bugfix/modal-options-undefined
[bugfix] handle undefined modal options properly
2024-09-21 21:07:24 +02:00
silentrald aae23c18d6 [bugfix] handle undefined modal options properly 2024-09-21 22:46:54 +08:00
MathieuG-P 7a8eee9266 Merge pull request #582 from Zagrios/bugfix/playlist-download-status-not-updating
[bugfix] Downloading a playlist from its details modal was not updati…
2024-09-20 11:01:16 +02:00
MathieuG-P 757069d657 [bugfix] Downloading a playlist from its details modal was not updating the playlist's download status in the playlists list 2024-09-20 10:48:49 +02:00
MathieuG-P 15178f0e60 Merge branch 'master' of https://github.com/Zagrios/bs-manager 2024-09-19 21:42:52 +02:00
MathieuG-P 02d0aef497 [bugfix] fix overflow hidden causing weird blury issue 2024-09-19 21:42:50 +02:00
MathieuG-P ee6b49f028 Merge pull request #580 from Zagrios/feature/fix-version-issue
Feature/fix version issue
2024-09-19 13:56:48 +02:00
MathieuG-P 5c383b66a7 Merge pull request #581 from Zagrios/bugfix/487/bug-when-i-clone-the-version-weird-stuff-happens
[bugfix-487] Cloning a version was changing its color even if an error was occuring
2024-09-18 16:38:36 +02:00
MathieuG-P 50cef104f0 [bugfix-487] Cloning a version was changing its color even if an error was occurring 2024-09-18 14:56:34 +02:00
Shidorien 37a44b54f3 Skip showing changelog for alpha versions 2024-09-17 15:31:28 +02:00
Shidorien 0027832bdb Remove posibility to store alpha version in LastAppVersion and clear this var when user already stored an alpha version 2024-09-17 15:20:44 +02:00
MathieuG-P ad4da530eb bumb to 1.5.0-alpha.4 2024-09-16 22:07:15 +02:00
MathieuG-P e4a894f75a [bugfix] Choose content folder was asked after each restart of BSM + add a log 2024-09-16 22:06:59 +02:00
MathieuG-P 1a62ae67c9 add patreon 2024-09-12 21:11:06 +02:00
MathieuG-P 78951f0de1 [chore] temporary add my name in readme for code signing certificate verification 2024-09-11 20:45:58 +02:00
MathieuG-P fd52fb8d78 Merge pull request #566 from silentrald/feat/560
[feat-560] ask user for installation folder/path
2024-09-11 12:45:37 +02:00
MathieuG-P e14ba2727b [feature-560] translations 2024-09-10 21:18:46 +02:00
MathieuG-P f6cd2fc316 Merge pull request #577 from silentrald/bugfix/linux-bs-version-readonly
[bugfix] use static config for version list in linux
2024-09-10 20:25:18 +02:00
silentrald 57f0679f57 [bugfix] use static config for version list in linux 2024-09-10 00:47:25 +08:00
Iluhadesu d407b396cf Add v1.37.5 to bs-versions.json 2024-09-07 01:35:36 +00:00
Iluhadesu 4bfc00d698 Add v1.37.4 to bs-versions.json 2024-09-05 16:02:25 +00:00
silentrald bd371b56e1 [bugfix-560] don't move old installation path if it doesn't exist 2024-09-05 09:37:32 +08:00
MathieuG-P 7ae097b990 [feature-560] some adjustments 2024-09-04 20:45:28 +02:00
silentrald 9734d714d7 [refactor] created installation-location.service.ts on client side 2024-09-04 08:57:19 +08:00
silentrald 9c86adf2d4 [feat-560] applied PR comments 2024-09-04 08:52:43 +08:00
silentrald 6258de5b06 [feat-560] added description text and default button tooltip
added tooltip support in BsmButton component
2024-09-04 08:52:43 +08:00
silentrald d3c797041f [feat-560] delay song details cache loading after user setup is done
added check if ElectronStore is null
2024-09-04 08:52:40 +08:00
silentrald 5e6837b8cd [feat-560] ask user for installation folder/path 2024-09-04 08:21:26 +08:00
MathieuG-P eba2b6fb03 Add patreon 2024-09-02 23:17:03 +02:00
MathieuG-P 1b2b8baa48 Merge pull request #572 from Zagrios/feature/add-advanced-settings
[feature] add advanced settings
2024-09-01 22:58:40 +02:00
MathieuG-P 846945b75a [feature] fix sonarlint issues 2024-09-01 22:55:21 +02:00
MathieuG-P f84758c42d [feature] translate + simplify code 2024-09-01 22:46:08 +02:00
MathieuG-P 55aeab896c [feature] disable hardware acceleration and use symlinks work, just still need to translate 2024-09-01 18:08:03 +02:00
MathieuG-P ac62926f8d [feature] create toogle gid for advanced settings 2024-08-29 22:30:29 +02:00
MathieuG-P 98bfbfa868 remove console log 2024-08-29 19:37:16 +02:00
MathieuG-P 150efb1a5c nothing 2024-08-29 19:34:56 +02:00
Iluhadesu b68c84dd6a Bump Beat Saber recommended version to 1.37.1 2024-08-26 17:05:22 +00:00
MathieuG-P 3145d2fdfa Merge pull request #568 from Zagrios/bugfix/improve-folder-linking-reliability
[bugfix] Improve folder linking reliability
2024-08-25 16:32:22 +02:00
MathieuG-P dac67ebe4e [bugfix] dont move the folder if all subitem in the folder already exist in the destination 2024-08-25 16:21:04 +02:00
MathieuG-P aea325a254 [bugfix] Improve folder linking reliability 2024-08-25 15:03:08 +02:00
MathieuG-P 462499fe81 Merge pull request #567 from silentrald/bugfix/bsipa-linux
[bugfix] wrap dotnet cmdlet's path argument with double quotes for installing bsipa
2024-08-24 15:57:56 +02:00
silentrald f6f69e2f13 [bugfix] wrap dotnet cmdlet's path argument with double quotes for installing bsipa 2024-08-24 21:41:10 +08:00
MathieuG-P c7ac3f2d9c Merge pull request #564 from Zagrios/bugfix/mods-using-manifests-sometimes-was-not-recognised-as-installed
[bugfix] Sometime mods using manifests were not recognised as installed + remove dead code
2024-08-20 21:16:11 +02:00
MathieuG-P cb95d5d88d [bugfix] Sometime mods using manifests were not recognised as installed + remove dead code 2024-08-20 21:06:37 +02:00
MathieuG-P 388ad76780 Merge pull request #563 from Zagrios/bugfix/translate-all-mods-already-notification
[bugfix] translate all mods already notification
2024-08-19 21:12:23 +02:00
MathieuG-P be4afb71e3 Merge pull request #562 from joriskleiber/window-restore-icon
Add window restore icon
2024-08-19 20:35:35 +02:00
MathieuG-P 11ddfc29b5 [bugfix] translate all mods already notification 2024-08-19 20:30:06 +02:00
joriskleiber b1eaad8df0 add window restore icon 2024-08-19 20:20:15 +02:00
Iluhadesu 36fc7cb9e2 Remove the . in the version name once again... 2024-08-19 10:28:48 +00:00
Iluhadesu 8df9516c11 Add v.1.37.3 to bs-versions.json 2024-08-19 10:27:39 +00:00
MathieuG-P 6bd8727067 Merge pull request #559 from kgabriel-dev/chore/improved-german-language/544
Improving the German language file
2024-08-17 21:51:56 +02:00
GabiKleebi02 b620b11eb5 improve German language file 2024-08-12 22:23:09 +02:00
MathieuG-P 3ff3ec7410 Merge pull request #558 from Zagrios/bugfix/466/bsmanager-does-not-handle-mods-with-missing-dependencies-on-beatmods
[bugfix-466] fix potential crashs when beatmods mod has invalid dependencies
2024-08-12 21:02:57 +02:00
MathieuG-P 8f9b3405bc Merge pull request #557 from Zagrios/bugfix/520/bug-unselect-item-when-you-switch-tab
[bugfix-520] clear selected items when switching BS version tab
2024-08-12 20:54:53 +02:00
MathieuG-P 698058710f [bugfix-466] fix potential crashs when beatmods mod has invalid dependencies 2024-08-12 20:53:51 +02:00
MathieuG-P 5fbc69e772 [bugfix-520] clear selected items when switching BS version tab + remove a console log 2024-08-12 20:22:33 +02:00
MathieuG-P daa2f864cc Merge pull request #555 from Insprill/fix/linux-admin
Fix Steam elevated check on Linux
2024-08-12 19:10:34 +02:00
Pierce Thompson f073f66533 Fix Steam elevated check on Linux 2024-08-10 18:00:07 -04:00
MathieuG-P ff9bede111 Merge pull request #554 from Zagrios/bugfix/542/exclude-tag-result-in-javascript-error
[bugfix-542] Fix crash when exclude map tag when browsing for new maps
2024-08-10 15:37:55 +02:00
MathieuG-P 650338d25b [bugfix-542] Fix crash when exclude map tag when browsing for new maps 2024-08-10 15:34:07 +02:00
MathieuG-P be32418b93 nothing 2024-08-10 15:19:02 +02:00
MathieuG-P 3bb9250269 Merge pull request #553 from Zagrios/feature/550/feat-show-any-number-stats-for-unranked-maps-difficulty
[feature-550] When its possible display the NPS ou NJS of the map instead of just the difficulty
2024-08-10 15:16:37 +02:00
MathieuG-P 13da659c0d [feature-550] When its possible display the NPS ou NJS of the map instead of just the difficulty 2024-08-10 15:10:49 +02:00
MathieuG-P 9f4a133b74 Update package-lock.json and release/app/package-lock.json to version 1.5.0-alpha.3 2024-08-07 10:53:24 +02:00
MathieuG-P 25b647630f [chore] add step to install correct Node version in the contributing guide 2024-08-07 10:53:12 +02:00
Iluhadesu 373c6b979d Correct version typo because they can't type 2024-08-06 12:56:57 +00:00
Iluhadesu ded5b58020 Add v.1.37.2 to bs-versions.json 2024-08-06 12:55:33 +00:00
Zagrios 763b3c65f5 fix depotdownload on linux again 2024-08-04 15:20:12 +02:00
Zagrios ef69e64ad7 all file in script is binary 2024-08-04 15:13:56 +02:00
Zagrios d82b7bab54 fix depotdownloader on linux 2024-08-04 14:59:11 +02:00
Zagrios 095ceccc19 should fix depotdownload on linux 2024-08-04 14:36:06 +02:00
MathieuG-P 32e5ee98ad Merge pull request #547 from silentrald/fix/issue-546
[bugfix] exclude regedit-rs on externals when webpack bundles it in linux
2024-08-04 11:19:34 +02:00
silentrald b2a57ea307 [bugfix] exclude regedit-rs on externals when webpack bundles it in linux 2024-08-03 12:00:03 +08:00
109 changed files with 2676 additions and 995 deletions
+13 -1
View File
@@ -6,8 +6,20 @@ import webpack from "webpack";
import webpackPaths from "./webpack.paths";
import { dependencies as externals } from "../../release/app/package.json";
function createExternals(): string[] {
const webpackExternals: string[] = [...Object.keys(externals || {})];
const excludedExternals: string[] = [];
if (process.platform === "linux") {
// Linux only uses regedit-rs types
excludedExternals.push("regedit-rs");
}
return webpackExternals.filter(external => !excludedExternals.includes(external));
}
const configuration: webpack.Configuration = {
externals: [...Object.keys(externals || {})],
externals: createExternals(),
stats: "errors-only",
+1
View File
@@ -15,3 +15,4 @@
*.ttf binary
*.woff binary
*.woff2 binary
assets/scripts/* binary
@@ -1,35 +0,0 @@
---
name: "[BUG] Bug report"
about: Create a report to help us improve
title: "[BUG] : "
labels: bug
assignees: Zagrios
---
## Bug Description
<!-- A clear and concise description of what the bug is. -->
## Reproduction Steps
<!-- Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
-->
## Expected Behavior
<!-- A clear and concise description of what you expected to happen. -->
## Screenshots
<!-- If applicable, add screenshots to help explain your problem. -->
## System Specs
<!-- **Desktop (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 22]
-->
## Additional context
<!-- Add any other context about the problem here. -->
@@ -1,22 +0,0 @@
---
name: "[FEAT.] Feature request"
about: Suggest an idea for this project
title: "[FEAT.] : "
labels: enhancement
assignees: Zagrios
---
## Problem
<!-- **Is your feature request related to a problem? Please describe.** -->
<!-- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->
## Solution
<!-- **Describe the solution you'd like** -->
<!-- A clear and concise description of what you want to happen. -->
## Alternative solutions (if any)
<!-- **Describe alternatives you've considered** -->
<!-- A clear and concise description of any alternative solutions or features you've considered. -->
## Additional context
<!-- Add any other context or screenshots about the feature request here. -->
+54
View File
@@ -0,0 +1,54 @@
name: "[BUG] Bug report"
description: Create a report to help us improve
title: "[BUG] : "
labels: bug
assignees: Zagrios
body:
- type: markdown
attributes:
value: |
## Thanks for taking the time to fill out this bug report! 😊
- type: textarea
id: description
attributes:
label: Issue encountered
description: Tell us what issue you've encountered.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected Behavior
description: Tell us what should expected to happen.
- type: textarea
id: replication
attributes:
label: Steps to Reproduce
description: Provide a link to a live example, or an unambiguous set of steps to reproduce this bug. Include code to reproduce, if relevant.
placeholder: |
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
- type: dropdown
id: os
attributes:
label: Operating System
options:
- Windows 10
- Windows 11
- Linux
- type: input
id: version
attributes:
label: Version
description: What version of BSManager are you running?
placeholder: eg. 1.4.8 or 1.5.0-alpha-4
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Any other context that you may share about the issue. You may add your log files here.
@@ -0,0 +1,45 @@
name: "[FEAT.] Feature request"
description: Suggest an idea for this project
title: "[FEAT.] : "
labels: enhancement
assignees: Zagrios
body:
- type: markdown
attributes:
value: |
## Thanks for taking the time to fill out this feature request! 😊
- type: textarea
id: problem
attributes:
label: Problem
description: Is your feature request related to a problem? Please describe.
placeholder: Ex. I'm always frustrated when [...]
validations:
required: true
- type: textarea
id: solution
attributes:
label: Solution
description: Describe the solution you'd like.
validations:
required: true
- type: textarea
id: alternative-solution
attributes:
label: Alternative Solution
description: Describe alternatives you've considered.
- type: dropdown
id: os
attributes:
label: Operating System
description: If the feature is only applicable to a specific OS. Leave it to `None` if it's applicable to all OS'es.
options:
- Windows
- Linux
- type: textarea
id: additional-context
attributes:
label: Additional Context
description: Add any other context or screenshots about the feature request here.
+5
View File
@@ -0,0 +1,5 @@
lank_issues_enabled: true
contact_links:
- name: Discord Support
url: https://discord.gg/uSqbHVpKdV
about: You can join our Discord server for a quick and interactive support.
+20
View File
@@ -0,0 +1,20 @@
name: Publish wiki
on:
push:
branches: [master]
paths:
- docs/wiki/**
- .github/workflows/docs.yml
concurrency:
group: publish-wiki
cancel-in-progress: true
permissions:
contents: write
jobs:
publish-wiki:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: Andrew-Chen-Wang/github-wiki-action@v4
with:
path: docs/wiki/
+26
View File
@@ -0,0 +1,26 @@
name: Labeler
on:
issues:
types: [opened, edited]
jobs:
label-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Search Linux Term
uses: actions-ecosystem/action-regex-match@v2
id: regex-match
with:
text: ${{ github.event.issue.body }}
regex: '\# Operating System(\s*)Linux'
flags: m
- name: Add Linux label
uses: actions-ecosystem/action-add-labels@v1
if: ${{ steps.regex-match.outputs.match != '' }}
with:
github_token: ${{ secrets.GH_TOKEN }}
labels: linux
+1 -1
View File
@@ -1,6 +1,6 @@
# This workflow will...
name: Realease Linux
name: Release Linux
env:
GH_TOKEN: ${{ secrets.GH_TOKEN }}
+1
View File
@@ -18,6 +18,7 @@ We'd also love PRs. If you're thinking of a large PR, we advise opening up an is
## Submitting a pull request
1. [Fork][fork] and clone the repository.
1. Install the correct NodeJS version (highly recommend installing [Volta](https://volta.sh/) for that).
1. Configure and install the dependencies: `npm install`.
1. Create a new branch following naming convention: `git checkout -b (feature|bugfix|hotfix|chore)/(short-description)(/issue-id)`.
1. Make your change, test, and make sure BSManager work fine.
+1 -1
View File
@@ -546,7 +546,7 @@
<div>
<h2>Credits</h2>
<ul>
<li><a href="https://github.com/Zagrios">Zagrios</a> - Lead Developer & Founder.</li>
<li><a href="https://github.com/Zagrios">Zagrios - Mathieu Gries</a> - Lead Developer & Founder.</li>
<li><a href="https://github.com/Iluhadesu">Iluhadesu</a> - Co-Developer & Co-Founder, Discord Bot Developer.</li>
<li><a href="https://github.com/GaetanGrd">GaetanGrd</a> - Co-Developer & Co-Founder, Documentation Lead.</li>
<li><a href="https://github.com/cheddZy">cheddZy</a> - Icon Creator.</li>
+45 -2
View File
@@ -733,8 +733,7 @@
"ReleaseURL": "https://store.steampowered.com/news/app/620980/view/4161968170153497460",
"ReleaseImg": "https://clan.akamai.steamstatic.com/images//32055887/f82d34c7582137a6748a28c5b02670ff4b235356.png",
"ReleaseDate": "1717513346",
"year": "2024",
"recommended": true
"year": "2024"
},
{
"BSVersion": "1.37.1",
@@ -744,5 +743,49 @@
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/039be718c1494f948257cd5b09f0082986484a58.png",
"ReleaseDate": "1720710529",
"year": "2024"
},
{
"BSVersion": "1.37.2",
"BSManifest": "6848299977215652352",
"OculusBinaryId": "7438773609555687",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4349999161850643744",
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/7c03af6d12fad23f1d439666d4be86b492d40003.png",
"ReleaseDate": "1722945814",
"year": "2024"
},
{
"BSVersion": "1.37.3",
"BSManifest": "5834150512217183366",
"OculusBinaryId": "7553569971409383",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/6808965920900622902",
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/a1c9a6737cc5182fca8cddc7c8088eda6dd4d82d.png",
"ReleaseDate": "1724061712",
"year": "2024"
},
{
"BSVersion": "1.37.4",
"BSManifest": "7585106640515547731",
"OculusBinaryId": "7630971613669218",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4601078012425941968",
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/23e6e6f219a56d2f9ea4a27b560c54ac1214aec3.png",
"ReleaseDate": "1725548547",
"year": "2024"
},
{
"BSVersion": "1.37.5",
"BSManifest": "3122533396458693889",
"OculusBinaryId": "7685108391588873",
"ReleaseDate": "1725628356",
"year": "2024"
},
{
"BSVersion": "1.38.0",
"BSManifest": "3482902979520746178",
"OculusBinaryId": "7834818833284494",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4669760442595134838",
"ReleaseImg": "https://clan.akamai.steamstatic.com/images/32055887/57cfa043407c091e67b7e0a8ae86fe5590deae33.png",
"ReleaseDate": "1728399815",
"year": "2024",
"recommended": true
}
]
+11 -1
View File
@@ -99,5 +99,15 @@
},
{
"username": "Austin"
}
},
{
"username": "Fatalution",
"type": "diamond",
"link": "https://x.com/fatalution"
},
{
"username": "Taurus Arcade",
"type": "gold"
}
]
+259 -191
View File
@@ -1,23 +1,24 @@
{
"misc": {
"download": "Laden",
"download": "Herunterladen",
"add": "Hinzufügen",
"verify": "Prüfen",
"launch": "Starten",
"mods": "Mods",
"maps": "Maps",
"playlists": "Playlisten",
"playlists": "Playlists",
"models": "Modelle",
"cancel": "Abbruch",
"cancel": "Abbrechen",
"delete": "Löschen",
"accept": "Akzeptieren",
"refuse": "Ablehnen",
"apply": "Übernehmen",
"copy": "Kopieren",
"copied": "Kopiert!"
"copied": "Kopiert!",
"confirm": "Bestätigen"
},
"nav-bar": {
"add-version": "Version Hinzufügen",
"add-version": "Version hinzufügen",
"settings": "Einstellungen",
"shared": {
"text": "Geteilt",
@@ -28,14 +29,14 @@
"version-viewer": {
"launch-mods": {
"oculus": "Oculus Modus",
"oculus-description": "Wenn du Beat Saber über Steam benutzt, es ermöglicht dir Oculus's VR-Composer zu nutzen ohne SteamVR, um bessere Leistung zu erhalten. Dies wird nicht benötigt für Oculus Headsets",
"oculus-description": "Wenn du Beat Saber über Steam benutzt, ermöglicht dir diese Einstellung, Oculus's VR-Composer ohne SteamVR zu nutzen, um eine bessere Leistung zu erhalten. Dies wird nicht für Oculus Headsets benötigt.",
"desktop": "FPFC Modus",
"desktop-description": "Auf diese Weise kannst du WASD und die Maus verwenden, um im Spiel durch das Menü zu navigieren. Dies erleichtert das Testen erheblich, da Du dein Headset nicht aufsetzen musst!",
"desktop-description": "Auf diese Weise kannst du WASD und die Maus verwenden, um im Spiel durch das Menü zu navigieren. Dies erleichtert das Testen erheblich, da du dein Headset nicht aufsetzen musst!",
"debug": "Debug Modus",
"debug-description": "Aktiviert das Ausgabeprotokollfenster für IPA. Dies zeigt die Debug-Konsole, die Mods verwenden.",
"debug-description": "Aktiviert das Ausgabeprotokollfenster für IPA. Dies zeigt die Debug-Konsole, welche die Mods verwenden.",
"advanced-launch": {
"button": "Erweiterter Start",
"placeholder": "Weitere Argumente Bspw: --revert; --nowait"
"placeholder": "Weitere Argumente, bspw: --revert; --nowait"
}
},
"maps": {
@@ -43,8 +44,9 @@
"search-placeholder": "Suche eine Map",
"filters-btn": "Filter",
"dropdown": {
"export-maps": "Maps Exportieren",
"delete-maps": "Maps Löschen"
"export-maps": "Maps exportieren",
"delete-maps": "Maps löschen",
"delete-duplicate-maps": "Duplikate löschen"
}
},
"tabs": {
@@ -55,14 +57,14 @@
},
"link-maps": {
"tooltips": {
"link": "Maps Verknüpfen",
"unlink": "Maps Verknüpfung aufheben"
"link": "Maps verknüpfen",
"unlink": "Maps-Verknüpfung aufheben"
}
}
},
"empty-maps": {
"text": "Keine Maps",
"button": "Maps Herunterladen"
"button": "Maps herunterladen"
}
}
}
@@ -70,7 +72,7 @@
"mods": {
"loading-mods": "Lade Mods...",
"no-internet": "Kein Internet",
"mods-not-available": "Für diese Version von Beat Saber sind noch keine Mods verfügbar",
"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",
@@ -86,11 +88,17 @@
"uninstall-all": "Alle deinstallieren"
}
}
},
"notifications": {
"all-mods-already-installed": {
"title": "Mods bereits installiert",
"description": "Alle ausgewählten Mods sind bereits installiert"
}
}
},
"dropdown": {
"open-folder": "Ordner Öffnen",
"verify-files": "Daten Prüfen",
"open-folder": "Ordner öffnen",
"verify-files": "Daten prüfen",
"clone": "Klonen",
"edit": "Bearbeiten",
"uninstall": "Entfernen",
@@ -100,40 +108,41 @@
},
"available-versions": {
"title": "Wähle eine Version",
"steam-release": "Versions Seite",
"steam-release": "Versionsseite",
"dropdown": {
"refresh": "Aktualisieren Sie die Versionen",
"refresh": "Aktualisiere die Versionen",
"import-version": "Importiere eine Version"
},
"recommended": "empfohlen",
"recommended-tooltip": "Am modbarsten Version"
"recommended-tooltip": "Am modbarste Version"
},
"settings": {
"steam-and-oculus": {
"title": "Steam & Oculus",
"description": "Abmelden ermöglicht es dir, beim nächsten Beat Saber Download den Account zu wechseln.",
"description": "Das Abmelden ermöglicht es dir, beim nächsten Download von Beat Saber den Account zu wechseln.",
"logout": "Abmelden",
"logout-success": "Abmeldung erfolgreich",
"download-platform": {
"title": "Standardplattform",
"desc": "Wähle die Standardplattform, die für das Herunterladen von Beat Saber-Versionen verwendet wird.",
"desc": "Wähle die Standardplattform, die für das Herunterladen von Beat Saber-Versionen verwendet werden soll.",
"always-ask": "Immer fragen"
}
},
"appearance": {
"title": "Aussehen",
"description": "Wähle Zwei von den Hauptfarben von BSManager",
"description": "Wähle die zwei Hauptfarben von BSManager.",
"reset": "Zurücksetzen",
"sub-title": "Design",
"themes": {
"dark": "Dunkel",
"light": "Hell",
"os": "Mit Computer Synchronisieren"
"os": "Mit Computer synchronisieren"
}
},
"installation-folder": {
"title": "Installations Ordner",
"description": "Ändern Sie den Standardordner für Beat Saber-Versionen und andere kommende Funktionen.",
"choose-folder": "Ordner Wählen"
"title": "Installations-Ordner",
"description": "Ändern Sie den Ordner, der alle von BSManager heruntergeladenen Inhalte enthalten wird.",
"choose-folder": "Ordner wählen"
},
"additional-content": {
"title": "Zusätzlicher Inhalt",
@@ -172,7 +181,7 @@
"title": "BSManager Unterstützen 💖",
"description": "Unterstütze das Projekt und helfe uns, BSManager kontinuierlich zu verbessern.",
"buttons": {
"support": "BSManager Unterstützen 🥰",
"support": "BSManager unterstützen 🥰",
"supporters": "Unterstützer 👀"
},
"view": {
@@ -182,12 +191,12 @@
}
},
"discord": {
"description": "Trete der BSManager-Community bei, indem Du uns in unseren sozialen Medien folgst!"
"description": "Trete der BSManager-Community bei, indem du uns in unseren sozialen Medien folgst!"
},
"contribution": {
"description": "Schlage neue Funktionen vor, oder melde einen Fehler und BSManager zu verbessern!",
"description": "Schlage neue Funktionen vor, oder melde einen Fehler, um BSManager zu verbessern!",
"buttons": {
"request-features": "Funktion Vorschlagen",
"request-features": "Funktion vorschlagen",
"report-bug": "Fehler melden",
"open-logs": "Protokolle öffnen"
}
@@ -195,6 +204,34 @@
"changelogs": {
"open" : "🚀 Neues im Changelog entdecken!",
"not-founds": "😕 Hoppla, kein Changelog zu sehen!"
},
"advanced": {
"title": "Erweiterte",
"description": "Erweiterte Einstellungen für BSManager.",
"hardware-acceleration": {
"title": "Hardware-Beschleunigung",
"description": "Aktivieren Sie die Hardware-Beschleunigung, um Ihre GPU zu nutzen und die Leistung von BSManager zu verbessern. Deaktivieren Sie diese Option, wenn Sie Frame-Drops erleben.",
"modal": {
"title": "Neustart erforderlich",
"body": "Das Ändern der Hardware-Beschleunigungseinstellung wird BSManager beenden und neu starten. Sind Sie sicher, dass Sie dies tun möchten?",
"confirm-btn": "Ja, ich bin sicher"
},
"error-notification": {
"message": "Ein Fehler ist aufgetreten, die Hardware-Beschleunigung kann nicht deaktiviert werden."
}
},
"use-symlinks": {
"title": "Symbolische Links verwenden",
"description": "Verwenden Sie symbolische Links anstelle von Verknüpfungen, um Ordner zu verknüpfen. Aktivieren Sie dies nur, wenn Sie es wirklich benötigen.",
"modal": {
"title": "Berechtigungen für symbolische Links",
"body": "Beim Erstellen symbolischer Links benötigt BSManager Administratorrechte oder den aktivierten Entwicklermodus auf Ihrem System. Sind Sie sicher, dass Sie fortfahren möchten?",
"confirm-btn": "Ja, ich bin sicher"
},
"error-notification": {
"message": "Ein Fehler ist aufgetreten, die Einstellungen für symbolische Links können nicht geändert werden."
}
}
}
}
},
@@ -206,7 +243,7 @@
},
"common": {
"msg": {
"error-occurred": "Ein Fehler ist aufgetreten"
"error-occurred": "Ein Fehler ist aufgetreten."
}
},
"shared": {
@@ -216,7 +253,7 @@
"no-internet": "Kein Internet"
},
"msg": {
"operation-running": "Warte auf das Ende des Aktuellen Vorgangs und versuche es erneut",
"operation-running": "Warte auf das Ende des aktuellen Vorgangs und versuche es erneut.",
"no-internet": "Überprüfe deine Verbindung und versuche es erneut."
}
}
@@ -224,14 +261,14 @@
"bs-download": {
"success": {
"titles": {
"download-success": "Erfolgreich Heruntergeladen",
"verification-finished": "Verifikation Erfolgreich"
"download-success": "Erfolgreich heruntergeladen",
"verification-finished": "Verifikation erfolgreich"
}
},
"steam-download": {
"warnings": {
"msg": {
"ManifestChecksum": "Die zuvor heruntergeladene Manifest stimmt nicht mit dem neuen überein 🤔",
"ManifestChecksum": "Das zuvor heruntergeladene Manifest stimmt nicht mit dem neuen überein 🤔",
"ConnectionTimeout": "Deine Internetverbindung scheint instabil zu sein 🥶",
"ConnectionLost": "Die Verbindung wurde unterbrochen, versuche es erneut...",
"ConnectionError": "Kann keine Verbindung zu Steam herstellen, versuche es erneut...",
@@ -240,27 +277,27 @@
},
"errors": {
"msg": {
"401": "Steam scheint uns BeatSaber nicht herunterladen zu lassen 😢",
"401": "Steam scheint uns Beat Saber nicht herunterladen zu lassen 😢",
"404": "Steam-Server können nicht kontaktiert werden.",
"Password": "Passwort ist ungültig.",
"InvalidCredentials": "Ungültige Anmeldedaten, nicht genehmigte Verbindung oder zu viele Anmeldeversuche.",
"NoManifest": "Es wurde keine Manifest gefunden",
"NoManifest": "Es wurde kein Manifest gefunden.",
"DirectoryCreate": "Die erforderlichen Ordner können nicht installiert werden.",
"NotAvailableApp": "Versuchst du, Beat Saber herunterzuladen, wenn Du es nicht hast? 🤣",
"DepotNotFound": "BeatSaber kann nicht heruntergeladen werden 😥 versuche es später noch einmal 😕",
"NotAvailableApp": "Versuchst du, Beat Saber herunterzuladen, obwohl du es nicht hast? 🤣",
"DepotNotFound": "Beat Saber kann nicht heruntergeladen werden 😥 Versuche es später noch einmal 😕",
"NotCompleted": "Der Download konnte nicht beendet werden ¯\\_(ツ)_/¯",
"InvalidManifest": "BeatSaber kann nicht heruntergeladen werden 😥 versuche es später noch einmal 😕",
"NoValidKey": "BeatSaber kann nicht heruntergeladen werden 😥 versuche es später noch einmal 😕",
"NoManifestCode": "BeatSaber kann nicht heruntergeladen werden 😥 versuche es später noch einmal 😕",
"Unknown": "Ein Unbekannter Fehler ist aufgetreten ¯\\_(ツ)_/¯",
"InvalidManifest": "Beat Saber kann nicht heruntergeladen werden 😥 Versuche es später noch einmal 😕",
"NoValidKey": "Beat Saber kann nicht heruntergeladen werden 😥 Versuche es später noch einmal 😕",
"NoManifestCode": "Beat Saber kann nicht heruntergeladen werden 😥 Versuche es später noch einmal 😕",
"Unknown": "Ein unbekannter Fehler ist aufgetreten ¯\\_(ツ)_/¯",
"NoServer": "Steam-Server können nicht kontaktiert werden.",
"NotAllowed": "Anscheinend darfst Du BeatSaber nicht herunterladen 🥱",
"NotAllowed": "Anscheinend darfst du Beat Saber nicht herunterladen 🥱",
"ConnectionTimeout": "Kann keine Verbindung zu Steam herstellen 😕",
"SteamLib": "Wenn dieser Fehler auftritt, melde den Fehler auf Github mit Protokollen, bitte.",
"SteamLib": "Wenn dieser Fehler auftritt, melde den Fehler bitte auf Github mit den Protokollen.",
"ConnectionError": "Kann nach 10 Versuchen keine Verbindung zu Steam herstellen 🤯",
"LicenceError": "Die Liste der Lizenzen kann nicht abgerufen werden.",
"RateLimitExceeded": "Du hast es zu oft versucht, warte eine Weile und versuche es später erneut.",
"TokenRejected": "Ihr Anmeldetoken wurde abgelehnt 😕 Bitte versuchen Sie es erneut.",
"RateLimitExceeded": "Du hast es zu oft versucht. Warte eine Weile und versuche es später erneut.",
"TokenRejected": "Dein Anmeldetoken wurde abgelehnt 😕 Bitte versuche es erneut.",
"AccessDenied": "Der Zugang zu Steam wurde verweigert."
}
}
@@ -268,15 +305,15 @@
"oculus-download": {
"errors": {
"msg": {
"DOWNLOAD_MANIFEST_FAILED": "Es ist nicht möglich, das Manifest für diese Version herunterzuladen; Ihr Anmelde-Token ist möglicherweise ungültig.",
"DOWNLOAD_MANIFEST_FAILED": "Es ist nicht möglich, das Manifest für diese Version herunterzuladen. Dein Anmelde-Token ist möglicherweise ungültig.",
"MANIFEST_FILE_NOT_FOUND": "Unmöglich, das Manifest für diese Version zu finden.",
"PARSE_MANIFEST_FILE_FAILED": "Beim Lesen des Manifests ist ein Fehler aufgetreten.",
"ALREADY_DOWNLOADING": "Eine Version wird bereits heruntergeladen.",
"UNABLE_TO_GET_MANIFEST": "Unmöglich, das für den Download erforderliche Manifest zu erhalten.",
"VERIFY_INTEGRITY_FAILED": "Beim Überprüfen der Dateien ist ein Fehler aufgetreten.",
"SOME_FILES_FAILED_TO_DOWNLOAD": "Einige Dateien konnten nicht heruntergeladen werden.",
"OCULUS_LOGIN_TIMED_OUT": "Das Anmelde-Token hat zu lange gedauert, um abgerufen zu werden.",
"OCULUS_LOGIN_WINDOW_CLOSED_BY_USER": "Das Meta Anmeldefenster wurde geschlossen.",
"OCULUS_LOGIN_TIMED_OUT": "Es hat zu lange gedauert, das Anmelde-Token abzurufen.",
"OCULUS_LOGIN_WINDOW_CLOSED_BY_USER": "Das Meta-Anmeldefenster wurde geschlossen.",
"NO_META_AUTH_TOKEN": "Unmöglich, das für den Download erforderliche Meta-Anmelde-Token zu erhalten.",
"UNKNOWN_ERROR": "Ein unbekannter Fehler ist aufgetreten."
}
@@ -290,7 +327,7 @@
"desc": "Der Import kann je nach Konfiguration einige Minuten dauern."
},
"imported": {
"title": "Versionen Importiert 🎉"
"title": "Versionen importiert 🎉"
}
},
"errors": {
@@ -329,7 +366,7 @@
},
"additional-content": {
"deep-link": {
"select-all": "Alle Auswählen",
"select-all": "Alle auswählen",
"activation": {
"success": {
"title": "OneClick aktiviert!",
@@ -345,7 +382,7 @@
"description": "OneClick-Installationen wurden deaktiviert."
},
"error": {
"description": "Ein Unbekannter Fehler ist aufgetreten."
"description": "Ein unbekannter Fehler ist aufgetreten."
}
},
"check-all-enabled": {
@@ -353,7 +390,7 @@
"description": "Eine oder mehrere OneClick-Installationen sind deaktiviert. Gehe in die Einstellungen, um sie zu aktivieren.",
"actions": {
"settings": "Einstellungen",
"not-remind": "Erinnere mich nicht"
"not-remind": "Nicht mehr erinnern"
}
}
}
@@ -362,7 +399,7 @@
"bs-launch": {
"success": {
"titles": {
"BS_LAUNCHING": "Starte...🚀",
"BS_LAUNCHING": "Starte... 🚀",
"STEAM_LAUNCHING": "Steam startet!"
},
"msg": {
@@ -376,7 +413,7 @@
"STEAM_NOT_RUNNING": "Steam läuft nicht",
"OCULUS_NOT_RUNNING": "Oculus läuft nicht",
"BS_ALREADY_RUNNING": "Beat Saber läuft bereits",
"EXE_NOT_FINDED": "Dateien Fehlen",
"EXE_NOT_FINDED": "Dateien fehlen",
"EXIT": "Abrupter Stop",
"OCULUS_LIB_NOT_FOUND": "Oculus-Bibliothek nicht gefunden"
},
@@ -384,9 +421,9 @@
"UNKNOWN_ERROR": "Ein unbekannter Fehler ist aufgetreten.",
"STEAM_NOT_RUNNING": "Steam muss laufen, um Beat Saber zu starten.",
"OCULUS_NOT_RUNNING": "Oculus muss ausgeführt werden, um Beat Saber zu starten.",
"BS_ALREADY_RUNNING": "Schließe Beat Saber, bevor Du es erneut startest.",
"EXE_NOT_FINDED": "Einige Dateien scheinen zu fehlen, versuche die Dateien zu überprüfen.",
"EXIT": "Beat Saber stoppte abrupt, versuche die Dateien zu überprüfen.",
"BS_ALREADY_RUNNING": "Schließe Beat Saber, bevor du es erneut startest.",
"EXE_NOT_FINDED": "Einige Dateien scheinen zu fehlen. Versuche, die Dateien zu überprüfen.",
"EXIT": "Beat Saber stoppte abrupt. Versuche, die Dateien zu überprüfen.",
"OCULUS_LIB_NOT_FOUND": "Überprüfe, ob die Oculus-Anwendung korrekt installiert ist und dass die Bibliotheken in Oculus richtig definiert sind."
},
"actions": {
@@ -406,7 +443,8 @@
"CantEditSteam": "Kann nicht bearbeitet werden",
"CantRename": "Umbenennen unmöglich",
"VersionAlreadExist": "Diese Version existiert bereits",
"CantClone": "Klonen unmöglich"
"CantClone": "Klonen unmöglich",
"UnknownError": "Ein unbekannter Fehler ist aufgetreten"
},
"msg": {
"CantEditSteam": "Die Steam-Version kann nicht bearbeitet werden. Du kannst es aber klonen."
@@ -421,15 +459,15 @@
"mods": {
"install-mods": {
"titles": {
"success": "Mods Installiert 🎉",
"warning": "Mods Installiert 🤔"
"success": "Mods installiert 🎉",
"warning": "Mods installiert 🤔"
},
"msg": {
"success": "Alle Mods wurden installiert.",
"warning": "Eine oder mehr Mods konnten nicht installiert werden.",
"warning": "Eine oder mehrere Mods konnten nicht installiert werden.",
"errors": {
"no-mods": "Keine Mods zum installieren.",
"cannot-install-bsipa": "BSIPA installation fehlgeschlagen 😨"
"cannot-install-bsipa": "BSIPA-Installation fehlgeschlagen 😨"
}
}
},
@@ -457,27 +495,44 @@
},
"maps": {
"one-click-install": {
"success": "Map installation abgeschlossen",
"error": "Ein Fehler ist aufgetreten während der installation der Map"
"success": "Map-Installation abgeschlossen",
"error": "Ein Fehler ist während der Installation der Map aufgetreten."
},
"no-duplicates-maps": {
"title": "Keine Duplikate",
"msg": "Keine Karten wurden gelöscht"
},
"duplicates-maps-deleted": {
"title": "Duplikate gelöscht",
"msg": "Duplikate wurden gelöscht"
}
},
"playlists": {
"one-click-install": {
"success": "Playlist installation abgeschlossen",
"error": "Ein Fehler ist aufgetreten während der installation der Playlist"
"success": "Playlist-Installation abgeschlossen",
"error": "Ein Fehler ist während der Installation der Playlist aufgetreten."
}
},
"models": {
"one-click-install": {
"success": "Modell installation abgeschlossen",
"error": "Ein Fehler ist aufgetreten während der installation des Modells"
"success": "Modell-Installation abgeschlossen",
"error": "Ein Fehler ist während der Installation des Modells aufgetreten."
}
},
"shared-folder": {
"info": {
"userdata-backup-created": {
"title": "Sicherung erstellt",
"msg": "Das Freigeben des Ordners „UserData“ kann zu Fehlern führen. Bei Problemen trenne den Ordner, um die Sicherung wiederherzustellen"
"msg": "Das Freigeben des Ordners „UserData“ kann zu Fehlern führen. Bei Problemen trenne den Ordner, um die Sicherung wiederherzustellen."
}
},
"linking-error": {
"title": "Fehler beim Verknüpfen des Ordners",
"msg": {
"EPERM": "BSManager hat nicht die erforderlichen Berechtigungen, um den Ordner zu verknüpfen.",
"EACCES": "BSManager hat nicht die erforderlichen Berechtigungen, um den Ordner zu verknüpfen.",
"ENOSPC": "Die Festplatte ist voll, machen Sie Platz und versuchen Sie es erneut.",
"UNKNOWN_ERROR": "Ein unbekannter Fehler ist beim Verknüpfen des Ordners aufgetreten."
}
}
},
@@ -493,11 +548,11 @@
},
"modals": {
"misc": {
"remember-my-choice": "Merke meine Wahl"
"remember-my-choice": "Meine Wahl merken"
},
"choose-store": {
"title": "Welche Plattform?",
"body": "Wählen Sie die Plattform, von der Sie Beat Saber herunterladen möchten.",
"body": "Wähle die Plattform, von der du Beat Saber herunterladen möchtest.",
"set-in-settings": "Die Standardplattform in den Einstellungen festlegen"
},
"guard": {
@@ -517,19 +572,19 @@
"inputs": {
"username": {
"label": "Mit Accountnamen anmelden",
"placeholder": "Geben Sie Ihren Accountnamen ein"
"placeholder": "Gebe deinen Accountnamen ein"
},
"password": {
"label": "Passwort",
"placeholder": "Geben Sie Ihr Passwort ein",
"max-length-warning": "Das Passwort überschreitet 64 Zeichen! Wenn das Passwort ungültig ist, versuchen Sie nur seine ersten 64 Zeichen einzugeben."
"placeholder": "Gebe dein Passwort ein",
"max-length-warning": "Das Passwort überschreitet 64 Zeichen! Wenn das Passwort ungültig ist, versuche nur seine ersten 64 Zeichen einzugeben."
},
"qr": {
"label": "Oder mit einem QR-Code",
"note": {
"use-the": "Anmelden Sie sich über die ",
"use-the": "Melde dich über die ",
"steam-mobile-app": "Steam-Mobile-App",
"to-connect-with-qr": " per QR-Code an."
"to-connect-with-qr": "oder per QR-Code an."
}
},
"stay": "Angemeldet bleiben"
@@ -543,13 +598,13 @@
"steam-auth-approve": {
"title": "Warten auf Bestätigung",
"protected-by-mobile-auth": "Konto durch mobile Authentifizierung geschützt.",
"use-steam-app-to-approve": "Verwenden Sie die Steam Mobile-App, um die Verbindung zu bestätigen...",
"use-steam-app-to-approve": "Verwende die Steam Mobile-App, um die Verbindung zu bestätigen...",
"not-access-to-steam-app": "Ich habe keinen Zugang zur Steam Mobile-App"
},
"steam-credentials": {
"title": "Steam Anmeldeinformationen",
"p-1": "Die Anmeldeinformationen werden nur zum Herunterladen des Spiels verwendet, da Steam überprüfen muss, ob Du das Spiel bezahlt hast, um es herunterladen zu dürfen. Sie werden nicht gespeichert und direkt an DepotDownloader weitergegeben. Wenn Du deine Anmeldeinformationen nicht eingeben möchtest, kannst Du diesem Tutorial folgen:",
"p-2": "und klicke dann auf das Zahnradsymbol in der oberen rechten Ecke und wähle \"Version importieren\", wähle den Ordner aus, in den Beat Saber heruntergeladen wurde (Wenn Du dem obigen Tutorial folgst, solltest Du den richtigen Speicherort haben)"
"p-1": "Die Anmeldeinformationen werden nur zum Herunterladen des Spiels verwendet, da Steam überprüfen muss, ob du das Spiel bezahlt hast, um es herunterladen zu dürfen. Sie werden nicht gespeichert und direkt an den Depot-Downloader weitergegeben. Wenn du deine Anmeldeinformationen nicht eingeben möchtest, kannst du diesem Tutorial folgen:",
"p-2": "und klicke dann auf das Zahnradsymbol in der oberen rechten Ecke und wähle \"Version importieren\", wähle den Ordner aus, in den Beat Saber heruntergeladen wurde (Wenn du dem obigen Tutorial folgst, solltest du den richtigen Speicherort haben)."
},
"bs-import-version": {
"title": "Importiere eine Version",
@@ -568,10 +623,10 @@
}
},
"install-folder": {
"title": "Installation Ordner",
"title": "Installations-Ordner",
"description": "Das Ändern des Standardinstallationsordners führt dazu, dass alle installierten Daten in den neuen Ordner verschoben werden.",
"buttons": {
"submit": "Ordner Wählen"
"submit": "Ordner wählen"
}
},
"edit-version": {
@@ -581,12 +636,12 @@
}
},
"clone-version": {
"title": "Version Klonen",
"title": "Version klonen",
"description": "Durch das Klonen der Version kannst Du zusätzlichen Beat Saber-Inhalt zwischen zwei Versionen trennen.",
"inputs": {
"name": {
"label": "Name",
"placeholder": "Versions Name"
"placeholder": "Versionsname"
},
"color": {
"label": "Farbe"
@@ -598,52 +653,57 @@
},
"uninstall-mod": {
"title": "Entfernen",
"description": "Bist du sicher das du die Mod {mod} entfernen möchtest? Es könnte dafür sorgen, dass andere installationen nicht mehr richtig funktionieren.",
"description-bsipa": "Bist du sicher, dass du BSIPA entfernen möchtest? Dannach werden deine Mods nicht mehr funktionieren."
"description": "Bist du sicher, dass du die Mod {mod} entfernen möchtest? Es könnte dafür sorgen, dass andere Installationen nicht mehr richtig funktionieren.",
"description-bsipa": "Bist du sicher, dass du BSIPA entfernen möchtest? Danach werden deine Mods nicht mehr funktionieren."
},
"uninstall-all-mods": {
"title": "Entferne Alle Mods",
"description": "Bist du sicher das du alle Mods von der Version {version} entfernen möchtest? Diese Aktion kann nicht rückgängig gemacht werden."
"title": "Entferne alle Mods",
"description": "Bist du sicher, dass du alle Mods von der Version {version} entfernen möchtest? Diese Aktion kann nicht rückgängig gemacht werden."
},
"maps-actions": {
"delete-maps": {
"title": {
"single": "Map Entfernen?",
"multiple": "Maps Entfernen?"
"single": "Map entfernen?",
"multiple": "Maps entfernen?"
},
"desc": {
"single": "Bist du sicher das du die Map {name} entfernen willst?",
"multiple": "Bist du sicher das du alle {nb} maps entfernen willst?"
"single": "Bist du sicher, dass du die Map {name} entfernen willst?",
"multiple": "Bist du sicher, dass du alle {nb} Maps entfernen willst?"
},
"info": {
"desc": {
"single": "Diese Map ist teil der geteilten Maps",
"multiple": "Diese Maps sind teil der geteilten Maps"
"single": "Diese Map ist Teil der geteilten Maps.",
"multiple": "Diese Maps sind Teil der geteilten Maps."
},
"title": {
"single": "Diese Map wird auch aus Versionen entfernt, die geteilte Maps verwenden",
"multiple": "Diese Maps werden auch aus Versionen entfernt, die geteilte Maps verwenden"
"single": "Diese Map wird auch aus Versionen entfernt, die geteilte Maps verwenden.",
"multiple": "Diese Maps werden auch aus Versionen entfernt, die geteilte Maps verwenden."
}
}
},
"link-maps": {
"title": "Maps Verknüpfen",
"desc": "Das Verknüpfen von Maps ermöglicht es, Maps zwischen allen Versionen zu teilen. Nach der Verknüpfung profitiert diese Version von den geteilten Maps",
"info": "Das Hinzufügen und Löschen von Maps wird ebenfalls geteilt",
"title": "Maps verknüpfen",
"desc": "Das Verknüpfen von Maps ermöglicht es, Maps zwischen allen Versionen zu teilen. Nach der Verknüpfung profitiert diese Version von den geteilten Maps.",
"info": "Das Hinzufügen und Löschen von Maps wird ebenfalls geteilt.",
"keep-maps": {
"label": "Maps behalten",
"title": "Maps behalten verschiebt die Maps der aktuellen Version in den geteilten Maps. Andernfalls gehen sie verloren."
"title": "\"Maps behalten\" verschiebt die Maps der aktuellen Version in den geteilten Maps-Ordner. Andernfalls gehen sie verloren."
},
"valid-btn": "Maps Verknüpfen"
"valid-btn": "Maps verknüpfen"
},
"unlink-maps": {
"title": "Maps Verknüpfung aufheben",
"title": "Maps-Verknüpfung aufheben",
"desc": "Bitte beachte, dass das Aufheben der Verknüpfung von Maps die Verwendung von geteilten Maps für diese Version nicht zulässt.",
"keep-maps": {
"label": "Maps behalten",
"title": "Maps behalten verschiebt die Maps der aktuellen Version in den geteilten Maps. Andernfalls gehen sie verloren."
"title": "\"Maps behalten\" kopiert die Maps aus dem geteilten Maps-Ordner in den Ordner der ausgewählten Version. Andernfalls gehen sie verloren."
},
"valid-btn": "Verknüpfung aufheben"
},
"delete-duplicate-maps": {
"title": "Map entfernen?",
"desc": "Nur die Karte \"{map}\" ist ein Duplikat. Bist du sicher, dass du sie löschen möchtest?",
"desc-plural": "Es wurden {nb} doppelte Karten gefunden. Bist du sicher, dass du sie löschen möchtest?"
}
},
"download-maps": {
@@ -654,7 +714,7 @@
},
"mods-disclaimer": {
"title": "Haftungsausschluss",
"p-1": "Indem Du dich für die Verwendung von Mods entscheiden, verstehst Du Folgendes:",
"p-1": "Indem du dich für die Verwendung von Mods entscheidest, verstehst du Folgendes:",
"li-1": "Möglicherweise treten Probleme auf, die im Vanilla-Spiel nicht vorhanden sind. 99.9% der Fehler, Abstürze und Verzögerungen sind auf Mods zurückzuführen.",
"li-2": "Mods können durch Updates beschädigt werden und das ist normal - sei geduldig und respektvoll, wenn dies passiert, da Modder Freiwillige mit echtem Leben sind.",
"li-3": "Beat Games versuchen nicht absichtlich, Mods kaputt zu machen. Sie möchten an der Codebasis arbeiten und manchmal macht dies Mods kaputt, aber sie sind nicht darauf aus, Mods zu verbieten.",
@@ -664,26 +724,26 @@
"title": "Geteilte Ordner",
"description": "Verknüpfe Beat Saber-Ordner, um ihre Inhalte mit freigegebenen Ordnern in anderen Versionen zu synchronisieren. Beachte, dass auch das Löschen von Inhalten geteilt wird.",
"buttons": {
"add-folder": "Ordner Hinzufügen",
"link-folder": "Ordner Verknüpfen",
"unlink-folder": "Ordner Verknüpfung Aufheben",
"add-folder": "Ordner hinzufügen",
"link-folder": "Ordner verknüpfen",
"unlink-folder": "Ordner-Verknüpfung aufheben",
"link-all": "Alle verlinken"
}
},
"create-launch-shortcut": {
"title": "Verknüpfung erstellen",
"desc": "Das Erstellen einer Verknüpfung ermöglicht es Ihnen, Beat Saber mit den ausgewählten Optionen zu starten, ohne durch BSManager zu gehen.",
"desc": "Das Erstellen einer Verknüpfung ermöglicht es dir, Beat Saber mit den ausgewählten Optionen zu starten, ohne durch BSManager zu gehen.",
"launch-options": "Startoptionen",
"advanced-launch": "Erweiterte Start",
"advanced-launch": "Erweiterter Start",
"valid-btn": "Verknüpfung erstellen"
},
"connect-to-meta": {
"title": "Mit Meta verbinden",
"body": {
"token-needed": "Dein Meta-Verbindungstoken wird benötigt, um Beat Saber herunterzuladen.",
"need-cookie-enabled": "Beim Einloggen in Meta wird ein Anmeldefenster geöffnet und du kannst dann den Anmeldevorgang beginnen. Bitte stelle sicher, dass Cookies akzeptiert werden, sonst könnten wir deinen Token zum Starten des Downloads nicht abrufen.",
"need-cookie-enabled": "Beim Einloggen in Meta wird ein Anmeldefenster geöffnet und du kannst dann den Anmeldevorgang beginnen. Bitte stelle sicher, dass Cookies akzeptiert werden, sonst können wir deinen Token zum Starten des Downloads nicht abrufen.",
"enter-token-manually": "Mein Anmelde-Token manuell eingeben",
"enter-token-manually-tooltip": "Dies ermöglicht es Ihnen, Ihren Anmelde-Token einzugeben, ohne sich über Meta anzumelden."
"enter-token-manually-tooltip": "Dies ermöglicht es dir, deinen Anmelde-Token einzugeben, ohne dich über Meta anzumelden."
},
"stay": "Angemeldet bleiben",
"connect-to-meta": "Mit Meta verbinden"
@@ -691,26 +751,26 @@
"original-version-backup-oculus": {
"title": "Achtung",
"body": {
"must-be-installed-once": "Sie müssen Beat Saber mindestens einmal aus dem Oculus Store installiert haben, sonst könnte sich Beat Saber nach dem Start automatisch schließen.",
"will-backup": "Um diese Version zu starten, wird der ursprüngliche Installationsordner von Beat Saber in Ihrer Oculus-Bibliothek umbenannt und beim Schließen von Beat Saber automatisch wiederhergestellt."
"must-be-installed-once": "Du musst Beat Saber mindestens einmal aus dem Oculus Store installiert haben, sonst könnte sich Beat Saber nach dem Start automatisch schließen.",
"will-backup": "Um diese Version zu starten, wird der ursprüngliche Installationsordner von Beat Saber in deiner Oculus-Bibliothek umbenannt und beim Schließen von Beat Saber automatisch wiederhergestellt."
},
"not-remind-me": "Nicht mehr erinnern",
"understood": "Verstanden"
},
"enter-meta-token": {
"title": "Oculus Token",
"title": "Oculus-Token",
"body": {
"info-enter-token": "Um Beat Saber herunterzuladen, ist Ihr Oculus-Anmeldetoken erforderlich.",
"info-enter-token": "Um Beat Saber herunterzuladen, ist dein Oculus-Anmeldetoken erforderlich.",
"how-obtain-token": "Wie erhalte ich mein Oculus Token?",
"oculus-token": "Oculus Token",
"oculus-token": "Oculus-Token",
"token-is-invalid": "Das Token ist ungültig.",
"save-my-token": "Mein Token speichern",
"have-token-saved": "Ich habe bereits ein Token gespeichert",
"save-token-info": "Dies wird Ihr Token zur einfacheren Wiederverwendung speichern. Sie müssen ein Passwort erstellen, um Ihr Token zu verschlüsseln und zu speichern. Wenn Sie Ihr Passwort vergessen, müssen Sie nur Ihr Token erneut angeben.",
"save-my-token": "Meinen Token speichern",
"have-token-saved": "Ich habe bereits einen Token gespeichert",
"save-token-info": "Dies wird deinen Token zur einfacheren Wiederverwendung speichern. Du musst ein Passwort erstellen, um dein Token zu verschlüsseln und zu speichern. Wenn du dein Passwort vergisst, musst du nur deinen Token erneut angeben.",
"password": "Passwort",
"password-too-short": "Passwort zu kurz",
"info-enter-password": "Um Beat Saber herunterzuladen, ist Ihr Oculus-Anmeldetoken erforderlich. Geben Sie das Passwort ein, mit dem Ihr Oculus-Token gespeichert wurde.",
"info-disabled-btn-password": "Der mit dem angegebenen Passwort entschlüsselte Token ist ungültig. Stellen Sie sicher, dass das Passwort dasselbe ist wie das zum Speichern des Tokens verwendete.",
"info-enter-password": "Um Beat Saber herunterzuladen, ist dein Oculus-Anmeldetoken erforderlich. Gib das Passwort ein, mit dem dein Oculus-Token gespeichert wurde.",
"info-disabled-btn-password": "Der mit dem angegebenen Passwort entschlüsselte Token ist ungültig. Stelle sicher, dass das Passwort dasselbe ist, wie das zum Speichern des Tokens verwendete.",
"enter-oculus-token": "Ein Oculus-Token eingeben"
},
"valid-btn": "Validieren"
@@ -718,22 +778,30 @@
"launch-as-admin": {
"title": "Administratorberechtigungen",
"body": {
"info": "Es wurde festgestellt, dass Steam mit Administratorrechten ausgeführt wird. Um mit Steam zu kommunizieren, muss auch Beat Saber als Administrator gestartet werden. Andernfalls könnte Beat Saber Probleme haben und nach dem Starten schließen.",
"info": "Es wurde festgestellt, dass Steam mit Administratorrechten ausgeführt wird. Um mit Steam zu kommunizieren, muss auch Beat Saber als Administrator gestartet werden. Andernfalls könnte Beat Saber Probleme haben und sich nach dem Starten schließen.",
"info-2": "Das Starten von Beat Saber im Administratormodus gibt auch den installierten Mods Administratorrechte. Daher wird empfohlen, Steam ohne Administratorrechte neu zu starten.",
"info-3": "Beachten Sie, dass es nicht ratsam ist, Steam Administratorrechte zu geben, da dies auch die installierten Spiele und Mods betrifft und ein Sicherheitsrisiko darstellt."
"info-3": "Beachte, dass es nicht ratsam ist, Steam Administratorrechte zu geben, da dies auch die installierten Spiele und Mods betrifft und ein Sicherheitsrisiko darstellt."
},
"launch-as-admin": "Als Administrator starten",
"not-remind-me": "Nicht mehr erinnern"
},
"ask-install-path": {
"title": "Installations Ordner",
"choose-folder-description": "Wählen Sie den Ordner aus, der alle von BSManager heruntergeladenen Inhalte enthalten soll. (Versionen, Mods, Karten, Playlists, etc.)",
"choose-folder": "Ordner Wählen",
"default": "Standard",
"default-tooltip": "Standardmäßig in Ihrem persönlichen Ordner"
}
},
"maps": {
"map-filter-panel": {
"duration": "Länge",
"nps" : "Noten Pro Sekunde",
"tags": "Stichworte",
"nps" : "Noten pro Sekunde",
"njs": "Notensprunggeschwindigkeit",
"tags": "Stichwörter",
"specificities": "Allgemein",
"requirements": "Anforderungen",
"exclude": "ausschließen"
"exclude": "Ausschließen"
},
"map-types": {
"accuracy": "Präzision",
@@ -750,7 +818,7 @@
"nightcore": "Nightcore",
"folk": "Folk",
"family": "Familie",
"ambient": "umgebung",
"ambient": "Umgebung",
"funk": "Funk",
"jazz": "Jazz",
"soul": "Soul",
@@ -780,24 +848,24 @@
"rock": "Rock",
"pop": "Pop",
"electronic": "Elektronisch",
"classical-orchestral": "Klassisch & Orchestra"
"classical-orchestral": "Klassisch & Orchester"
},
"map-specificities": {
"automapper": "KI",
"ranked": "ranked",
"curated": "curated",
"verified": "verifiziert",
"fullSpread": "full spread"
"ranked": "Ranked",
"curated": "Curated",
"verified": "Verifiziert",
"fullSpread": "Full Spread"
},
"map-excludes": {
"installed": "installiert"
"installed": "Installiert"
},
"difficulties": {
"Easy": "einfach",
"Normal": "normal",
"Hard": "hard",
"Expert": "expert",
"ExpertPlus": "expert+"
"Easy": "Einfach",
"Normal": "Normal",
"Hard": "Hard",
"Expert": "Expert",
"ExpertPlus": "Expert+"
},
"map-item": {
"by": "Von {songAutor}",
@@ -849,12 +917,12 @@
"modals": {
"delete-model": {
"title": "Das Modell löschen",
"desc": "Sind Sie sicher, dass Sie das Modell {modelName} löschen möchten?",
"desc": "Bist du dir sicher, dass du das Modell {modelName} löschen möchtest?",
"linked-annotation": "Dieses Modell wird aus allen verlinkten Versionen entfernt."
},
"delete-models": {
"title": "Modelle löschen",
"desc": "Sind Sie sicher, dass Sie die {nb} Modelle löschen möchten?",
"desc": "Bist du dir sicher, dass du die {nb} Modelle löschen möchtest?",
"linked-annotation": "Diese Modelle werden aus allen verlinkten Versionen entfernt."
},
"download-models": {
@@ -879,41 +947,41 @@
"link-models": {
"avatar": {
"title": "Avatare verbinden",
"desc": "Das Verbinden von Avataren ermöglicht das Teilen von Avataren zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Avataren profitieren",
"info": "Das Hinzufügen und Entfernen von Avataren wird ebenfalls geteilt",
"desc": "Das Verbinden von Avataren ermöglicht das Teilen von Avataren zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Avataren profitieren.",
"info": "Das Hinzufügen und Entfernen von Avataren wird ebenfalls geteilt.",
"keep-models": {
"label": "Avatare behalten",
"title": "Das Behalten der Avatare wird die Avatare der aktuellen Version in den Ordner der geteilten Avatare verschieben. Andernfalls gehen sie verloren"
"title": "Das Behalten der Avatare wird die Avatare der aktuellen Version in den Ordner der geteilten Avatare verschieben. Andernfalls gehen sie verloren."
},
"valid-btn": "Avatare verbinden"
},
"saber": {
"title": "Säbel verbinden",
"desc": "Das Verbinden von Säbeln ermöglicht das Teilen von Säbeln zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Säbeln profitieren",
"info": "Das Hinzufügen und Entfernen von Säbeln wird ebenfalls geteilt",
"desc": "Das Verbinden von Säbeln ermöglicht das Teilen von Säbeln zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Säbeln profitieren.",
"info": "Das Hinzufügen und Entfernen von Säbeln wird ebenfalls geteilt.",
"keep-models": {
"label": "Säbel behalten",
"title": "Das Behalten der Säbel wird die Säbel der aktuellen Version in den Ordner der geteilten Säbel verschieben. Andernfalls gehen sie verloren"
"title": "Das Behalten der Säbel wird die Säbel der aktuellen Version in den Ordner der geteilten Säbel verschieben. Andernfalls gehen sie verloren."
},
"valid-btn": "Säbel verbinden"
},
"platform": {
"title": "Plattformen verbinden",
"desc": "Das Verbinden von Plattformen ermöglicht das Teilen von Plattformen zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Plattformen profitieren",
"info": "Das Hinzufügen und Entfernen von Plattformen wird ebenfalls geteilt",
"desc": "Das Verbinden von Plattformen ermöglicht das Teilen von Plattformen zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Plattformen profitieren.",
"info": "Das Hinzufügen und Entfernen von Plattformen wird ebenfalls geteilt.",
"keep-models": {
"label": "Plattformen behalten",
"title": "Das Behalten der Plattformen wird die Plattformen der aktuellen Version in den Ordner der geteilten Plattformen verschieben. Andernfalls gehen sie verloren"
"title": "Das Behalten der Plattformen wird die Plattformen der aktuellen Version in den Ordner der geteilten Plattformen verschieben. Andernfalls gehen sie verloren."
},
"valid-btn": "Plattformen verbinden"
},
"bloq": {
"title": "Bloqs verbinden",
"desc": "Das Verbinden von Bloqs ermöglicht das Teilen von Bloqs zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Bloqs profitieren",
"info": "Das Hinzufügen und Entfernen von Bloqs wird ebenfalls geteilt",
"desc": "Das Verbinden von Bloqs ermöglicht das Teilen von Bloqs zwischen allen Versionen. Sobald verbunden, wird diese Version von den geteilten Bloqs profitieren.",
"info": "Das Hinzufügen und Entfernen von Bloqs wird ebenfalls geteilt.",
"keep-models": {
"label": "Bloqs behalten",
"title": "Das Behalten der Bloqs wird die Bloqs der aktuellen Version in den Ordner der geteilten Bloqs verschieben. Andernfalls gehen sie verloren"
"title": "Das Behalten der Bloqs wird die Bloqs der aktuellen Version in den Ordner der geteilten Bloqs verschieben. Andernfalls gehen sie verloren."
},
"valid-btn": "Bloqs verbinden"
}
@@ -960,9 +1028,9 @@
"notifications": {
"prevent-for-mods": {
"title": "Erforderliche Mods",
"desc": "Stellen Sie sicher, dass Sie die notwendigen Mods installiert haben, um Modelle in Beat Saber zu verwenden",
"go-to-mods": "Gehe zu Mods",
"not-remind": "Erinnere mich nicht"
"desc": "Stelle sicher, dass du die notwendigen Mods installiert hast, um Modelle in Beat Saber zu verwenden.",
"go-to-mods": "Zu den Mods",
"not-remind": "Nicht mehr erinnern"
},
"prevent-for-models-breaks": {
"title": "Gebrochene Modelle",
@@ -983,7 +1051,7 @@
},
"auto-update": {
"checking": "Prüfe auf Updates",
"downloading": "Lade Updates Herunter"
"downloading": "Lade Updates herunter"
},
"bs-shortcut-launch": {
"beat-saber-launching": "Start von Beat Saber",
@@ -992,8 +1060,8 @@
"status-text": {
"init": "Initialisierung...",
"success": {
"BS_LAUNCHING": "Start von Beat Saber...",
"STEAM_LAUNCHING": "Start von Steam...",
"BS_LAUNCHING": "Starte Beat Saber...",
"STEAM_LAUNCHING": "Starte Steam...",
"STEAM_LAUNCHED": "Steam erfolgreich gestartet!",
"UNABLE_TO_LAUNCH_STEAM": "Steam kann nicht gestartet werden, erzwungener Start von Beat Saber..."
}
@@ -1003,7 +1071,7 @@
"error-playlist-creation-title": "Fehler beim Erstellen der Playlist",
"error-playlist-creation-desc": "Beim Erstellen der Playlist ist ein Fehler aufgetreten.",
"playlist-created-title": "Playlist erstellt",
"playlist-created-desc": "Die Playlist wurde erfolgreich erstellt. Sie können jetzt ihre Karten synchronisieren!",
"playlist-created-desc": "Die Playlist wurde erfolgreich erstellt. Du kannst jetzt ihre Karten synchronisieren!",
"download-playlist": "Playlist herunterladen",
"synchronize-playlist": "Playlist synchronisieren",
"synchronize-maps": "Karten synchronisieren",
@@ -1024,7 +1092,7 @@
"playlist-edit-error-title": "Fehler beim Bearbeiten der Playlist",
"playlist-edit-error-desc": "Beim Bearbeiten der Playlist ist ein Fehler aufgetreten.",
"playlist-edited-title": "Playlist bearbeitet!",
"playlist-edited-desc": "Die Playlist wurde erfolgreich geändert. Sie können jetzt ihre Karten synchronisieren!",
"playlist-edited-desc": "Die Playlist wurde erfolgreich geändert. Du kannst jetzt ihre Karten synchronisieren!",
"playlists-loading": "Playlists werden geladen...",
"no-playlists": "Keine Playlists",
"download-playlists": "Playlists herunterladen",
@@ -1033,55 +1101,55 @@
"cancel-download": "Download abbrechen",
"open-file": "Datei öffnen",
"link-playlists": "Playlists verknüpfen",
"link-playlist-desc": "Das Verknüpfen von Playlists ermöglicht das Teilen von Playlists zwischen allen Versionen. Nach der Verknüpfung profitiert diese Version von geteilten Playlists",
"link-playlist-info": "Das Hinzufügen und Löschen von Playlists wird ebenfalls geteilt",
"link-playlist-desc": "Das Verknüpfen von Playlists ermöglicht das Teilen von Playlists zwischen allen Versionen. Nach der Verknüpfung profitiert diese Version von geteilten Playlists.",
"link-playlist-info": "Das Hinzufügen und Löschen von Playlists wird ebenfalls geteilt.",
"keep-playlists": "Playlists behalten",
"keep-playlists-tip": "Wenn Sie Playlists behalten, werden die Playlists von der aktuellen Version in den Ordner für geteilte Playlists verschoben. Andernfalls gehen sie verloren",
"keep-playlists-tip": "Wenn du Playlists behältst, werden die Playlists von der aktuellen Version in den Ordner für geteilte Playlists verschoben. Andernfalls gehen sie verloren.",
"unlink-playlists": "Playlists trennen",
"unlink-playlist-desc": "Achtung, das Trennen von Playlists verhindert die Nutzung geteilter Playlists für diese Version.",
"unlink-keep-playlists-tip": "Wenn Sie Playlists behalten, wird eine Kopie der geteilten Playlists für die aktuelle Version erstellt. Andernfalls werden keine Playlists für diese Version behalten.",
"unlink-keep-playlists-tip": "Wenn du Playlists behältst, wird eine Kopie der geteilten Playlists für die aktuelle Version erstellt. Andernfalls werden keine Playlists für diese Version behalten.",
"delete-playlist-ask": "Playlist löschen?",
"delete-playlists-ask": "Playlists löschen?",
"delete-playlist-desc": "Sind Sie sicher, dass Sie die Playlist \"{playlistTitle}\" löschen möchten?",
"delete-playlists-desc": "Sind Sie sicher, dass Sie {nb} Playlists löschen möchten?",
"delete-playlist-desc": "Bist du dir sicher, dass du die Playlist \"{playlistTitle}\" löschen möchtest?",
"delete-playlists-desc": "Bist du dir sicher, dass du {nb} Playlists löschen möchtest?",
"delete-maps": "Karten löschen",
"delete-playlist-maps-tip": "Wenn aktiviert, werden alle Karten in der Playlist gelöscht",
"delete-playlists-maps-tip": "Wenn aktiviert, werden alle Karten in den Playlists gelöscht",
"delete-playlist-maps-tip": "Wenn aktiviert, werden alle Karten in der Playlist gelöscht.",
"delete-playlists-maps-tip": "Wenn aktiviert, werden alle Karten in den Playlists gelöscht.",
"export-playlist-ask": "Playlist exportieren?",
"export-playlists-ask": "Playlists exportieren?",
"export-playlist-desc": "Sind Sie sicher, dass Sie die Playlist \"{playlistTitle}\" exportieren möchten?",
"export-playlists-desc": "Sind Sie sicher, dass Sie {nb} Playlists exportieren möchten?",
"export-playlist-desc": "Bist du dir sicher, dass du die Playlist \"{playlistTitle}\" exportieren möchtest?",
"export-playlists-desc": "Bist du dir sicher, dass du {nb} Playlists exportieren möchtest?",
"export-maps": "Karten exportieren",
"export-playlist-maps-tip": "Wenn aktiviert, werden auch alle Karten in der Playlist exportiert",
"export-playlists-maps-tip": "Wenn aktiviert, werden auch alle Karten in den Playlists exportiert",
"export-playlist-maps-tip": "Wenn aktiviert, werden auch alle Karten in der Playlist exportiert.",
"export-playlists-maps-tip": "Wenn aktiviert, werden auch alle Karten in den Playlists exportiert.",
"export": "Exportieren",
"need-clone-title": "Warnung",
"need-clone-desc-1": "Diese Playlist wurde von einer externen Seite heruntergeladen und enthält einen Synchronisierungslink.",
"need-clone-desc-2": "Um Ihre Änderungen während der Synchronisierung nicht zu verlieren, wird die Playlist dupliziert und ihr Synchronisierungslink entfernt.",
"need-clone-desc-3": "Sie können dann, wenn Sie möchten, die ursprüngliche Playlist löschen.",
"need-clone-desc-2": "Um deine Änderungen während der Synchronisierung nicht zu verlieren, wird die Playlist dupliziert und ihr Synchronisierungslink entfernt.",
"need-clone-desc-3": "Du kannst dann, wenn du möchtest, die ursprüngliche Playlist löschen.",
"understood": "Ich verstehe",
"synchronize-playlist-ask": "Playlist synchronisieren?",
"synchronize-playlists-ask": "Playlists synchronisieren?",
"synchronize-playlist-desc": "Sind Sie sicher, dass Sie die Playlist \"{playlistTitle}\" synchronisieren möchten?",
"synchronize-playlists-desc": "Sind Sie sicher, dass Sie {nb} Playlists synchronisieren möchten?",
"synchronize-playlist-tip": "Diese Aktion aktualisiert Playlists und lädt fehlende Karten herunter; es kann mehrere Minuten dauern.",
"synchronize-playlist-desc": "Bist du sicher, dass du die Playlist \"{playlistTitle}\" synchronisieren möchtest?",
"synchronize-playlists-desc": "Bist du sicher, dass du {nb} Playlists synchronisieren möchtest?",
"synchronize-playlist-tip": "Diese Aktion aktualisiert Playlists und lädt fehlende Karten herunter. Dies kann mehrere Minuten dauern.",
"synchronize": "Synchronisieren",
"curated": "Empfohlen",
"verified-mapper": "Verifizierter Mapper",
"empty-playlists": "Leere Playlists",
"search-playlist": "Nach einer Playlist suchen",
"no-playlists-found": "Keine Playlists gefunden",
"error-occur-while-loading-playlists": "Beim Laden der Playlists ist ein Fehler aufgetreten",
"error-occur-while-loading-playlist": "Beim Laden der Playlist ist ein Fehler aufgetreten",
"no-playlists-found": "Keine Playlists gefunden.",
"error-occur-while-loading-playlists": "Beim Laden der Playlists ist ein Fehler aufgetreten.",
"error-occur-while-loading-playlist": "Beim Laden der Playlist ist ein Fehler aufgetreten.",
"loading-maps": "Karten werden geladen...",
"no-maps-found-for-playlist": "Keine Karten für diese Playlist gefunden",
"playlist-contain-no-maps": "Die Playlist enthält keine Karten",
"no-map-installed-for-playlist": "Keine Karten für diese Playlist installiert",
"playlist-is-waiting-to-download": "Die Playlist wartet auf den Download",
"no-maps-found-for-playlist": "Keine Karten für diese Playlist gefunden.",
"playlist-contain-no-maps": "Die Playlist enthält keine Karten.",
"no-map-installed-for-playlist": "Keine Karten für diese Playlist installiert.",
"playlist-is-waiting-to-download": "Die Playlist wartet auf den Download.",
"download-maps": "Karten herunterladen",
"download-missing-maps": "Fehlende Karten herunterladen",
"playlist-is-downloading": "Die Playlist wird heruntergeladen",
"some-playlist-maps-are-missing": "Einige Karten in dieser Playlist fehlen",
"playlist-is-downloading": "Die Playlist wird heruntergeladen.",
"some-playlist-maps-are-missing": "Einige Karten in dieser Playlist fehlen!",
"create-a-playlist": "Eine Playlist erstellen",
"synchronize-playlists": "Playlists synchronisieren",
"export-playlists": "Playlists exportieren",
@@ -1096,11 +1164,11 @@
"save": "Speichern",
"loading": "Wird geladen...",
"installed": "Installiert",
"no-map-found": "Keine Karte gefunden",
"edit-playlist-shortcuts": "Halten Sie Shift oder Strg gedrückt, um mehrere Karten auszuwählen",
"no-map-found": "Keine Karte gefunden!",
"edit-playlist-shortcuts": "Halte Shift oder Strg gedrückt, um mehrere Karten auszuwählen.",
"add-to-playlist": "Zur Playlist hinzufügen",
"remove-from-playlist": "Aus Playlist entfernen",
"playlist-is-empty": "Die Playlist ist leer",
"playlist-is-empty": "Die Playlist ist leer.",
"continue": "Fortfahren",
"nb-maps": "Anzahl der Karten",
"nb-mappers": "Anzahl der Mapper",
+72 -4
View File
@@ -14,7 +14,8 @@
"refuse": "Refuse",
"apply": "Apply",
"copy": "Copy",
"copied": "Copied!"
"copied": "Copied!",
"confirm": "Confirm"
},
"nav-bar": {
"add-version": "Add a version",
@@ -44,7 +45,8 @@
"filters-btn": "Filters",
"dropdown": {
"export-maps": "Export maps",
"delete-maps": "Delete maps"
"delete-maps": "Delete maps",
"delete-duplicate-maps": "Delete duplicates"
}
},
"tabs": {
@@ -86,6 +88,12 @@
"uninstall-all": "Uninstall all"
}
}
},
"notifications": {
"all-mods-already-installed": {
"title": "Mods already installed",
"description": "All selected mods are already installed"
}
}
},
"dropdown": {
@@ -113,6 +121,7 @@
"title": "Steam & Oculus",
"description": "Logging out will allow you to switch accounts on the next Beat Saber download.",
"logout": "Log out",
"logout-success": "Logout Successful",
"download-platform": {
"title": "Default Platform",
"desc": "Choose the default platform that will be used to download versions of Beat Saber.",
@@ -132,7 +141,7 @@
},
"installation-folder": {
"title": "Installation folder",
"description": "Change the default folder for Beat Saber versions and other upcoming features.",
"description": "Change the folder that will contain all the content downloaded by BSManager.",
"choose-folder": "Choose folder"
},
"proton-path": {
@@ -200,6 +209,34 @@
"changelogs": {
"open" : "🚀 Check Out the Changelog!",
"not-founds": "😕 Oops, No Changelog Here!"
},
"advanced": {
"title": "Advanced",
"description": "Advanced settings for BSManager.",
"hardware-acceleration": {
"title": "Hardware Acceleration",
"description": "Enable Hardware Acceleration to use your GPU and improve BSManager's performance. Turn this off if you're experiencing frame drops.",
"modal": {
"title": "Restart Needed",
"body": "Changing hardware acceleration setting will quit and re-launch BSManager. Are you sure you want to do this?",
"confirm-btn": "Yes I'm sure"
},
"error-notification": {
"message": "An error occur, unable to disable hardware acceleration."
}
},
"use-symlinks": {
"title": "Use Symlinks",
"description": "Use Symlinks instead of Junctions to link folders. Turn this on only if you really need it.",
"modal": {
"title": "Symlink Permissions",
"body": "When creating symlinks, BSManager will require administrator privileges or developer mode enabled on your system. Are you sure you want to continue?",
"confirm-btn": "Yes I'm sure"
},
"error-notification": {
"message": "An error occur, unable to change symlinks settings."
}
}
}
}
},
@@ -413,7 +450,8 @@
"CantEditSteam": "Unable to edit",
"CantRename": "Renaming impossible",
"VersionAlreadExist": "This version already exists",
"CantClone": "Cloning impossible"
"CantClone": "Cloning impossible",
"UnknownError": "An unknown error occurred"
},
"msg": {
"CantEditSteam": "You can't edit the Steam version. You can clone it though."
@@ -466,6 +504,14 @@
"one-click-install": {
"success": "Map installation complete",
"error": "An error occurred while installing the map"
},
"no-duplicates-maps": {
"title": "No Duplicates",
"msg": "No maps were deleted"
},
"duplicates-maps-deleted": {
"title": "Duplicates Deleted",
"msg": "Duplicates were deleted"
}
},
"playlists": {
@@ -486,6 +532,15 @@
"title": "Backup created",
"msg": "Sharing the 'UserData' folder can generate errors, in case of problems unlink the folder to restore the backup"
}
},
"linking-error": {
"title": "Error while linking folder",
"msg": {
"EPERM": "BSManager does not have the necessary permissions to link the folder.",
"EACCES": "BSManager does not have the necessary permissions to link the folder.",
"ENOSPC": "The disk is full, make some space and try again.",
"UNKNOWN_ERROR":"An unknown error has occurred while linking the folder."
}
}
},
"create-launch-shortcut": {
@@ -651,6 +706,11 @@
"title": "Keeping maps will copy all maps from the shared folder to the current version after unlinking. Maps will not be lost if this is disabled."
},
"valid-btn": "Unlink maps"
},
"delete-duplicate-maps": {
"title": "Delete maps?",
"desc": "Only the map \"{map}\" is a duplicate. Are you sure you want to delete it?",
"desc-plural": "{nb} duplicate maps have been found. Are you sure you want to delete them?"
}
},
"download-maps": {
@@ -731,12 +791,20 @@
},
"launch-as-admin": "Launch as Administrator",
"not-remind-me": "Do not remind me"
},
"ask-install-path": {
"title": "Installation folder",
"choose-folder-description": "Choose the folder that will contain all the content downloaded by BSManager. (versions, mods, maps, playlists, etc.)",
"choose-folder": "Choose folder",
"default": "Default",
"default-tooltip": "Defaults to your home folder"
}
},
"maps": {
"map-filter-panel": {
"duration": "Duration",
"nps" : "Notes Per Second",
"njs": "Note Jump Speed",
"tags": "tags",
"specificities": "general",
"requirements": "requirements",
+72 -4
View File
@@ -14,7 +14,8 @@
"refuse": "Rechazar",
"apply": "Aplicar",
"copy": "Copiar",
"copied": "¡Copiado!"
"copied": "¡Copiado!",
"confirm": "Confirmar"
},
"nav-bar": {
"add-version": "Agregar una versión",
@@ -44,7 +45,8 @@
"filters-btn": "Filtros",
"dropdown": {
"export-maps": "Exportar mapas",
"delete-maps": "Borrar mapas"
"delete-maps": "Borrar mapas",
"delete-duplicate-maps": "Borrar duplicados"
}
},
"tabs": {
@@ -86,6 +88,12 @@
"uninstall-all": "Desinstalar todos"
}
}
},
"notifications": {
"all-mods-already-installed": {
"title": "Mods ya instalados",
"description": "Todos los mods seleccionados ya están instalados"
}
}
},
"dropdown": {
@@ -113,6 +121,7 @@
"title": "Steam & Oculus",
"description": "Desconectarte te permitirá cambiar de cuenta en la próxima descarga de Beat Saber.",
"logout": "Cerrar sesión",
"logout-success": "Cierre de sesión exitoso",
"download-platform": {
"title": "Plataforma predeterminada",
"desc": "Elige la plataforma predeterminada que se utilizará para descargar las versiones de Beat Saber.",
@@ -132,7 +141,7 @@
},
"installation-folder": {
"title": "Carpeta de instalación",
"description": "Cambia la carpeta por defecto para las versiones de Beat Saber y próximas funciones.",
"description": "Cambiar la carpeta que contendrá todo el contenido descargado por BSManager.",
"choose-folder": "Elige la carpeta"
},
"additional-content": {
@@ -195,6 +204,34 @@
"changelogs": {
"open" : "🚀 ¡Descubre lo nuevo en el Changelog!",
"not-founds": "😕 Uy, ¡Changelog no encontrado!"
},
"advanced": {
"title": "Avanzados",
"description": "Configuraciones avanzadas para BSManager.",
"hardware-acceleration": {
"title": "Aceleración de hardware",
"description": "Habilite la aceleración de hardware para usar su GPU y mejorar el rendimiento de BSManager. Desactive esta opción si experimenta caídas de fotogramas.",
"modal": {
"title": "Reinicio necesario",
"body": "Cambiar la configuración de aceleración de hardware cerrará y reiniciará BSManager. ¿Estás seguro de que quieres hacer esto?",
"confirm-btn": "Sí, estoy seguro"
},
"error-notification": {
"message": "Ocurrió un error, no se puede desactivar la aceleración de hardware."
}
},
"use-symlinks": {
"title": "Usar enlaces simbólicos",
"description": "Utilice enlaces simbólicos en lugar de uniones para enlazar carpetas. Actívelo solo si realmente lo necesita.",
"modal": {
"title": "Permisos de enlace simbólico",
"body": "Al crear enlaces simbólicos, BSManager requerirá privilegios de administrador o el modo desarrollador activado en su sistema. ¿Estás seguro de que quieres continuar?",
"confirm-btn": "Sí, estoy seguro"
},
"error-notification": {
"message": "Ocurrió un error, no se pueden cambiar los ajustes de los enlaces simbólicos."
}
}
}
}
},
@@ -406,7 +443,8 @@
"CantEditSteam": "Imposible editar",
"CantRename": "Cambio de nombre imposible",
"VersionAlreadExist": "Esta versión ya existe",
"CantClone": "Clonación imposible"
"CantClone": "Clonación imposible",
"UnknownError": "Ocurrió un error desconocido"
},
"msg": {
"CantEditSteam": "No puedes editar la versión de Steam. Sin embargo, puedes clonarla."
@@ -459,6 +497,14 @@
"one-click-install": {
"success": "Instalación del mapa completada",
"error": "Se produjo un error durante la instalación del mapa"
},
"no-duplicates-maps": {
"title": "Sin duplicados",
"msg": "No se eliminó ninguna carta"
},
"duplicates-maps-deleted": {
"title": "Duplicados eliminados",
"msg": "Se eliminaron los duplicados"
}
},
"playlists": {
@@ -479,6 +525,15 @@
"title": "Backup creado",
"msg": "Compartir la carpeta 'UserData' puede generar errores, en caso de problemas desvincula la carpeta para restaurar la copia de seguridad"
}
},
"linking-error": {
"title": "Error al enlazar la carpeta",
"msg": {
"EPERM": "BSManager no tiene los permisos necesarios para enlazar la carpeta.",
"EACCES": "BSManager no tiene los permisos necesarios para enlazar la carpeta.",
"ENOSPC": "El disco está lleno, libera espacio e inténtalo de nuevo.",
"UNKNOWN_ERROR": "Se ha producido un error desconocido al enlazar la carpeta."
}
}
},
"create-launch-shortcut": {
@@ -644,6 +699,11 @@
"title": "Mantener los mapas creará una copia de los mapas compartidos para la versión actual. De lo contrario, no se mantendrá ningún mapa para esta versión."
},
"valid-btn": "Desvincular mapas"
},
"delete-duplicate-maps": {
"title": "¿Borrar mapas?",
"desc": "Solo el mapa \"{map}\" está duplicado. ¿Estás seguro de que quieres eliminarlo?",
"desc-plural": "Se han encontrado {nb} mapas duplicados. ¿Estás seguro de que quieres eliminarlos?"
}
},
"download-maps": {
@@ -724,12 +784,20 @@
},
"launch-as-admin": "Iniciar como Administrador",
"not-remind-me": "No volver a recordármelo"
},
"ask-install-path": {
"title": "Carpeta de instalación",
"choose-folder-description": "Elija la carpeta que contendrá todo el contenido descargado por BSManager. (versiones, mods, mapas, listas de reproducción, etc.)",
"choose-folder": "Elige la carpeta",
"default": "Predeterminado",
"default-tooltip": "Por defecto, en su carpeta personal"
}
},
"maps": {
"map-filter-panel": {
"duration": "Duración",
"nps" : "Notas Por Segundo",
"njs": "Velocidad de salto de nota",
"tags": "tags",
"specificities": "general",
"requirements": "requisitos",
+73 -4
View File
@@ -14,7 +14,8 @@
"refuse": "Refuser",
"apply": "Appliquer",
"copy": "Copier",
"copied": "Copié !"
"copied": "Copié !",
"confirm": "Confirmer"
},
"nav-bar": {
"add-version": "Ajouter une version",
@@ -44,7 +45,8 @@
"filters-btn": "Filtres",
"dropdown": {
"export-maps": "Exporter les maps",
"delete-maps": "Supprimer les maps"
"delete-maps": "Supprimer les maps",
"delete-duplicate-maps": "Supprimer les doublons"
}
},
"tabs": {
@@ -86,6 +88,12 @@
"uninstall-all": "Tout désinstaller"
}
}
},
"notifications": {
"all-mods-already-installed": {
"title": "Mods déjà installées",
"description": "Tous les mods séléctionnées sont déjà installées"
}
}
},
"dropdown": {
@@ -113,6 +121,7 @@
"title": "Steam & Oculus",
"description": "Te déconnecter te permettra de changer de compte au prochain téléchargement de Beat Saber.",
"logout": "Déconnexion",
"logout-success": "Déconnexion réussie",
"download-platform": {
"title": "Plateforme par défaut",
"desc": "Choisi la plateforme par défaut qui sera utilisée pour télécharger les versions de Beat Saber.",
@@ -132,7 +141,7 @@
},
"installation-folder": {
"title": "Dossier d'installation",
"description": "Change le dossier par défaut pour les versions de Beat Saber et d'autres fonctionnalités à venir.",
"description": "Changer le dossier qui contiendra tout le contenu téléchargé par BSManager.",
"choose-folder": "Choisir un dossier"
},
"additional-content": {
@@ -195,6 +204,34 @@
"changelogs": {
"open" : "🚀 Jetez un œil au Changelog !",
"not-founds": "😕 Oups, Changelog introuvable !"
},
"advanced": {
"title": "Avancés",
"description": "Paramètres avancés pour BSManager.",
"hardware-acceleration": {
"title": "Accélération matérielle",
"description": "Activez l'accélération matérielle pour utiliser votre GPU et améliorer les performances de BSManager. Désactivez cette option si vous rencontrez des chutes d'IPS.",
"modal": {
"title": "Redémarrage nécessaire",
"body": "Changer le paramètre d'accélération matérielle va quitter et relancer BSManager. Êtes-vous sûr de vouloir continuer ?",
"confirm-btn": "Oui, je suis sûr"
},
"error-notification": {
"message": "Une erreur s'est produite, impossible de désactiver l'accélération matérielle."
}
},
"use-symlinks": {
"title": "Utiliser des liens symboliques",
"description": "Utilisez des liens symboliques au lieu de jonctions pour lier des dossiers. Activez cette option uniquement si vous en avez besoin.",
"modal": {
"title": "Permissions des liens symboliques",
"body": "Lors de la création de liens symboliques, BSManager nécessitera des privilèges administrateur ou le mode développeur activé sur votre système. Êtes-vous sûr de vouloir continuer ?",
"confirm-btn": "Oui, je suis sûr"
},
"error-notification": {
"message": "Une erreur s'est produite, impossible de changer les paramètres des liens symboliques."
}
}
}
}
},
@@ -406,7 +443,8 @@
"CantEditSteam": "Modification impossible",
"CantRename": "Renommage impossible",
"VersionAlreadExist": "Cette version existe déjà",
"CantClone": "Clonage impossible"
"CantClone": "Clonage impossible",
"UnknownError": "Une erreur inconnue s'est produite"
},
"msg": {
"CantEditSteam": "Tu ne peux pas modifier la version Steam, cependant tu peux la cloner."
@@ -459,6 +497,14 @@
"one-click-install": {
"success": "Installation de la map terminée",
"error": "Une erreur s'est produite lors de l'installation de la map"
},
"no-duplicates-maps": {
"title": "Pas de doublons",
"msg": "Aucune carte n'a été supprimée"
},
"duplicates-maps-deleted": {
"title": "Doublons supprimés",
"msg": "Les doublons ont été supprimés"
}
},
"playlists": {
@@ -479,7 +525,17 @@
"title": "Sauvegarde créée",
"msg": "Le partage du dossier 'UserData' peut générer des erreurs, en cas de soucis déliez le dossier pour restaurer la sauvegarde"
}
},
"linking-error": {
"title": "Erreur lors de la liaison du dossier",
"msg": {
"EPERM": "BSManager n'a pas les autorisations nécessaires pour lier le dossier.",
"EACCES": "BSManager n'a pas les autorisations nécessaires pour lier le dossier.",
"ENOSPC": "Le disque est plein, faites de la place et réessayez.",
"UNKNOWN_ERROR": "Une erreur inconnue est survenue lors de la liaison du dossier."
}
}
},
"create-launch-shortcut": {
"success": {
@@ -644,6 +700,11 @@
"title": "Conserver les maps créera une copie des maps partagées pour la version actuelle. Dans le cas contraire, aucune map ne sera conservée pour cette version."
},
"valid-btn": "Délier les maps"
},
"delete-duplicate-maps": {
"title": "Supprimer les maps ?",
"desc": "Seule la map \"{map}\" est en double. Es-tu sûr de vouloir la supprimer ?",
"desc-plural": "{nb} maps en double ont été trouvées. Es-tu sûr de vouloir les supprimer ?"
}
},
"download-maps": {
@@ -724,12 +785,20 @@
},
"launch-as-admin": "Lancer en administateur",
"not-remind-me": "Ne plus me rappeler"
},
"ask-install-path": {
"title": "Dossier d'installation",
"choose-folder-description": "Choisissez le dossier qui contiendra tout le contenu téléchargé par BSManager. (versions, mods, cartes, playlists, etc.)",
"choose-folder": "Choisir un dossier",
"default": "Par défaut",
"default-tooltip": "Par défaut, dans votre dossier personnel"
}
},
"maps": {
"map-filter-panel": {
"duration": "Durée",
"nps" : "Notes Par Seconde",
"njs": "Vitesse de saut des notes",
"tags": "tags",
"specificities": "général",
"requirements": "requis",
+72 -4
View File
@@ -14,7 +14,8 @@
"refuse": "拒否する",
"apply": "適用",
"copy": "コピー",
"copied": "コピー済み!"
"copied": "コピー済み!",
"confirm": "確認"
},
"nav-bar": {
"add-version": "バージョンを追加",
@@ -44,7 +45,8 @@
"filters-btn": "絞り込み",
"dropdown": {
"export-maps": "マップをエクスポート",
"delete-maps": "マップを削除"
"delete-maps": "マップを削除",
"delete-duplicate-maps": " 重複を削除"
}
},
"tabs": {
@@ -86,6 +88,12 @@
"uninstall-all": "全てアンインストールする"
}
}
},
"notifications": {
"all-mods-already-installed": {
"title": "すでにインストール済みのMOD",
"description": "選択したすべてのMODはすでにインストールされています"
}
}
},
"dropdown": {
@@ -113,6 +121,7 @@
"title": "Steam & Oculus",
"description": "ログアウトすると、次のBeat Saberダウンロード時にアカウントを切り替えることができます。",
"logout": "ログアウト",
"logout-success": "ログアウト成功",
"download-platform": {
"title": "デフォルトプラットフォーム",
"desc": "Beat Saberのダウンロードに使用するデフォルトのプラットフォームを選択します。",
@@ -132,7 +141,7 @@
},
"installation-folder": {
"title": "インストールフォルダー",
"description": "BeatSaberのバージョンとその他の今後の機能を入れるフォルダを変更します。",
"description": "BSManager によってダウンロードされたすべてのコンテンツを含むフォルダを変更します。",
"choose-folder": "フォルダーを選択"
},
"additional-content": {
@@ -195,6 +204,34 @@
"changelogs": {
"open" : "🚀 チェンジログを覗いてみて!",
"not-founds": "😕 あれ、チェンジログが見当たらない!"
},
"advanced": {
"title": "高度な設定",
"description": "BSManagerの高度な設定。",
"hardware-acceleration": {
"title": "ハードウェアアクセラレーション",
"description": "ハードウェアアクセラレーションを有効にしてGPUを使用し、BSManagerのパフォーマンスを向上させます。フレームドロップが発生している場合は、これをオフにしてください。",
"modal": {
"title": "再起動が必要",
"body": "ハードウェアアクセラレーションの設定を変更すると、BSManagerが終了して再起動します。本当に続行しますか?",
"confirm-btn": "はい、確かです"
},
"error-notification": {
"message": "エラーが発生しました。ハードウェアアクセラレーションを無効にできません。"
}
},
"use-symlinks": {
"title": "シンボリックリンクを使用",
"description": "フォルダをリンクするためにジャンクションの代わりにシンボリックリンクを使用します。本当に必要な場合にのみこれをオンにしてください。",
"modal": {
"title": "シンボリックリンクの権限",
"body": "シンボリックリンクを作成する際、BSManagerは管理者権限またはシステムで有効になっている開発者モードを必要とします。本当に続行しますか?",
"confirm-btn": "はい、確かです"
},
"error-notification": {
"message": "エラーが発生しました。シンボリックリンク設定を変更できません。"
}
}
}
}
},
@@ -406,7 +443,8 @@
"CantEditSteam": "編集不可",
"CantRename": "改名不可能",
"VersionAlreadExist": "このバージョンは既に存在しています!",
"CantClone": "クローン作成不可"
"CantClone": "クローン作成不可",
"UnknownError": "不明なエラーが発生しました"
},
"msg": {
"CantEditSteam": "Steam版は編集できませんがクローンを作ることは可能です。"
@@ -459,6 +497,14 @@
"one-click-install": {
"success": "マップのインストール完了",
"error": "マップのインストール中にエラーが発生しました"
},
"no-duplicates-maps": {
"title": "重複なし",
"msg": "マップは削除されませんでした"
},
"duplicates-maps-deleted": {
"title": "重複削除",
"msg": "重複が削除されました"
}
},
"playlists": {
@@ -479,6 +525,15 @@
"title": "バックアップを作成しました",
"msg": "'UserData'フォルダを共有するとエラーが発生する可能性があります。問題が発生した場合は、フォルダをリンク解除してバックアップを復元してください。"
}
},
"linking-error": {
"title": "フォルダのリンク中にエラーが発生しました",
"msg": {
"EPERM": "BSManagerにはフォルダをリンクするための必要な権限がありません。",
"EACCES": "BSManagerにはフォルダをリンクするための必要な権限がありません。",
"ENOSPC": "ディスクがいっぱいです。空き容量を作ってもう一度試してください。",
"UNKNOWN_ERROR": "フォルダをリンク中に不明なエラーが発生しました。"
}
}
},
"create-launch-shortcut": {
@@ -644,6 +699,11 @@
"title": "マップを保持は、リンクを解除した後、共有フォルダから現在のバージョンにすべてのマップをコピーします。これを無効をしても共有マップは失われません。"
},
"valid-btn": "マップのリンクを解除"
},
"delete-duplicate-maps": {
"title": "マップを削除しますか?",
"desc": "マップ「{map}」のみが重複しています。削除してもよろしいですか?",
"desc-plural": "{nb}個の重複したマップが見つかりました。削除してもよろしいですか?"
}
},
"download-maps": {
@@ -724,12 +784,20 @@
},
"launch-as-admin": "管理者として起動",
"not-remind-me": "Больше не напоминать"
},
"ask-install-path": {
"title": "インストールフォルダー",
"choose-folder-description": "BSManager によってダウンロードされたすべてのコンテンツ (バージョン、MOD、マップ、プレイリストなど) を含むフォルダを選択してください。",
"choose-folder": "フォルダーを選択",
"default": "デフォルト",
"default-tooltip": "デフォルトでは、ホームフォルダに設定されます"
}
},
"maps": {
"map-filter-panel": {
"duration": "尺",
"nps" : "秒間ノート数",
"njs": "ノートジャンプ速度",
"tags": "タグ",
"specificities": "一般",
"requirements": "要Mod",
+72 -4
View File
@@ -14,7 +14,8 @@
"refuse": "Отказаться",
"apply": "Применить",
"copy": "Скопировать",
"copied": "Скопировано!"
"copied": "Скопировано!",
"confirm": "Подтвердить"
},
"nav-bar": {
"add-version": "Добавить версию игры",
@@ -44,7 +45,8 @@
"filters-btn": "Фильтр",
"dropdown": {
"export-maps": "Экспорт карт",
"delete-maps": "Удалить карты"
"delete-maps": "Удалить карты",
"delete-duplicate-maps": "Удалить дубликаты"
}
},
"tabs": {
@@ -86,6 +88,12 @@
"uninstall-all": "Удалить всё"
}
}
},
"notifications": {
"all-mods-already-installed": {
"title": "Моды уже установлены",
"description": "Все выбранные моды уже установлены"
}
}
},
"dropdown": {
@@ -113,6 +121,7 @@
"title": "Steam & Oculus",
"description": "Выход позволит вам сменить учетную запись при следующей загрузке Beat Saber.",
"logout": "Выйти",
"logout-success": "Выход выполнен успешно",
"download-platform": {
"title": "Основная платформа",
"desc": "Выберите основную платформу, которая будет использоваться для скачивания версий Beat Saber.",
@@ -132,7 +141,7 @@
},
"installation-folder": {
"title": "Папка установок",
"description": "Измените стандартную папку, где будут версии Beat Saber и прочее.",
"description": "Изменить папку, которая будет содержать весь контент, загруженный BSManager.",
"choose-folder": "Изменить папку"
},
"additional-content": {
@@ -195,6 +204,34 @@
"changelogs": {
"open" : "🚀 Изучаем Changelog!",
"not-founds": "😕 Ой, Changelog нигде не видно!"
},
"advanced": {
"title": "Дополнительные",
"description": "Дополнительные настройки для BSManager.",
"hardware-acceleration": {
"title": "Аппаратное ускорение",
"description": "Включите аппаратное ускорение, чтобы использовать ваш GPU и улучшить производительность BSManager. Отключите эту опцию, если у вас возникают пропуски кадров.",
"modal": {
"title": "Требуется перезагрузка",
"body": "Изменение настройки аппаратного ускорения приведет к завершению и перезапуску BSManager. Вы уверены, что хотите это сделать?",
"confirm-btn": "Да, я уверен"
},
"error-notification": {
"message": "Произошла ошибка, невозможно отключить аппаратное ускорение."
}
},
"use-symlinks": {
"title": "Использовать символические ссылки",
"description": "Используйте символические ссылки вместо соединений для связывания папок. Включайте эту опцию только если это действительно необходимо.",
"modal": {
"title": "Разрешения для символических ссылок",
"body": "При создании символических ссылок BSManager потребуется права администратора или включенный режим разработчика на вашем устройстве. Вы уверены, что хотите продолжить?",
"confirm-btn": "Да, я уверен"
},
"error-notification": {
"message": "Произошла ошибка, невозможно изменить настройки символических ссылок."
}
}
}
}
},
@@ -406,7 +443,8 @@
"CantEditSteam": "Не удалось изменить",
"CantRename": "Переименование невозможно",
"VersionAlreadExist": "Эта версия уже добавлена",
"CantClone": "Клонирование невозможно"
"CantClone": "Клонирование невозможно",
"UnknownError": "Произошла неизвестная ошибка"
},
"msg": {
"CantEditSteam": "Вы не можете изменить версию из Steam, но вы можете её клонировать."
@@ -459,6 +497,14 @@
"one-click-install": {
"success": "Карта установлена",
"error": "Ошибка установки карты"
},
"no-duplicates-maps": {
"title": "Нет дубликатов",
"msg": "Ни одна карта не была удалена"
},
"duplicates-maps-deleted": {
"title": "Дубликаты удалены",
"msg": "Дубликаты были удалены"
}
},
"playlists": {
@@ -479,6 +525,15 @@
"title": "Бэкап создан",
"msg": "Общая папка 'UserData' может создавать ошибки. В случае проблем отвяжите папку, чтобы восстановить бэкап."
}
},
"linking-error": {
"title": "Ошибка при связывании папки",
"msg": {
"EPERM": "BSManager не имеет необходимых прав для связывания папки.",
"EACCES": "BSManager не имеет необходимых прав для связывания папки.",
"ENOSPC": "Диск заполнен, освободите место и попробуйте снова.",
"UNKNOWN_ERROR": "Произошла неизвестная ошибка при связывании папки."
}
}
},
"create-launch-shortcut": {
@@ -644,6 +699,11 @@
"title": "Общие карты будут скопированы в папку карт этой версии. Карты не будут потеряны, если это не выбрано."
},
"valid-btn": "Отвязать карты"
},
"delete-duplicate-maps": {
"title": "Удалить карту?",
"desc": "Только карта \"{map}\" является дубликатом. Вы уверены, что хотите ее удалить?",
"desc-plural": "Найдено {nb} дубликатов карт. Вы уверены, что хотите их удалить?"
}
},
"download-maps": {
@@ -723,12 +783,20 @@
"info-3": "Обратите внимание, что не рекомендуется предоставлять права администратора Steam, так как это также влияет на установленные игры и моды, представляя угрозу безопасности."
},
"launch-as-admin": "Запустить от имени администратора"
},
"ask-install-path": {
"title": "Папка установок",
"choose-folder-description": "Выберите папку, которая будет содержать весь контент, загруженный BSManager. (версии, моды, карты, плейлисты и т.д.)",
"choose-folder": "Изменить папку",
"default": "По умолчанию",
"default-tooltip": "По умолчанию в вашей домашней папке"
}
},
"maps": {
"map-filter-panel": {
"duration": "Длительность",
"nps" : "Нот в Секунду",
"njs": "Скорость прыжка нот",
"tags": "тэги",
"specificities": "основное",
"requirements": "требуемые моды",
+72 -4
View File
@@ -14,7 +14,8 @@
"refuse": "拒絕",
"apply": "應用",
"copy": "複製",
"copied": "已複製!"
"copied": "已複製!",
"confirm": "確認"
},
"nav-bar": {
"add-version": "新增版本",
@@ -44,7 +45,8 @@
"filters-btn": "篩選",
"dropdown": {
"export-maps": "導出譜面",
"delete-maps": "刪除譜面"
"delete-maps": "刪除譜面",
"delete-duplicate-maps": "刪除重複項"
}
},
"tabs": {
@@ -86,6 +88,12 @@
"uninstall-all": "全部移除"
}
}
},
"notifications": {
"all-mods-already-installed": {
"title": "模組已安裝",
"description": "所有選中的模組已經安裝"
}
}
},
"dropdown": {
@@ -113,6 +121,7 @@
"title": "Steam & Oculus",
"description": "登出後,您可以在下一次下載 Beat Saber 時切換帳戶。",
"logout": "登出",
"logout-success": "登出成功",
"download-platform": {
"title": ",預設平台",
"desc": "選擇要下載 BeatSaber 的預設平台。",
@@ -132,7 +141,7 @@
},
"installation-folder": {
"title": "安裝文件夾",
"description": "為 BeatSaber 不同版本及未來其他特性修改預設文件夾",
"description": "更改將包含 BSManager 下載的所有內容的文件夾",
"choose-folder": "選擇文件夾"
},
"additional-content": {
@@ -195,6 +204,34 @@
"changelogs": {
"open" : "🚀 探索更新日誌!",
"not-founds": "😕 噢,找不到更新日誌!"
},
"advanced": {
"title": "高级设置",
"description": "BSManager的高级设置。",
"hardware-acceleration": {
"title": "硬體加速",
"description": "啟用硬體加速以使用您的GPU並提高BSManager的性能。如果您遇到幀丟失,請關閉此功能。",
"modal": {
"title": "需要重啟",
"body": "更改硬體加速設置將退出並重新啟動BSManager。您確定要這樣做嗎?",
"confirm-btn": "是的,我確定"
},
"error-notification": {
"message": "發生錯誤,無法禁用硬體加速。"
}
},
"use-symlinks": {
"title": "使用符號鏈接",
"description": "使用符號鏈接而不是連接來鏈接文件夾。僅在確實需要時才啟用此功能。",
"modal": {
"title": "符號鏈接權限",
"body": "創建符號鏈接時,BSManager將需要管理員權限或啟用開發者模式。您確定要繼續嗎?",
"confirm-btn": "是的,我確定"
},
"error-notification": {
"message": "發生錯誤,無法更改符號鏈接設置。"
}
}
}
}
},
@@ -406,7 +443,8 @@
"CantEditSteam": "無法編輯",
"CantRename": "無法重命名",
"VersionAlreadExist": "該版本已存在",
"CantClone": "無法複製"
"CantClone": "無法複製",
"UnknownError": "發生了未知錯誤"
},
"msg": {
"CantEditSteam": "你不能編輯 Steam 版本。不過你可以複製它。"
@@ -459,6 +497,14 @@
"one-click-install": {
"success": "譜面安裝完成",
"error": "安裝譜面時發生錯誤"
},
"no-duplicates-maps": {
"title": "沒有重複",
"msg": "沒有刪除地圖"
},
"duplicates-maps-deleted": {
"title": "重複已刪除",
"msg": "重複已刪除"
}
},
"playlists": {
@@ -479,6 +525,15 @@
"title": "備份已創建",
"msg": "共享 “UserData” 文件夾可能會產生錯誤,如果出現問題,請取消關聯該文件夾以恢復備份"
}
},
"linking-error": {
"title": "連結文件夾時出錯",
"msg": {
"EPERM": "BSManager沒有必要的權限來連結文件夾。",
"EACCES": "BSManager沒有必要的權限來連結文件夾。",
"ENOSPC": "磁碟已滿,請騰出空間後再試。",
"UNKNOWN_ERROR": "連結文件夾時發生未知錯誤。"
}
}
},
"create-launch-shortcut": {
@@ -644,6 +699,11 @@
"title": "保留譜面將會在取消關聯後把共享文件夾的所有譜面複製到當前版本。如果此項被禁用,譜面也不會遺失。"
},
"valid-btn": "取消關聯譜面"
},
"delete-duplicate-maps": {
"title": "刪除地圖",
"desc": "只有地圖「{map}」是重複的。你確定要刪除它嗎?",
"desc-plural": "發現了 {nb} 個重複的地圖。你確定要刪除它們嗎?"
}
},
"download-maps": {
@@ -724,12 +784,20 @@
},
"launch-as-admin": "以管理員身份啟動",
"not-remind-me": "不再提醒我"
},
"ask-install-path": {
"title": "安裝文件夾",
"choose-folder-description": "選擇將包含 BSManager 下載的所有內容的文件夾。(版本、mod、地圖、播放列表等)",
"choose-folder": "選擇文件夾",
"default": "預設",
"default-tooltip": "預設為您的主資料夾"
}
},
"maps": {
"map-filter-panel": {
"duration": "時長",
"nps" : "每秒音符數",
"njs": "音符跳躍速度",
"tags": "標籤",
"specificities": "general",
"requirements": "要求",
+72 -4
View File
@@ -14,7 +14,8 @@
"refuse": "拒绝",
"apply": "应用",
"copy": "复制",
"copied": "已复制!"
"copied": "已复制!",
"confirm": "确认"
},
"nav-bar": {
"add-version": "添加版本",
@@ -44,7 +45,8 @@
"filters-btn": "筛选",
"dropdown": {
"export-maps": "导出谱面",
"delete-maps": "删除谱面"
"delete-maps": "删除谱面",
"delete-duplicate-maps": "删除重复项"
}
},
"tabs": {
@@ -86,6 +88,12 @@
"uninstall-all": "全部卸载"
}
}
},
"notifications": {
"all-mods-already-installed": {
"title": "模组已安装",
"description": "所有选中的模组已经安装"
}
}
},
"dropdown": {
@@ -113,6 +121,7 @@
"title": "Steam & Oculus",
"description": "登出后,您可以在下一次下载 Beat Saber 时切换帐户。",
"logout": "登出",
"logout-success": "注销成功",
"download-platform": {
"title": ",默认平台",
"desc": "选择要下载 BeatSaber 的默认平台。",
@@ -132,7 +141,7 @@
},
"installation-folder": {
"title": "安装文件夹",
"description": "为 BeatSaber 不同版本及未来其他特性修改默认文件夹",
"description": "更改将包含 BSManager 下载的所有内容的文件夹",
"choose-folder": "选择文件夹"
},
"additional-content": {
@@ -195,6 +204,34 @@
"changelogs": {
"open" : "🚀 探索更新日志!",
"not-founds": "😕 哎呀,找不到更新日志!"
},
"advanced": {
"title": "高级设置",
"description": "BSManager的高级设置。",
"hardware-acceleration": {
"title": "硬件加速",
"description": "启用硬件加速以使用您的GPU并提高BSManager的性能。如果您遇到帧丢失,请关闭此功能。",
"modal": {
"title": "需要重启",
"body": "更改硬件加速设置将退出并重新启动BSManager。您确定要这样做吗?",
"confirm-btn": "是的,我确定"
},
"error-notification": {
"message": "发生错误,无法禁用硬件加速。"
}
},
"use-symlinks": {
"title": "使用符号链接",
"description": "使用符号链接而不是联接来链接文件夹。仅在确实需要时才启用此功能。",
"modal": {
"title": "符号链接权限",
"body": "创建符号链接时,BSManager将需要管理员权限或启用开发者模式。您确定要继续吗?",
"confirm-btn": "是的,我确定"
},
"error-notification": {
"message": "发生错误,无法更改符号链接设置。"
}
}
}
}
},
@@ -406,7 +443,8 @@
"CantEditSteam": "无法编辑",
"CantRename": "无法重命名",
"VersionAlreadExist": "该版本已存在",
"CantClone": "无法克隆"
"CantClone": "无法克隆",
"UnknownError": "发生了未知错误"
},
"msg": {
"CantEditSteam": "你不能编辑 Steam 版本。不过你可以克隆它。"
@@ -459,6 +497,14 @@
"one-click-install": {
"success": "谱面安装完成",
"error": "安装谱面时发生错误"
},
"no-duplicates-maps": {
"title": "没有重复",
"msg": "没有删除地图"
},
"duplicates-maps-deleted": {
"title": "重复已删除",
"msg": "重复已删除"
}
},
"playlists": {
@@ -479,6 +525,15 @@
"title": "备份已创建",
"msg": "共享 “UserData” 文件夹可能会产生错误,如果出现问题,请取消关联该文件夹以恢复备份"
}
},
"linking-error": {
"title": "链接文件夹时出错",
"msg": {
"EPERM": "BSManager没有必要的权限来链接文件夹。",
"EACCES": "BSManager没有必要的权限来链接文件夹。",
"ENOSPC": "磁盘已满,请腾出空间后再试。",
"UNKNOWN_ERROR": "链接文件夹时发生未知错误。"
}
}
},
"create-launch-shortcut": {
@@ -644,6 +699,11 @@
"title": "保留谱面将会在取消关联后把共享文件夹的所有谱面复制到当前版本。如果此项被禁用,谱面也不会丢失。"
},
"valid-btn": "取消关联谱面"
},
"delete-duplicate-maps": {
"title": "删除地图",
"desc": "只有地图 \"{map}\" 是重复的。你确定要删除它吗?",
"desc-plural": "发现了 {nb} 个重复的地图。你确定要删除它们吗?"
}
},
"download-maps": {
@@ -724,12 +784,20 @@
},
"launch-as-admin": "以管理员身份启动",
"not-remind-me": "不再提醒我"
},
"ask-install-path": {
"title": "安装文件夹",
"choose-folder-description": "选择将包含 BSManager 下载的所有内容的文件夹。(版本、mod、地图、播放列表等)",
"choose-folder": "选择文件夹",
"default": "默认",
"default-tooltip": "默认为您的主文件夹"
}
},
"maps": {
"map-filter-panel": {
"duration": "时长",
"nps" : "每秒音符数",
"njs": "音符跳跃速度",
"tags": "标签",
"specificities": "general",
"requirements": "要求",
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,21 @@
If you encounter a keyboard shortcut conflict between AMD Software and Oculus Rift, you can either disable or modify the conflicting shortcut.
_Special thanks to_ `𝔽𝕪𝕟𝕟 (fynn07)` _for its contribution with information and screenshots_
### __Follow these steps to disable the shortcut in AMD Software:__
# Step 1 - Access Settings:
- Open AMD Software and click on the gear icon in the top right corner to go to settings.
![image](https://github.com/Zagrios/bs-manager/assets/40648115/2762f59c-f12a-46ba-80fa-9bf79beb64a9)
# Step 2 - Modify Keyboard Shortcuts:
- In the settings menu, select the "Hotkey" tab.
![image](https://github.com/Zagrios/bs-manager/assets/40648115/262d4fcc-529f-4c4c-9b87-e2da21df1b95)
# Step 3 -Disabling Shortcuts:
- Turn off the "Use Hotkeys" option to prevent any conflicts with Oculus Rift.
![image](https://github.com/Zagrios/bs-manager/assets/40648115/756018ad-7de3-469c-9f40-0082be007805)
# Step 3 alt - Modify the Shortcut:
- If you prefer to keep using keyboard shortcuts, consider changing the specific conflicting shortcut (e.g., "Ctrl + Shift + I") to another combination that is less likely to interfere.
️ _**Keep in mind you can't change the full keybind!**_
_**You can only Change the "i" to an different Button!**_
+14
View File
@@ -0,0 +1,14 @@
Welcome to the bs-manager wiki! (wip c:)
to get you oculus token check here : https://github.com/Zagrios/bs-manager/wiki/How-to-obtain-your-Oculus-Token
---
This Wiki accepts contributions!
To contribute to the wiki:
- Fork the project
- Add/Edit `.md` files in the `docs/wiki/` folder
- Create a Pull Request
- Once the PR is merged (after review), it will be present in the repository's wiki
@@ -0,0 +1,30 @@
### Important
Your token is a confidential piece of information. Possession of this token allows individuals to download applications, send messages, among other actions, under your identity.
However, you might wonder why it is necessary to provide this token to BSManager. The reason is that BSManager requires the token to continue the download with Oculus. Once you've input the token, it is used exclusively to communicate with Oculus servers to verify that you are the rightful owner of the game.
## Step 1 - Install and log into the Oculus Rift app
- Get the Oculus Rift app setup from the [Meta website](https://www.oculus.com/rift/setup/)
- Install the Oculus Rift app
**If you bought Beat Saber from the Quest store, it won't appear in your Rift library by default. To download it with BSManager, first claim it from its store page**
## Step 2 - Open developer tools
- Open Oculus app
- Open the developer tools by pressing <kbd>Ctrl</kbd> + <kbd>Shift</kbd> + <kbd>i</kbd>.
## Step 3 - Copy your Token
In the developer tools :
- Open the `Network` tab ***(1)***
- Filter for `graph` ***(2)***
- Click on the first request ***(3)***
- Open the `Headers` tab ***(4)***
- Scroll to the bottom to locate your token, it should start with `FRL` ***(5)***
- Select the token using your mouse and press <kbd>Ctrl</kbd> + <kbd>c</kbd> to copy it
![image](https://github.com/Zagrios/bs-manager/assets/40181755/b46be623-cefe-4349-bace-70a4109685a3)
# Known bugs
- [Nothing opens when I press `Ctrl`+`Shift`+`i`.](https://github.com/Zagrios/bs-manager/wiki/Nothing-opens-when-I-press-%60Ctrl%60%E2%80%90%60Shift%60%E2%80%90%60i%60.)
@@ -0,0 +1,4 @@
If nothing opens when you press `Ctrl` + `Shift` + `I`, it's possible that the keyboard shortcut to open the development tools is being used by another application.

Here's a non-exhaustive list of applications that have caused a problem and their suggested solutions:
- [AMD software](https://github.com/Zagrios/bs-manager/wiki/AMD-Sofware-%E2%80%90-Nothing-opens-when-I-press-%60Ctrl%60%E2%80%90%60Shift%60%E2%80%90%60I%60)
+53 -30
View File
@@ -1,12 +1,12 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.2",
"version": "1.5.0-alpha.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.5.0-alpha.2",
"version": "1.5.0-alpha.4",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
@@ -25,7 +25,7 @@
"electron-debug": "^3.2.0",
"electron-log": "^4.4.8",
"electron-store": "^8.1.0",
"electron-updater": "^6.2.1",
"electron-updater": "^6.3.4",
"fast-deep-equal": "^3.1.3",
"format-duration": "^3.0.2",
"framer-motion": "^11.2.6",
@@ -39,14 +39,14 @@
"node-fetch": "^3.3.2",
"node-stream-zip": "^1.15.0",
"pako": "^2.1.0",
"protobufjs": "^7.3.2",
"qrcode.react": "^3.1.0",
"protobufjs": "^7.4.0",
"qrcode.react": "^4.0.1",
"query-process": "^0.0.3",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-range": "^1.8.14",
"react-range": "^1.10.0",
"react-router-dom": "^6.24.1",
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
@@ -57,7 +57,7 @@
"semver": "^7.6.3",
"serialize-error": "^11.0.3",
"striptags": "^4.0.0-alpha.4",
"tailwind-merge": "^2.4.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-scoped-groups": "^2.0.0",
"tippy.js": "^6.3.7",
"to-ico": "^1.1.5",
@@ -106,7 +106,7 @@
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
"detect-port": "^1.5.1",
"electron": "^31.3.0",
"electron": "^32.1.2",
"electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0",
"electronmon": "^2.0.2",
@@ -138,7 +138,7 @@
"sass": "^1.70.0",
"sass-loader": "^14.1.0",
"style-loader": "^3.3.4",
"tailwindcss": "^3.4.7",
"tailwindcss": "^3.4.12",
"terser-webpack-plugin": "^5.3.10",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
@@ -8622,6 +8622,7 @@
"version": "9.2.4",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.4.tgz",
"integrity": "sha512-upp+biKpN/XZMLim7aguUyW8s0FUpDvOtK6sbanMFDAMBzpHDqdhgVYm6zc9HJ6nWo7u2Lxk60i2M6Jd3aiNrA==",
"dev": true,
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
@@ -10718,11 +10719,12 @@
}
},
"node_modules/electron": {
"version": "31.3.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-31.3.0.tgz",
"integrity": "sha512-3LMRMmK4UK0A+jYSLGLYdfhc20TgY2v5jD3iGmhRZlDYj0gn7xBj/waRjlNalysZ0D2rgPvoes0wHuf5e/Bguw==",
"version": "32.1.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-32.1.2.tgz",
"integrity": "sha512-CXe6doFzhmh1U7daOvUzmF6Cj8hssdYWMeEPRnRO6rB9/bbwMlWctcQ7P8NJXhLQ88/vYUJQrJvlJPh8qM0BRQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
"@electron/get": "^2.0.0",
"@types/node": "^20.9.0",
@@ -11048,20 +11050,34 @@
"license": "ISC"
},
"node_modules/electron-updater": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.2.1.tgz",
"integrity": "sha512-83eKIPW14qwZqUUM6wdsIRwVKZyjmHxQ4/8G+1C6iS5PdDt7b1umYQyj1/qPpH510GmHEQe4q0kCPe3qmb3a0Q==",
"version": "6.3.4",
"resolved": "https://registry.npmjs.org/electron-updater/-/electron-updater-6.3.4.tgz",
"integrity": "sha512-uZUo7p1Y53G4tl6Cgw07X1yF8Jlz6zhaL7CQJDZ1fVVkOaBfE2cWtx80avwDVi8jHp+I/FWawrMgTAeCCNIfAg==",
"license": "MIT",
"dependencies": {
"builder-util-runtime": "9.2.4",
"builder-util-runtime": "9.2.5",
"fs-extra": "^10.1.0",
"js-yaml": "^4.1.0",
"lazy-val": "^1.0.5",
"lodash.escaperegexp": "^4.1.2",
"lodash.isequal": "^4.5.0",
"semver": "^7.3.8",
"semver": "^7.6.3",
"tiny-typed-emitter": "^2.1.0"
}
},
"node_modules/electron-updater/node_modules/builder-util-runtime": {
"version": "9.2.5",
"resolved": "https://registry.npmjs.org/builder-util-runtime/-/builder-util-runtime-9.2.5.tgz",
"integrity": "sha512-HjIDfhvqx/8B3TDN4GbABQcgpewTU4LMRTQPkVpKYV3lsuxEJoIfvg09GyWTNmfVNSUAYf+fbTN//JX4TH20pg==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.4",
"sax": "^1.2.4"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/electron-updater/node_modules/fs-extra": {
"version": "10.1.0",
"license": "MIT",
@@ -18634,10 +18650,11 @@
}
},
"node_modules/protobufjs": {
"version": "7.3.2",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.3.2.tgz",
"integrity": "sha512-RXyHaACeqXeqAKGLDl68rQKbmObRsTIn4TYVUUug1KfS47YWCo5MacGITEryugIgZqORCvJWEk4l449POg5Txg==",
"version": "7.4.0",
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz",
"integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
"@protobufjs/aspromise": "^1.1.2",
"@protobufjs/base64": "^1.1.2",
@@ -18712,7 +18729,9 @@
"license": "MIT"
},
"node_modules/qrcode.react": {
"version": "3.1.0",
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.0.1.tgz",
"integrity": "sha512-Lpj0tPBn561WiQ3QQWXbkx8xTtB8BZkJeMZWLJIL8iaPBCoWzW1IpCeU3gY5MDqsb0+efCvEGkl9O0naP64crA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
@@ -18914,11 +18933,13 @@
"license": "MIT"
},
"node_modules/react-range": {
"version": "1.8.14",
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/react-range/-/react-range-1.10.0.tgz",
"integrity": "sha512-kDo0LiBUHIQIP8menx0UoxTnHr7UXBYpIYl/DR9jCaO1o29VwvCLpkP/qOTNQz5hkJadPg1uEM07XJcJ1XGoKw==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0",
"react-dom": "^16.8.0-0 || ^17.0.0-0 || ^18.0.0-0"
"react": "*",
"react-dom": "*"
}
},
"node_modules/react-redux": {
@@ -20798,9 +20819,10 @@
}
},
"node_modules/tailwind-merge": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.4.0.tgz",
"integrity": "sha512-49AwoOQNKdqKPd9CViyH5wJoSKsCDjUlzL8DxuGp3P1FsGY36NJDAa18jLZcaHAUUuTj+JB8IAo8zWgBNvBF7A==",
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.5.2.tgz",
"integrity": "sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/dcastil"
@@ -20823,9 +20845,10 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz",
"integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==",
"version": "3.4.12",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.12.tgz",
"integrity": "sha512-Htf/gHj2+soPb9UayUNci/Ja3d8pTmu9ONTfh4QY8r3MATTZOzmv6UYWF7ZwikEIC8okpfqmGqrmDehua8mF8w==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
+11 -10
View File
@@ -2,7 +2,7 @@
"name": "bs-manager",
"description": "Manage maps, mods and more for Beat Saber",
"main": "./.erb/dll/main.bundle.dev.js",
"version": "1.5.0-alpha.3",
"version": "1.5.0-alpha.4",
"scripts": {
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
@@ -19,7 +19,7 @@
"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=2164d6a7d641ecf6ad57852f665a518ca2bf960f --publish always --win --x64",
"publish:linux": "npm run build && electron-builder --publish always --linux --x64"
},
"lint-staged": {
@@ -53,6 +53,7 @@
"afterSign": ".erb/scripts/notarize.js",
"afterPack": ".erb/scripts/after-pack.js",
"win": {
"signingHashAlgorithms": ["sha256"],
"target": [
"nsis",
"nsis-web"
@@ -183,7 +184,7 @@
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
"detect-port": "^1.5.1",
"electron": "^31.3.0",
"electron": "^32.1.2",
"electron-builder": "^24.13.3",
"electron-devtools-installer": "^3.2.0",
"electronmon": "^2.0.2",
@@ -215,7 +216,7 @@
"sass": "^1.70.0",
"sass-loader": "^14.1.0",
"style-loader": "^3.3.4",
"tailwindcss": "^3.4.7",
"tailwindcss": "^3.4.12",
"terser-webpack-plugin": "^5.3.10",
"ts-jest": "^29.1.2",
"ts-loader": "^9.5.1",
@@ -243,7 +244,7 @@
"electron-debug": "^3.2.0",
"electron-log": "^4.4.8",
"electron-store": "^8.1.0",
"electron-updater": "^6.2.1",
"electron-updater": "^6.3.4",
"fast-deep-equal": "^3.1.3",
"format-duration": "^3.0.2",
"framer-motion": "^11.2.6",
@@ -257,14 +258,14 @@
"node-fetch": "^3.3.2",
"node-stream-zip": "^1.15.0",
"pako": "^2.1.0",
"protobufjs": "^7.3.2",
"qrcode.react": "^3.1.0",
"protobufjs": "^7.4.0",
"qrcode.react": "^4.0.1",
"query-process": "^0.0.3",
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-range": "^1.8.14",
"react-range": "^1.10.0",
"react-router-dom": "^6.24.1",
"react-virtualized-auto-sizer": "^1.0.24",
"react-window": "^1.8.10",
@@ -275,7 +276,7 @@
"semver": "^7.6.3",
"serialize-error": "^11.0.3",
"striptags": "^4.0.0-alpha.4",
"tailwind-merge": "^2.4.0",
"tailwind-merge": "^2.5.2",
"tailwindcss-scoped-groups": "^2.0.0",
"tippy.js": "^6.3.7",
"to-ico": "^1.1.5",
@@ -320,6 +321,6 @@
"logLevel": "quiet"
},
"volta": {
"node": "20.11.0"
"node": "20.17.0"
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.2",
"version": "1.5.0-alpha.4",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.5.0-alpha.2",
"version": "1.5.0-alpha.4",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.3",
"version": "1.5.0-alpha.4",
"description": "BSManager",
"main": "./dist/main/main.js",
"author": {
+19 -17
View File
@@ -1,4 +1,4 @@
import { CopyOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, stat, symlink } from "fs-extra";
import { CopyOptions, MoveOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, stat, symlink } from "fs-extra";
import { access, mkdir, rm, readdir, unlink, lstat, readlink } from "fs/promises";
import path from "path";
import { Observable, concatMap, from } from "rxjs";
@@ -7,6 +7,7 @@ import { BsmException } from "shared/models/bsm-exception.model";
import crypto from "crypto";
import { execSync } from "child_process";
import { tryit } from "../../shared/helpers/error.helpers";
import { CustomError } from "shared/models/exceptions/custom-error.class";
export async function pathExist(path: string): Promise<boolean> {
try {
@@ -78,36 +79,37 @@ export async function getFilesInFolder(folderPath: string): Promise<string[]> {
return dirEntries.filter(entry => entry.isFile()).map(file => path.join(folderPath, file.name));
}
export function moveFolderContent(src: string, dest: string): Observable<Progression> {
export function moveFolderContent(src: string, dest: string, option?: MoveOptions): Observable<Progression> {
const progress: Progression = { current: 0, total: 0 };
return new Observable<Progression>(subscriber => {
subscriber.next(progress);
(async () => {
const srcExist = await pathExist(src);
const srcExist = await pathExists(src);
if (!srcExist) {
return subscriber.complete();
}
ensureFolderExist(dest);
await ensureFolderExist(dest);
const files = await readdir(src, { encoding: "utf-8" });
const files = await readdir(src, { encoding: "utf-8", withFileTypes: true });
progress.total = files.length;
const promises = files.map(async file => {
const srcFullPath = path.join(src, file);
const destFullPath = path.join(dest, file);
if (await pathExist(destFullPath)) {
progress.current++;
return subscriber.next(progress);
for(const file of files){
const srcFullPath = path.join(src, file.name);
const destFullPath = path.join(dest, file.name);
const srcChilds = file.isDirectory() ? await readdir(srcFullPath, { encoding: "utf-8", recursive: true }) : [];
const allChildsAlreadyExist = srcChilds.every(child => pathExistsSync(path.join(destFullPath, child)));
if(file.isFile() || !allChildsAlreadyExist){
await move(srcFullPath, destFullPath, option);
}
await move(srcFullPath, destFullPath);
progress.current++;
subscriber.next(progress);
});
Promise.allSettled(promises).then(() => subscriber.complete());
})();
}
})().catch(err => subscriber.error(CustomError.fromError(err, err?.code))).finally(() => subscriber.complete());
});
}
@@ -151,7 +153,7 @@ export async function copyDirectoryWithJunctions(src: string, dest: string, opti
const symlinkTarget = await readlink(sourcePath);
const relativePath = path.relative(src, symlinkTarget);
const newTarget = path.join(dest, relativePath);
await symlink(newTarget, destinationPath, "junction");
await symlink(newTarget, destinationPath, "junction"); // Only junction to avoid right issues while copying content of BSManager folder
}
}
}
+27
View File
@@ -0,0 +1,27 @@
import { pathExistsSync } from "fs-extra";
import { from, of } from "rxjs";
import { InstallationLocationService } from "main/services/installation-location.service";
import { IpcService } from "main/services/ipc.service";
const ipc = IpcService.getInstance();
ipc.on("bs-installer.folder-exists", (_, reply) => {
const service = InstallationLocationService.getInstance();
reply(of(pathExistsSync(service.installationDirectory())));
});
ipc.on("bs-installer.default-install-path", (_, reply) => {
const service = InstallationLocationService.getInstance();
reply(of(service.defaultInstallationDirectory()));
});
ipc.on("bs-installer.install-path", (_, reply) => {
const service = InstallationLocationService.getInstance();
reply(of(service.installationDirectory()));
});
ipc.on("bs-installer.set-install-path", (args, reply) => {
const service = InstallationLocationService.getInstance();
reply(from(service.setInstallationDirectory(args.path, args.move)));
});
+5
View File
@@ -27,6 +27,11 @@ ipc.on("download-map", async (args, reply) => {
reply(from(maps.downloadMap(args.map, args.version)));
});
ipc.on("last-downloaded-map", (_, reply) => {
const maps = LocalMapsManagerService.getInstance();
reply(maps.lastDownloadedMap$);
})
ipc.on("one-click-install-map", (args, reply) => {
const maps = LocalMapsManagerService.getInstance();
reply(from(maps.oneClickDownloadMap(args)))
+3 -3
View File
@@ -16,15 +16,15 @@ ipc.on("get-installed-mods", (args, reply) => {
ipc.on("install-mods", (args, reply) => {
const modsManager = BsModsManagerService.getInstance();
reply(from(modsManager.installMods(args.mods, args.version)));
reply(modsManager.installMods(args.mods, args.version));
});
ipc.on("uninstall-mods", (args, reply) => {
const modsManager = BsModsManagerService.getInstance();
reply(from(modsManager.uninstallMods(args.mods, args.version)));
reply(modsManager.uninstallMods(args.mods, args.version));
});
ipc.on("uninstall-all-mods", (args, reply) => {
const modsManager = BsModsManagerService.getInstance();
reply(from(modsManager.uninstallAllMods(args)));
reply(modsManager.uninstallAllMods(args));
});
@@ -1,8 +1,7 @@
import { BsOculusDownloaderService } from "../../services/bs-version-download/bs-oculus-downloader.service";
import { BsSteamDownloaderService } from "../../services/bs-version-download/bs-steam-downloader.service";
import { InstallationLocationService } from "../../services/installation-location.service";
import { IpcService } from "../../services/ipc.service";
import { from, of } from "rxjs";
import { of } from "rxjs";
import { BSLocalVersionService } from "../../services/bs-local-version.service";
const ipc = IpcService.getInstance();
@@ -14,16 +13,6 @@ ipc.on("import-version", (args, reply) => {
// #region Steam
ipc.on("bs-download.installation-folder", (_, reply) => {
const installLocation = InstallationLocationService.getInstance();
reply(of(installLocation.installationDirectory()));
});
ipc.on("bs-download.set-installation-folder", (args, reply) => {
const installerService = InstallationLocationService.getInstance();
reply(from(installerService.setInstallationDirectory(args)));
});
ipc.on("auto-download-bs-version", (args, reply) => {
const bsInstaller = BsSteamDownloaderService.getInstance();
reply(bsInstaller.autoDownloadBsVersion(args));
+2
View File
@@ -1,4 +1,5 @@
import "./os-controls-ipcs";
import "./bs-installer-ipcs.ts";
import "./bs-launcher-ipcs";
import "./bs-version-ipcs";
import "./bs-uninstall-ipcs";
@@ -12,3 +13,4 @@ import "./bs-playlist-ipcs";
import "./model-saber.ipcs";
import "./bs-model-ipcs";
import "./bs-version-download/bs-download-ipcs";
import "./static-configuration.ipcs";
+1 -1
View File
@@ -6,7 +6,7 @@ const ipc = IpcService.getInstance();
ipc.on("download-update", (_, reply) => {
const updaterService = AutoUpdaterService.getInstance();
reply(from(updaterService.downloadUpdate()));
reply(updaterService.downloadUpdate());
});
ipc.on("check-update", (_, reply) => {
+11 -1
View File
@@ -3,6 +3,7 @@ import { NotificationService } from "../services/notification.service";
import { IpcService } from "../services/ipc.service";
import { from, of } from "rxjs";
import { readFileSync } from "fs-extra";
import log from "electron-log";
// TODO IMPROVE WINDOW CONTROL BY USING WINDOW SERVICE
@@ -16,7 +17,7 @@ ipc.on("choose-folder", (args, reply) => {
reply(from(dialog.showOpenDialog({ properties: ["openDirectory"], defaultPath: args ?? "" })));
});
ipc.on<string>("choose-file", async (args, reply) => {
ipc.on("choose-file", async (args, reply) => {
reply(from(dialog.showOpenDialog({ properties: ["openFile"], defaultPath: args ?? "" })));
});
@@ -62,3 +63,12 @@ ipc.on("choose-image", (args, reply) => {
return res.filePaths;
})));
});
ipc.on("restart-app", (_, reply) => {
log.info("App was requested to restart");
reply(of()); // Reply before restarting to avoid any issue
app.relaunch();
app.quit();
});
@@ -0,0 +1,14 @@
import { of } from "rxjs";
import { IpcService } from "../services/ipc.service";
import { StaticConfigurationService } from "../services/static-configuration.service";
const ipc = IpcService.getInstance();
const staticConfig = StaticConfigurationService.getInstance();
ipc.on("static-configuration.get", (args, reply) => {
reply(of(staticConfig.get(args)));
});
ipc.on("static-configuration.set", (args, reply) => {
reply(of(staticConfig.set(args.key, args.value)));
});
+10 -1
View File
@@ -24,8 +24,10 @@ import { SteamLauncherService } from "./services/bs-launcher/steam-launcher.serv
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";
import { StaticConfigurationService } from "./services/static-configuration.service";
const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true";
const staticConfig = StaticConfigurationService.getInstance();
export const filterStrings = new Set<string>();
export const filterPatterns = new Set<RegExp>();
@@ -37,6 +39,13 @@ initLogger();
deleteOlestLogs();
deleteOldLogs();
staticConfig.take("disable-hadware-acceleration", disabled => {
if(disabled === true){ // strictly check for true
log.info("Disabling hardware acceleration");
app.disableHardwareAcceleration();
}
});
if (process.env.NODE_ENV === "production") {
const sourceMapSupport = require("source-map-support");
@@ -108,7 +117,7 @@ if (!gotTheLock) {
app.whenReady().then(() => {
// C:\\Users\\Mathieu\\Desktop\\BSManager\\BSInstances\\My Version\\UserData\\SongDetailsCache.proto
app.setAppUserModelId(APP_NAME);
+3
View File
@@ -24,6 +24,9 @@ contextBridge.exposeInMainWorld("electron", {
},
path: {
sep,
basename: (path: string): string => {
return !path ? "" : path.split(sep).at(-1);
},
join: (...args: string[]): string => {
return args.join(sep);
}
@@ -14,7 +14,7 @@ import sanitize from "sanitize-filename";
import { DeepLinkService } from "../../deep-link.service";
import log from "electron-log";
import { WindowManagerService } from "../../window-manager.service";
import { Observable, lastValueFrom } from "rxjs";
import { Observable, Subject, lastValueFrom } from "rxjs";
import { Archive } from "../../../models/archive.class";
import { Progression, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers";
import { readFile } from "fs/promises";
@@ -23,7 +23,6 @@ import { allSettled } from "../../../../shared/helpers/promise.helpers";
import { splitIntoChunk } from "../../../../shared/helpers/array.helpers";
import { SongDetailsCacheService } from "./song-details-cache.service";
import { SongCacheService } from "./song-cache.service";
import { IpcService } from "../../ipc.service";
import { pathToFileURL } from "url";
import { sToMs } from "../../../../shared/helpers/time.helpers";
import { FieldRequired } from "shared/helpers/type.helpers";
@@ -57,7 +56,8 @@ export class LocalMapsManagerService {
private readonly linker: FolderLinkerService;
private readonly songDetailsCache: SongDetailsCacheService;
private readonly songCache: SongCacheService;
private readonly ipc: IpcService;
private readonly _lastDownloadedMap = new Subject<{ map: BsmLocalMap, version?: BSVersion }>();
private constructor() {
this.localVersion = BSLocalVersionService.getInstance();
@@ -69,7 +69,6 @@ export class LocalMapsManagerService {
this.linker = FolderLinkerService.getInstance();
this.songDetailsCache = SongDetailsCacheService.getInstance();
this.songCache = SongCacheService.getInstance();
this.ipc = IpcService.getInstance();
const handleOneClick = (mapId: string, isHash = false) => {
this.windows.openWindow(`oneclick-download-map.html?mapId=${mapId}&isHash=${isHash}`);
@@ -344,7 +343,7 @@ export class LocalMapsManagerService {
const localMap = await this.loadMapInfoFromPath(mapPath);
localMap.songDetails = this.songDetailsCache.getSongDetails(localMap.hash);
this.ipc.send<{map: BsmLocalMap, version?: BSVersion}>("map-downloaded", this.windows.getWindows("index.html").at(0), { map: localMap, version });
this._lastDownloadedMap.next({ map: localMap, version });
return localMap;
}
@@ -392,4 +391,8 @@ export class LocalMapsManagerService {
public isDeepLinksEnabled(): boolean {
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistered(link));
}
public get lastDownloadedMap$(): Observable<{ map: BsmLocalMap, version?: BSVersion }> {
return this._lastDownloadedMap.asObservable();
}
}
@@ -1,7 +1,6 @@
import path from "path";
import { ensureDirSync, existsSync, readFile, writeFile } from "fs-extra";
import { BehaviorSubject, Observable, catchError, filter, lastValueFrom, of, take, timeout } from "rxjs";
import { ConfigurationService } from "../../configuration.service";
import { RequestService } from "../../request.service";
import { tryit } from "shared/helpers/error.helpers";
import { CACHE_PATH, HTTP_STATUS_CODES } from "main/constants";
@@ -12,6 +11,7 @@ import { SongDetails } from "shared/models/maps/song-details-cache/song-details-
import { inflate } from "pako";
import { RawSongDetailsCache } from "shared/models/maps/song-details-cache/raw-song-details-cache.model";
import { RawSongDetailsDeserializer } from "shared/models/maps/song-details-cache/raw-song-details-deserializer.class";
import { StaticConfigurationService } from "main/services/static-configuration.service";
export class SongDetailsCacheService {
@@ -32,7 +32,7 @@ export class SongDetailsCacheService {
private readonly PROTO_CACHE_PATH = path.join(CACHE_PATH, "song-details-cache");
private readonly etagKey = "song-details-cache-etag";
private readonly config: ConfigurationService;
private readonly staticConfig: StaticConfigurationService;
private readonly request: RequestService;
private readonly utils: UtilsService;
@@ -41,7 +41,7 @@ export class SongDetailsCacheService {
private readonly _loaded$ = new BehaviorSubject<boolean>(null);
private constructor(){
this.config = ConfigurationService.getInstance();
this.staticConfig = StaticConfigurationService.getInstance();
this.request = RequestService.getInstance();
this.utils = UtilsService.getInstance();
this.loadCache()
@@ -49,10 +49,10 @@ export class SongDetailsCacheService {
private async loadCache(): Promise<void> {
const protoCacheExists = existsSync(this.PROTO_CACHE_PATH);
const etag = protoCacheExists ? this.config.get<string>(this.etagKey) : null;
const etag = protoCacheExists ? this.staticConfig.get(this.etagKey) : null;
await this.downloadCacheFile(etag).then(etag => {
this.config.set(this.etagKey, etag);
this.staticConfig.set(this.etagKey, etag);
log.info("SongDetailsCache downloaded");
this.songDetailsIdIndex = this.createIdIndex(this.songDetailsCache);
log.info("SongDetailsIdIndex created");
+35 -27
View File
@@ -1,13 +1,12 @@
import { autoUpdater } from "electron-updater";
import { autoUpdater, CancellationToken, ProgressInfo } from "electron-updater";
import log from "electron-log";
import { UtilsService } from "./utils.service";
import { gt } from "semver";
import { Progression } from "main/helpers/fs.helpers";
import { Observable } from "rxjs";
export class AutoUpdaterService {
private static instance: AutoUpdaterService;
private readonly utilsService: UtilsService;
public static getInstance(): AutoUpdaterService {
if (!AutoUpdaterService.instance) {
AutoUpdaterService.instance = new AutoUpdaterService();
@@ -18,37 +17,46 @@ export class AutoUpdaterService {
constructor() {
autoUpdater.logger = log;
autoUpdater.autoDownload = false;
this.utilsService = UtilsService.getInstance();
}
public isUpdateAvailable(): Promise<boolean> {
return new Promise(resolve => {
autoUpdater
.checkForUpdates()
.then(info => {
const needUpdate = (() => {
if (!info?.updateInfo) {
return false;
}
return gt(info.updateInfo.version, autoUpdater.currentVersion.version);
})();
resolve(needUpdate);
})
.catch(() => resolve(false));
});
return autoUpdater.checkForUpdates().then(info => {
return !!info?.updateInfo && gt(info.updateInfo.version, autoUpdater.currentVersion.version);
}).catch(() => false);
}
public downloadUpdate(): Promise<boolean> {
autoUpdater.removeAllListeners("download-progress");
autoUpdater.addListener("download-progress", info => {
this.utilsService.ipcSend("update-download-progress", { success: true, data: info.percent });
});
public downloadUpdate(): Observable<Progression> {
return new Observable<Progression>(observer => {
return autoUpdater.downloadUpdate().then(res => !!res && !!res.length);
observer.next({ current: 0, total: 0 });
const progressListener = (progress: ProgressInfo) => {
observer.next({ current: progress.transferred, total: progress.total });
};
const downloadedListener = () => {
observer.next({ current: 100, total: 100 });
};
autoUpdater.addListener("download-progress", progressListener);
autoUpdater.addListener("update-downloaded", downloadedListener);
const cancelToken = new CancellationToken();
autoUpdater.downloadUpdate(cancelToken)
.catch(err => observer.error(err))
.finally(() => observer.complete());
return () => {
cancelToken.cancel();
autoUpdater.removeListener("download-progress", progressListener);
autoUpdater.removeListener("update-downloaded", downloadedListener);
}
});
}
public quitAndInstall() {
autoUpdater.quitAndInstall();
log.info("Quit and install");
return autoUpdater.quitAndInstall();
}
}
@@ -140,6 +140,8 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
"STEAM_COMPAT_INSTALL_PATH": bsFolderPath,
"STEAM_COMPAT_CLIENT_INSTALL_PATH": steamPath,
"STEAM_COMPAT_APP_ID": BS_APP_ID,
// Run game in steam environment; fixes #585 for unicode song titles
"SteamEnv": "1",
// Uncomment these to create a proton log file in the Beat Saber install directory.
// "PROTON_LOG": 1,
// "PROTON_LOG_DIR": bsFolderPath,
+46 -43
View File
@@ -6,14 +6,13 @@ import { BS_APP_ID, OCULUS_BS_BACKUP_DIR, OCULUS_BS_DIR } from "../constants";
import path from "path";
import { ConfigurationService } from "./configuration.service";
import { lstat, rename } from "fs/promises";
import { BsmException } from "shared/models/bsm-exception.model";
import log from "electron-log";
import { OculusService } from "./oculus.service";
import { DownloadLinkType } from "shared/models/mods";
import sanitize from "sanitize-filename";
import { Progression, copyDirectoryWithJunctions, deleteFolder, ensurePathNotAlreadyExist, getFoldersInFolder, rxCopy } from "../helpers/fs.helpers";
import { FolderLinkerService } from "./folder-linker.service";
import { ReadStream, createReadStream, pathExists, readFile, writeFile } from "fs-extra";
import { ReadStream, createReadStream, pathExists, pathExistsSync, readFile, writeFile } from "fs-extra";
import readline from "readline";
import { Observable, Subject, catchError, finalize, from, map, switchMap, throwError } from "rxjs";
import { BsStore } from "../../shared/models/bs-store.enum";
@@ -306,54 +305,58 @@ export class BSLocalVersionService {
.catch(() => { return false; })
}
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
if(version.steam || version.oculus){ throw {title: "CantEditSteam", message: "CantEditSteam"} as BsmException; }
const oldPath = await this.getVersionPath(version);
const editedVersion: BSVersion = version.BSVersion === name
? {...version, name: undefined, color}
: {...version, name: sanitize(name), color};
const newPath = await this.getVersionPath(editedVersion);
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
if(version.steam || version.oculus){ throw new CustomError("Do not edit official Beat Saber versions", "CantEditSteam") }
const oldPath = await this.getVersionPath(version);
const editedVersion: BSVersion = version.BSVersion === name
? {...version, name: undefined, color}
: {...version, name: sanitize(name), color};
const newPath = await this.getVersionPath(editedVersion);
if(oldPath === newPath){
this.deleteCustomVersion(version);
this.addCustomVersion(editedVersion);
return editedVersion;
}
if(oldPath === newPath){
this.deleteCustomVersion(version);
this.addCustomVersion(editedVersion);
return editedVersion;
}
if((await pathExists(newPath)) && newPath === oldPath){ throw {title: "VersionAlreadExist"} as BsmException; }
if(pathExistsSync(newPath)){
throw new CustomError("Unable to edit the version, path already exist", "VersionAlreadExist");
}
return rename(oldPath, newPath).then(() => {
this.deleteCustomVersion(version);
this.addCustomVersion(editedVersion);
return editedVersion;
}).catch((err: Error) => {
log.error("edit version error", err, version, name, color);
throw {title: "CantRename", ...err} as BsmException;
});
}
return rename(oldPath, newPath).then(() => {
this.deleteCustomVersion(version);
this.addCustomVersion(editedVersion);
return editedVersion;
}).catch((err: Error) => {
log.error("edit version error", err, version, name, color);
throw CustomError.fromError(err, "CantRename");
});
}
public async cloneVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
const originPath = await this.getVersionPath(version);
const cloneVersion: BSVersion = version.BSVersion === name
? {...version, name: undefined, color, steam: false, oculus: false}
: {...version, name: sanitize(name), color, steam: false, oculus: false};
const newPath = await this.getVersionPath(cloneVersion);
public async cloneVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
const originPath = await this.getVersionPath(version);
const cloneVersion: BSVersion = version.BSVersion === name
? {...version, name: undefined, color, steam: false, oculus: false}
: {...version, name: sanitize(name), color, steam: false, oculus: false};
const newPath = await this.getVersionPath(cloneVersion);
if(originPath === newPath){
this.deleteCustomVersion(version);
this.addCustomVersion(cloneVersion);
}
if(pathExistsSync(newPath)){
throw new CustomError("Unable to clone the version, path already exist", "VersionAlreadExist");
}
if(await pathExists(newPath)){ throw {title: "VersionAlreadExist"} as BsmException; }
if(originPath === newPath){
this.deleteCustomVersion(version);
this.addCustomVersion(cloneVersion);
}
return copyDirectoryWithJunctions(originPath, newPath).then(() => {
this.addCustomVersion(cloneVersion);
return cloneVersion;
}).catch((err: Error) => {
log.error("clone version error", err, version, name, color);
throw {title: "CantClone", ...err} as BsmException
})
}
return copyDirectoryWithJunctions(originPath, newPath).then(() => {
this.addCustomVersion(cloneVersion);
return cloneVersion;
}).catch((err: Error) => {
log.error("Error occured while cloning the version", err, version, name, color);
throw CustomError.fromError(err, "CantClone");
})
}
public importVersion(opt: ImportVersionOptions): Observable<Progression<BSVersion>>{
const { fromPath, store } = opt;
+20 -1
View File
@@ -5,6 +5,7 @@ import { BSVersion } from "shared/bs-version.interface";
import { RequestService } from "./request.service";
import { readJSON } from "fs-extra";
import { allSettled } from "../../shared/helpers/promise.helpers";
import { StaticConfigurationService } from "./static-configuration.service";
export class BSVersionLibService {
private readonly REMOTE_BS_VERSIONS_URL: string = "https://raw.githubusercontent.com/Zagrios/bs-manager/master/assets/jsons/bs-versions.json";
@@ -14,12 +15,14 @@ export class BSVersionLibService {
private utilsService: UtilsService;
private requestService: RequestService;
private staticConfigurationService: StaticConfigurationService;
private bsVersions: BSVersion[];
private constructor() {
this.utilsService = UtilsService.getInstance();
this.requestService = RequestService.getInstance();
this.staticConfigurationService = StaticConfigurationService.getInstance();
}
public static getInstance(): BSVersionLibService {
@@ -35,12 +38,28 @@ export class BSVersionLibService {
private async getLocalVersions(): Promise<BSVersion[]> {
const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE);
if (process.platform === "linux") {
let versions = this.staticConfigurationService.get("versions");
if (!versions) {
versions = (await readJSON(localVersionsPath)) as BSVersion[];
this.staticConfigurationService.set("versions", versions);
}
return versions;
}
return readJSON(localVersionsPath);
}
private async updateLocalVersions(versions: BSVersion[]): Promise<void> {
const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE);
writeFileSync(localVersionsPath, JSON.stringify(versions, null, "\t"), { encoding: "utf-8", flag: "w" });
// Do not write on readonly memory in linux when running on AppImage
if (process.platform === "linux") {
this.staticConfigurationService.set("versions", versions);
} else {
writeFileSync(localVersionsPath, JSON.stringify(versions, null, "\t"), { encoding: "utf-8", flag: "w" });
}
}
private async loadBsVersions(): Promise<BSVersion[]> {
+21 -3
View File
@@ -1,5 +1,7 @@
import ElectronStore from "electron-store";
import fs from "fs-extra";
import { InstallationLocationService } from "./installation-location.service";
import { CustomError } from "shared/models/exceptions/custom-error.class";
export class ConfigurationService {
private static instance: ConfigurationService;
@@ -13,17 +15,23 @@ export class ConfigurationService {
private readonly locations: InstallationLocationService;
private contentPath: string;
private store: ElectronStore;
private constructor() {
this.locations = InstallationLocationService.getInstance();
this.initStore();
this.initStore(false);
this.locations.onInstallLocationUpdate(() => { this.initStore() });
this.locations.onInstallLocationUpdate(() => this.initStore(true));
}
private async initStore() {
private async initStore(createFolder: boolean) {
const contentPath = this.locations.installationDirectory();
if (!createFolder && !fs.pathExistsSync(contentPath)) {
return;
}
this.contentPath = contentPath;
this.store = new ElectronStore({
cwd: contentPath,
name: "config",
@@ -32,15 +40,25 @@ export class ConfigurationService {
});
}
private checkStore(): void {
// Can be null if config.cfg does not exist or corrupted
if (!this.store) {
throw CustomError.fromError(new Error(`Can't read config.cfg on ${this.contentPath}`));
}
}
public set(key: string, value: unknown): void {
this.checkStore();
this.store.set(key, value);
}
public get<T>(key: string): T {
this.checkStore();
return this.store.get(key) as T;
}
public delete(key: string): void {
this.checkStore();
this.store.delete(key);
}
}
+26 -6
View File
@@ -4,6 +4,9 @@ import { deleteFolder, ensureFolderExist, moveFolderContent, pathExist, unlinkPa
import { lstat, symlink } from "fs/promises";
import path from "path";
import { copy, readlink } from "fs-extra";
import { lastValueFrom } from "rxjs";
import { noop } from "shared/helpers/function.helpers";
import { StaticConfigurationService } from "./static-configuration.service";
export class FolderLinkerService {
private static instance: FolderLinkerService;
@@ -16,9 +19,21 @@ export class FolderLinkerService {
}
private readonly installLocationService = InstallationLocationService.getInstance();
private readonly staticConfig: StaticConfigurationService;
private linkingType: "junction" | "symlink" = "junction";
private constructor() {
this.installLocationService = InstallationLocationService.getInstance();
this.staticConfig = StaticConfigurationService.getInstance();
this.linkingType = this.staticConfig.get("use-symlinks") === true ? "symlink" : "junction";
log.info(`Linking type is set to ${this.linkingType}`);
this.staticConfig.$watch("use-symlinks").subscribe((useSymlink) => {
this.linkingType = useSymlink === true ? "symlink" : "junction";
log.info(`Linking type set to ${this.linkingType}`);
});
}
private async sharedFolder(): Promise<string> {
@@ -49,6 +64,10 @@ export class FolderLinkerService {
});
}
private getLinkingType(): "junction" | undefined {
return this.linkingType === "junction" ? "junction" : undefined;
}
public async linkFolder(folderPath: string, options?: LinkOptions): Promise<void> {
const sharedPath = await this.getSharedFolder(folderPath, options?.intermediateFolder);
@@ -60,7 +79,9 @@ export class FolderLinkerService {
return;
}
await unlinkPath(folderPath);
return symlink(sharedPath, folderPath, "junction");
log.info(`Linking ${folderPath} to ${sharedPath}; type: ${this.linkingType}`);
return symlink(sharedPath, folderPath, this.getLinkingType());
}
await ensureFolderExist(sharedPath);
@@ -72,12 +93,13 @@ export class FolderLinkerService {
await ensureFolderExist(folderPath);
if (options?.keepContents !== false) {
await moveFolderContent(folderPath, sharedPath).toPromise();
await lastValueFrom(moveFolderContent(folderPath, sharedPath, { overwrite: true }));
}
await deleteFolder(folderPath);
return symlink(sharedPath, folderPath, "junction");
log.info(`Linking ${folderPath} to ${sharedPath}; type: ${this.linkingType}`);
return symlink(sharedPath, folderPath, this.getLinkingType());
}
public async unlinkFolder(folderPath: string, options?: UnlinkOptions): Promise<void> {
@@ -95,9 +117,7 @@ export class FolderLinkerService {
}
if (options.moveContents === true) {
return moveFolderContent(sharedPath, folderPath)
.toPromise()
.then(() => {});
return lastValueFrom(moveFolderContent(sharedPath, folderPath, { overwrite: true })).then(noop);
}
if (options?.keepContents === false) {
@@ -1,9 +1,9 @@
import path from "path";
import { app } from "electron";
import ElectronStore from "electron-store";
import { copyDirectoryWithJunctions, deleteFolder, ensureFolderExist } from "../helpers/fs.helpers";
import { tryit } from "../../shared/helpers/error.helpers";
import { pathExistsSync } from "fs-extra";
import { StaticConfigurationService } from "./static-configuration.service";
export class InstallationLocationService {
private static instance: InstallationLocationService;
@@ -23,15 +23,15 @@ export class InstallationLocationService {
private readonly STORE_INSTALLATION_PATH_KEY = "installation-folder";
private readonly installPathConfig: ElectronStore;
private readonly staticConfig: StaticConfigurationService;
private readonly updateListeners: Set<Listener> = new Set();
private _installationDirectory: string;
private constructor() {
this.installPathConfig = new ElectronStore({ watch: true });
this.staticConfig = StaticConfigurationService.getInstance();
this.installPathConfig.onDidChange(this.STORE_INSTALLATION_PATH_KEY, () => {
this.staticConfig.$watch(this.STORE_INSTALLATION_PATH_KEY).subscribe(() => {
this.triggerListeners();
});
}
@@ -40,17 +40,21 @@ export class InstallationLocationService {
this.updateListeners.forEach(listener => listener());
}
public async setInstallationDirectory(newDir: string): Promise<string> {
/**
* @param move - if true, move the old installation path to the path param
*/
public async setInstallationDirectory(newDir: string, move: boolean): Promise<string> {
newDir = path.basename(newDir) === this.INSTALLATION_FOLDER ? path.join(newDir, "..") : newDir;
const oldDir = this.installationDirectory();
await ensureFolderExist(oldDir);
await copyDirectoryWithJunctions(oldDir, path.join(newDir, this.INSTALLATION_FOLDER), { overwrite: true });
if (move) {
const oldDir = this.installationDirectory();
await ensureFolderExist(oldDir);
await copyDirectoryWithJunctions(oldDir, path.join(newDir, this.INSTALLATION_FOLDER), { overwrite: true });
deleteFolder(oldDir);
}
this._installationDirectory = newDir;
this.installPathConfig.set(this.STORE_INSTALLATION_PATH_KEY, newDir);
deleteFolder(oldDir);
this.staticConfig.set(this.STORE_INSTALLATION_PATH_KEY, newDir);
return this.installationDirectory();
}
@@ -59,6 +63,12 @@ export class InstallationLocationService {
this.updateListeners.add(fn);
}
public defaultInstallationDirectory(): string {
const { result: oldPath } = tryit(() => path.join(app.getPath("documents"), this.INSTALLATION_FOLDER));
const installationDirectory = (oldPath && pathExistsSync(oldPath)) ? app.getPath("documents") : app.getPath("home");
return path.join(installationDirectory, this.INSTALLATION_FOLDER);
}
public installationDirectory(): string {
const installParentPath = () => {
@@ -66,8 +76,8 @@ export class InstallationLocationService {
return this._installationDirectory;
}
if(this.installPathConfig.has(this.STORE_INSTALLATION_PATH_KEY)) {
return this.installPathConfig.get(this.STORE_INSTALLATION_PATH_KEY) as string;
if(this.staticConfig.has(this.STORE_INSTALLATION_PATH_KEY)) {
return this.staticConfig.get(this.STORE_INSTALLATION_PATH_KEY);
}
const { result: oldPath } = tryit(() => path.join(app.getPath("documents"), this.INSTALLATION_FOLDER));
+1 -1
View File
@@ -49,7 +49,7 @@ export class IpcService {
const sub = observable.subscribe({
next: data => this.send(channel, window, data),
error: error => {
log.error(error, error?.code);
log.error(error, error?.code, error?.data);
this.send(this.getErrorChannel(channel), window, serializeError(error));
},
complete: () => this.send(this.getCompleteChannel(channel), window)
+104 -128
View File
@@ -1,39 +1,32 @@
import { BSVersion } from "shared/bs-version.interface";
import { DownloadLink, InstallModsResult, Mod, ModInstallProgression, UninstallModsResult } from "shared/models/mods";
import { DownloadLink, Mod } from "shared/models/mods";
import { BeatModsApiService } from "./beat-mods-api.service";
import { BSLocalVersionService } from "../bs-local-version.service";
import path from "path";
import { UtilsService } from "../utils.service";
import md5File from "md5-file";
import { RequestService } from "../request.service";
import { spawn } from "child_process";
import { BS_EXECUTABLE } from "../../constants";
import log from "electron-log";
import { deleteFolder, pathExist, unlinkPath } from "../../helpers/fs.helpers";
import { lastValueFrom } from "rxjs";
import { deleteFolder, pathExist, Progression, unlinkPath } from "../../helpers/fs.helpers";
import { lastValueFrom, Observable } from "rxjs";
import JSZip from "jszip";
import { extractZip } from "../../helpers/zip.helpers";
import recursiveReadDir from "recursive-readdir";
import { sToMs } from "../../../shared/helpers/time.helpers";
import { ensureDir } from "fs-extra";
import { ensureDir, pathExistsSync } from "fs-extra";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { popElement } from "shared/helpers/array.helpers";
export class BsModsManagerService {
private static instance: BsModsManagerService;
private readonly beatModsApi: BeatModsApiService;
private readonly bsLocalService: BSLocalVersionService;
private readonly utilsService: UtilsService;
private readonly requestService: RequestService;
private manifestMatches: Mod[];
private nbModsToInstall = 0;
private nbInstalledMods = 0;
private nbModsToUninstall = 0;
private nbUninstalledMods = 0;
public static getInstance(): BsModsManagerService {
if (!BsModsManagerService.instance) {
BsModsManagerService.instance = new BsModsManagerService();
@@ -44,7 +37,6 @@ export class BsModsManagerService {
private constructor() {
this.beatModsApi = BeatModsApiService.getInstance();
this.bsLocalService = BSLocalVersionService.getInstance();
this.utilsService = UtilsService.getInstance();
this.requestService = RequestService.getInstance();
}
@@ -63,7 +55,7 @@ export class BsModsManagerService {
const bsPath = await this.bsLocalService.getVersionPath(version);
const modsPath = path.join(bsPath, modsDir);
if (!(await pathExist(modsPath))) {
if (!pathExistsSync(modsPath)) {
return [];
}
@@ -81,19 +73,22 @@ export class BsModsManagerService {
if (!mod) {
return undefined;
}
if (ext === ".manifest") {
this.manifestMatches.push(mod);
return undefined;
}
if (filePath.includes("Libs")) {
if (!this.manifestMatches.some(m => m.name === mod.name)) {
if (filePath.toLowerCase().includes("libs")) {
const manifestIndex = this.manifestMatches.findIndex(m => m.name === mod.name);
if (manifestIndex < 0) {
return undefined;
}
const modIndex = this.manifestMatches.indexOf(mod);
if (modIndex > -1) {
this.manifestMatches.splice(modIndex, 1);
}
this.manifestMatches.splice(manifestIndex, 1);
}
return mod;
});
@@ -147,7 +142,7 @@ export class BsModsManagerService {
return new Promise<boolean>(resolve => {
const cmd = process.platform === 'linux'
? `screen -dmS "BSIPA" dotnet ${ipaPath} ${args.join(" ")}` // Must run through screen, otherwise BSIPA tries to move console cursor and crashes.
? `screen -dmS "BSIPA" dotnet "${ipaPath}" ${args.join(" ")}` // Must run through screen, otherwise BSIPA tries to move console cursor and crashes.
: `"${ipaPath}" ${args.join(" ")}`;
log.info("START IPA PROCESS", cmd);
@@ -184,7 +179,6 @@ export class BsModsManagerService {
private async installMod(mod: Mod, version: BSVersion): Promise<boolean> {
log.info("INSTALL MOD", mod.name, "for version", `${version.BSVersion} - ${version.name}`);
this.utilsService.ipcSend<ModInstallProgression>("mod-installed", { success: true, data: { name: mod.name, progression: ((this.nbInstalledMods + 1) / this.nbModsToInstall) * 100 } });
const download = this.getModDownload(mod, version);
@@ -243,37 +237,9 @@ export class BsModsManagerService {
}))
: extracted;
if(res){
this.nbInstalledMods++;
}
return res;
}
private isDependency(mod: Mod, selectedMods: Mod[], availableMods: Mod[]) {
return selectedMods.some(m => {
const deps = m.dependencies.map(dep => Array.from(availableMods.values()).find(m => dep.name === m.name));
if (deps.some(depMod => depMod.name === mod.name)) {
return true;
}
return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep.name === mod.name));
});
}
private async resolveDependencies(mods: Mod[], version: BSVersion): Promise<Mod[]> {
const availableMods = await this.beatModsApi.getVersionMods(version);
return Array.from(
new Map<string, Mod>(
availableMods.reduce((res, mod) => {
if (this.isDependency(mod, mods, availableMods)) {
res.push([mod.name, mod]);
}
return res;
}, [])
).values()
);
}
private async uninstallBSIPA(mod: Mod, version: BSVersion): Promise<void> {
const download = this.getModDownload(mod, version);
@@ -296,9 +262,6 @@ export class BsModsManagerService {
}
private async uninstallMod(mod: Mod, version: BSVersion): Promise<void> {
this.nbUninstalledMods++;
this.utilsService.ipcSend<ModInstallProgression>("mod-uninstalled", { success: true, data: { name: mod.name, progression: (this.nbUninstalledMods / this.nbModsToUninstall) * 100 } });
if (mod.name.toLowerCase() === "bsipa") {
return this.uninstallBSIPA(mod, version);
}
@@ -322,100 +285,113 @@ export class BsModsManagerService {
const bsipa = await this.getBsipaInstalled(version);
const pluginsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.PLUGINS), this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING)]);
const libsMods = await Promise.all([this.getModsInDir(version, ModsInstallFolder.LIBS), this.getModsInDir(version, ModsInstallFolder.LIBS_PENDING)]);
return Promise.all([this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING), this.getModsInDir(version, ModsInstallFolder.LIBS_PENDING), this.getModsInDir(version, ModsInstallFolder.PLUGINS), this.getModsInDir(version, ModsInstallFolder.LIBS)]).then(dirMods => {
const modsDict = new Map<string, Mod>();
const dirMods = pluginsMods.flat().concat(libsMods.flat());
if (bsipa) {
modsDict.set(bsipa.name, bsipa);
const modsDict = new Map<string, Mod>();
if (bsipa) {
modsDict.set(bsipa.name, bsipa);
}
for (const mod of dirMods.flat()) {
if (modsDict.has(mod.name)) {
continue;
}
modsDict.set(mod.name, mod);
}
for (const mod of dirMods.flat()) {
if (modsDict.has(mod.name)) {
continue;
return Array.from(modsDict.values());
}
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
const progress = { current: 0, total: mods.length };
return new Observable<Progression>(obs => {
(async () => {
if (!mods?.length) {
throw CustomError.throw(new Error("No mods to install"), "no-mods", mods);
}
modsDict.set(mod.name, mod);
}
return Array.from(modsDict.values());
obs.next(progress);
const bsipa = popElement(mod => mod.name.toLowerCase() === "bsipa", mods);
if(bsipa){
const bsipaInstalled = await this.installMod(bsipa, version).catch(err => {
log.error("Error while installing BSIPA", err);
});
if(!bsipaInstalled){
throw CustomError.throw(new Error("BSIPA failed to install"), "cannot-install-bsipa");
}
progress.current++;
obs.next(progress);
}
for (const mod of mods) {
await this.installMod(mod, version);
progress.current++;
obs.next(progress);
}
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
});
}
public async installMods(mods: Mod[], version: BSVersion): Promise<InstallModsResult> {
if (!mods?.length) {
throw CustomError.fromError(new Error("No mods to install"), "no-mods");
}
public uninstallMods(mods: Mod[], version: BSVersion): Observable<Progression> {
const progress = { current: 0, total: mods.length };
const bsipa = mods.find(mod => mod.name.toLowerCase() === "bsipa");
if (bsipa) {
mods = mods.filter(mod => mod.name.toLowerCase() !== "bsipa");
}
return new Observable<Progression>(obs => {
(async () => {
if (!mods?.length) {
throw CustomError.throw(new Error("No mods to uninstall"), "no-mods", mods);
}
this.nbModsToInstall = mods.length + (bsipa ? 1 : 0);
this.nbInstalledMods = 0;
obs.next(progress);
if (bsipa) {
const installed = await this.installMod(bsipa, version).catch(err => {
log.error("INSTALL BSIPA", err);
return false;
});
if (!installed) {
throw CustomError.fromError(new Error("Unable to install BSIPA"), "cannot-install-bsipa");
}
}
for (const mod of mods) {
await this.installMod(mod, version);
}
return {
nbModsToInstall: this.nbModsToInstall,
nbInstalledMods: this.nbInstalledMods,
};
for (const mod of mods) {
await this.uninstallMod(mod, version);
progress.current++;
obs.next(progress);
}
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
});
}
public async uninstallMods(mods: Mod[], version: BSVersion): Promise<UninstallModsResult> {
if (!mods?.length) {
throw CustomError.fromError(new Error("No mods to uninstall"), "no-mods");
}
public uninstallAllMods(version: BSVersion): Observable<Progression> {
return new Observable<Progression>(obs => {
(async () => {
const mods = await this.getInstalledMods(version).catch(err => {
log.error(err);
return [];
});
this.nbModsToUninstall = mods.length;
this.nbUninstalledMods = 0;
const progress = { current: 0, total: mods.length };
for (const mod of mods) {
await this.uninstallMod(mod, version);
}
obs.next(progress);
return {
nbModsToUninstall: this.nbModsToUninstall,
nbUninstalledMods: this.nbUninstalledMods,
};
}
for (const mod of mods) {
await this.uninstallMod(mod, version);
progress.current++;
obs.next(progress);
}
public async uninstallAllMods(version: BSVersion): Promise<UninstallModsResult> {
const mods = await this.getInstalledMods(version);
const versionPath = await this.bsLocalService.getVersionPath(version);
if (!mods?.length) {
throw CustomError.fromError(new Error("This version has to mods to uninstall"), "no-mods");
}
this.nbModsToUninstall = mods.length;
this.nbUninstalledMods = 0;
for (const mod of mods) {
await this.uninstallMod(mod, version);
}
const versionPath = await this.bsLocalService.getVersionPath(version);
await deleteFolder(path.join(versionPath, ModsInstallFolder.PLUGINS));
await deleteFolder(path.join(versionPath, ModsInstallFolder.LIBS));
await deleteFolder(path.join(versionPath, ModsInstallFolder.IPA));
return {
nbModsToUninstall: this.nbModsToUninstall,
nbUninstalledMods: this.nbUninstalledMods,
};
await deleteFolder(path.join(versionPath, ModsInstallFolder.PLUGINS));
await deleteFolder(path.join(versionPath, ModsInstallFolder.LIBS));
await deleteFolder(path.join(versionPath, ModsInstallFolder.IPA));
})()
.catch(err => obs.error(err))
.finally(() => obs.complete());
});
}
}
@@ -0,0 +1,81 @@
import ElectronStore from "electron-store";
import { Observable, Subject } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
export class StaticConfigurationService {
private static instance: StaticConfigurationService;
public static getInstance(): StaticConfigurationService {
if (!StaticConfigurationService.instance) {
StaticConfigurationService.instance = new StaticConfigurationService();
}
return StaticConfigurationService.instance;
}
private readonly store: ElectronStore;
private readonly watchers: {
[K in StaticConfigKeys]?: Subject<StaticConfigKeyValues[K]>;
} = {};
private constructor() {
this.store = new ElectronStore();
}
public has<K extends StaticConfigKeys>(key: K): boolean {
return this.store.has(key);
}
public get<K extends StaticConfigKeys>(key: K): StaticConfigKeyValues[K] {
return this.store.get<K>(key) as StaticConfigKeyValues[K];
}
public take<K extends StaticConfigKeys>(key: K, cb: (val: StaticConfigKeyValues[K]) => void): void {
cb(this.get(key));
}
public set<K extends StaticConfigKeys>(key: K, value: StaticConfigKeyValues[K]): void {
this.store.set(key, value);
if (this.watchers[key]) {
this.watchers[key].next(value); // update watchers if any
}
}
public delete<K extends StaticConfigKeys>(key: K): void {
this.store.delete(key);
}
public getStore(): ElectronStore {
return this.store;
}
public $watch<K extends StaticConfigKeys>(key: K): Observable<StaticConfigKeyValues[K]> {
if (!this.watchers[key]) {
this.watchers[key] = new Subject() as any; // avoid type error here, the essential is that it work using the function
}
return this.watchers[key];
}
}
export interface StaticConfigKeyValues {
"versions": BSVersion[];
"installation-folder": string;
"song-details-cache-etag": string;
"disable-hadware-acceleration": boolean;
"use-symlinks": boolean;
}
export type StaticConfigKeys = keyof StaticConfigKeyValues;
export type StaticConfigGetIpcRequestResponse<K extends StaticConfigKeys> = {
request: K;
response: StaticConfigKeyValues[K];
};
export type StaticConfigSetIpcRequest<K extends StaticConfigKeys> = {
request: { key: K, value: StaticConfigKeyValues[K] };
response: void;
}
-1
View File
@@ -52,7 +52,6 @@ export class SteamService {
* @returns true if the Steam process is running as administrator
*/
public async isElevated(): Promise<boolean>{
if(process.platform === "linux"){ return true; }
const steamPid = await this.getSteamPid();
if(!steamPid){ return false; }
@@ -35,7 +35,8 @@ export class BeatSaverApiService {
const enbledTagsString = filter.enabledTags ? Array.from(filter.enabledTags) : null;
const excludedTagsString = filter.excludedTags ? Array.from(filter.excludedTags).map(tag => `!${tag}`) : null;
const tags = enbledTagsString || excludedTagsString ? [...enbledTagsString, excludedTagsString].join("|") : null;
let tags = [...(enbledTagsString ?? []), ...(excludedTagsString ?? [])].join("|");
tags = tags.length > 0 ? tags : null;
const params: Record<string, string> = this.objectToStringRecord({...filter, tags});
+9 -10
View File
@@ -1,14 +1,17 @@
import path from "path";
import { app, BrowserWindow } from "electron";
import { IpcResponse } from "shared/models/ipc";
import log from "electron-log";
import { app } from "electron";
// TODO : REFACTOR
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(path.dirname(path.dirname(__dirname)), "assets");
private readonly buildPath: string = app.isPackaged
? path.join(process.resourcesPath, "build")
: path.join(path.dirname(path.dirname(__dirname)), "build");
private constructor() {}
@@ -36,11 +39,7 @@ export class UtilsService {
return path.join(app.getPath("temp"), app.getName());
}
public ipcSend<T = unknown>(channel: string, response: IpcResponse<T>): void {
try {
BrowserWindow.getAllWindows().forEach(window => window?.webContents?.send(channel, response));
} catch (error) {
log.error(error);
}
public getBuildPath(filepath: string): string {
return path.join(this.buildPath, filepath);
}
}
@@ -1,6 +1,6 @@
import { getFoldersInFolder } from "../helpers/fs.helpers";
import path from "path";
import { VersionLinkerAction, VersionLinkFolderAction, VersionUnlinkFolderAction } from "renderer/services/version-folder-linker.service";
import { VersionLinkerAction, VersionUnlinkFolderAction } from "renderer/services/version-folder-linker.service";
import { BSVersion } from "shared/bs-version.interface";
import { LocalMapsManagerService } from "./additional-content/maps/local-maps-manager.service";
import { BSLocalVersionService } from "./bs-local-version.service";
@@ -58,17 +58,14 @@ export class VersionFolderLinkerService {
return path.join(parentPath, relativePath);
}
public async linkVersionFolder(action: VersionLinkerAction): Promise<boolean> {
public async linkVersionFolder(action: VersionLinkerAction): Promise<void> {
action.options = this.specialFolderOption(action.relativeFolder, action.options);
const versionPath = await this.localVersion.getVersionPath(action.version);
const folderPath = this.relativeToFullPath(versionPath, action.relativeFolder);
return this.folderLinker
.linkFolder(folderPath, action.options)
.catch(() => false)
.then(() => true);
return this.folderLinker.linkFolder(folderPath, action.options)
}
public async unlinkVersionFolder(action: VersionUnlinkFolderAction): Promise<boolean> {
public async unlinkVersionFolder(action: VersionUnlinkFolderAction): Promise<void> {
action.options = this.specialFolderOption(action.relativeFolder, action.options);
const versionPath = await this.localVersion.getVersionPath(action.version);
@@ -76,13 +73,10 @@ export class VersionFolderLinkerService {
action.options.moveContents = !(await this.isOtherVersionHaveFolderLinked(action.relativeFolder, folderPath));
return this.folderLinker
.unlinkFolder(folderPath, action.options)
.catch(() => false)
.then(() => true);
return this.folderLinker.unlinkFolder(folderPath, action.options);
}
public async doAction(action: VersionLinkerAction): Promise<boolean> {
public doAction(action: VersionLinkerAction): Promise<void> {
if (action.type === "link") {
return this.linkVersionFolder(action);
}
@@ -123,7 +117,7 @@ export class VersionFolderLinkerService {
for (const version of versions) {
const linkedFolders = await this.getLinkedFolders(version, { relative: true, ignoreSymlinkTargetError: true });
const actions = linkedFolders.map(folder => ({ type: "link", version, relativeFolder: folder } as VersionLinkFolderAction));
const actions = linkedFolders.map(folder => ({ type: "link", version, relativeFolder: folder } as VersionLinkerAction));
await Promise.all(actions.map(action => this.doAction(action)));
}
}
+3 -1
View File
@@ -25,7 +25,9 @@ export class WindowManagerService {
private readonly baseWindowOption: BrowserWindowConstructorOptions = {
title: APP_NAME,
icon: this.utilsService.getAssetsPath("favicon.ico"),
icon: process.platform === "linux"
? this.utilsService.getBuildPath(path.join("icons", "png", "256x256.png"))
: this.utilsService.getBuildPath(path.join("icons", "win", "favicon.ico")),
show: false,
frame: false,
titleBarOverlay: false,
@@ -1,6 +1,6 @@
import { createContext, useRef, useState } from "react";
import { BSVersion } from "shared/bs-version.interface";
import { LocalMapsListPanel } from "./maps/local-maps-list-panel.component";
import { LocalMapsListPanel, LocalMapsListPanelRef } from "./maps/local-maps-list-panel.component";
import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component";
import { FilterPanel } from "./maps/filter-panel.component";
import { MapFilter } from "shared/models/maps/beat-saver.model";
@@ -56,7 +56,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
setPlaylists: playlists$.next.bind(playlists$),
}));
const mapsRef = useRef<any>();
const mapsRef = useRef<LocalMapsListPanelRef>();
const playlistsRef = useRef<LocalPlaylistsListRef>();
const [mapFilter, setMapFilter] = useState<MapFilter>({});
@@ -106,6 +106,7 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
return [
{ icon: "export", text: "pages.version-viewer.maps.search-bar.dropdown.export-maps", onClick: () => mapsRef.current.exportMaps?.() },
{ icon: "trash", text: "pages.version-viewer.maps.search-bar.dropdown.delete-maps", onClick: () => mapsRef.current.deleteMaps?.() },
{ icon: "clean", text: "pages.version-viewer.maps.search-bar.dropdown.delete-duplicate-maps", onClick: () => mapsRef.current.removeDuplicates?.() },
];
}
@@ -118,8 +119,8 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
})();
return (
<div className="w-full h-full flex flex-col items-center justify-center gap-4">
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white">
<>
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 text-main-color-1 dark:text-white mb-4">
<BsmButton
className="flex items-center justify-center w-fit rounded-full px-2 py-1 font-bold whitespace-nowrap"
icon="add"
@@ -178,6 +179,6 @@ export function MapsPlaylistsPanel({ version, isActive }: Props) {
<LocalPlaylistsListPanel className="w-full h-full shrink-0" isActive={isActive && tabIndex === 1} ref={playlistsRef} version={version} linkedState={playlistLinkedState} filter={playlistFilter} search={search}/>
</InstalledMapsContext.Provider>
</BsContentTabPanel>
</div>
</>
);
}
@@ -2,7 +2,7 @@ import { MapsManagerService } from "renderer/services/maps-manager.service";
import { BSVersion } from "shared/bs-version.interface";
import { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useState } from "react";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { Subscription, BehaviorSubject } from "rxjs";
import { Subscription, BehaviorSubject, lastValueFrom } from "rxjs";
import { MapFilter } from "shared/models/maps/beat-saver.model";
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
import { last, tap } from "rxjs/operators";
@@ -34,7 +34,13 @@ type Props = {
isActive?: boolean;
};
export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, className, filter, search, linkedState, isActive }, forwardRef) => {
export type LocalMapsListPanelRef = {
deleteMaps: () => void;
exportMaps: () => void;
removeDuplicates: () => void;
}
export const LocalMapsListPanel = forwardRef<LocalMapsListPanelRef, Props>(({ version, className, filter, search, linkedState, isActive }, forwardRef) => {
const mapsManager = useService(MapsManagerService);
const mapsDownloader = useService(MapsDownloaderService);
@@ -63,6 +69,12 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
},
exportMaps() {
mapsManager.exportMaps(version, selectedMaps);
},
removeDuplicates() {
return lastValueFrom(mapsManager.deleteDuplicateMaps(maps$.value)).then(res => {
if(!res?.current){ return; }
loadMaps();
}).catch(noop);
}
}),[selectedMaps, maps, version]);
@@ -76,6 +88,8 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
return noop;
}, [linkedState]);
useOnUpdate(() => setSelectedMaps([]), [version]); // Clear selected maps when version changes
useEffect(() => {
if (isActiveOnce) {
loadMaps();
@@ -85,25 +99,27 @@ export const LocalMapsListPanel = forwardRef<unknown, Props>(({ version, classNa
setMaps(null);
loadPercent$.next(0);
subs.forEach(s => s.unsubscribe());
mapsDownloader.removeOnMapDownloadedListener(loadMaps);
};
}, [isActiveOnce, version, linked]);
useEffect(() => {
let sub: Subscription;
if(isActiveOnce){
mapsDownloader.addOnMapDownloadedListener((map, targetVersion) => {
sub = mapsDownloader.lastDownloadedMap$.subscribe({ next: ({map, version: targetVersion}) => {
if (!equal(targetVersion, version)) {
return;
}
setMaps((maps ? [map, ...maps] : [map]));
});
setMaps((maps$.value ? [map, ...maps$.value] : [map]));
}});
}
return () => {
mapsDownloader.removeOnMapDownloadedListener(loadMaps);
sub?.unsubscribe();
}
}, [isActiveOnce, version, maps])
}, [isActiveOnce, version])
const loadMaps = () => {
setMaps(null);
@@ -32,6 +32,8 @@ 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";
import { NpsIcon } from "renderer/components/svgs/icons/nps-icon.component";
import { SpeedIcon } from "renderer/components/svgs/icons/speed-icon.component";
export type MapItemComponentProps<T = unknown> = {
hash: string;
@@ -208,8 +210,8 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
<motion.ul key={hash} className="absolute top-[calc(100%-10px)] w-full h-fit max-h-[200%] pt-4 pb-2 px-2 overflow-y-scroll bg-light-main-color-3 dark:bg-main-color-3 text-main-color-1 dark:text-current brightness-125 rounded-md flex flex-col gap-3 scrollbar-default shadow-sm shadow-black" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={{ duration: 0.15 }} onHoverStart={diffsPanelHoverStart} onHoverEnd={diffsPanelHoverEnd}>
{Array.from(diffs.entries()).map(([charac, diffSet]) => (
<ol key={charac} className="flex flex-col w-full gap-1">
{diffSet.map(({ name, libelle, stars }) => (
<li key={`${name}${libelle}${stars}`} className="w-full h-4 flex items-center gap-1">
{diffSet.map(({ name, libelle, stars, nps, njs }) => (
<li key={`${name}${libelle}${stars}`} className="w-full h-[1.15rem] flex items-center gap-1">
{onHighlightedDiffsChange && (
<Tippy content={t("maps.map-item.hightlight-difficulty")} placement="top" theme="default">
<div className="h-full aspect-square">
@@ -218,9 +220,38 @@ export function MapItemComponent <T = unknown>({ hash, title, autor, songAutor,
</Tippy>
)}
<BsmIcon className="h-full w-fit p-px shrink-0" icon={charac} />
<span className="h-full px-2 flex items-center text-xs font-bold bg-current rounded-full" style={{ color: MAP_DIFFICULTIES_COLORS[name] }}>
{stars ? <span className="h-full block brightness-[.25]"> {stars.toFixed(2)}</span> : <span className="h-full brightness-[.25] leading-4 pb-[2px] capitalize">{parseDiffLabel(name)}</span>}
</span>
<div className="h-full px-2 shrink-0 flex items-center text-sm font-bold bg-current rounded-full" style={{ color: MAP_DIFFICULTIES_COLORS[name] }}>
{(() => {
if(stars){
return (
<div className="h-full brightness-[.15] flex justify-center items-center">
<span className="pb-0.5"> {stars.toFixed(2)}</span>
</div>
);
}
if(nps){
return (
<div className="h-full brightness-[.15] flex justify-center items-center gap-1" title={t("maps.map-filter-panel.nps")}>
<NpsIcon className="h-full py-px"/>
<span className="pb-0.5">{nps.toFixed(2)}</span>
</div>
);
}
if(njs){
return (
<p className="h-full brightness-[.15] flex justify-center items-center gap-1" title={t("maps.map-filter-panel.njs")}>
<SpeedIcon className="h-full py-px"/>
<span className="pb-0.5">{njs.toFixed(2)}</span>
</p>
);
}
return (
<div className="h-full brightness-[.15] flex justify-center items-center">
<span className="pb-0.5 capitalize">{parseDiffLabel(name)}</span>
</div>
);
})()}
</div>
<span className={cn("text-sm leading-4 pb-[2px] line-clamp-1", isDiffHightlighted({name, characteristic: charac}) && "text-yellow-400")}>{parseDiffLabel(libelle)}</span>
</li>
))}
@@ -151,7 +151,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
const obs$ = playlistService.exportPlaylists({ version, bpLists: toExport, dest: folderRes.filePaths.at(0), playlistsMaps: mapsToExport });
progess.show(obs$, true);
progess.show(obs$);
const { error } = await tryit(() => lastValueFrom(obs$));
@@ -163,7 +163,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
notification.notifySuccess({ title: "playlist.playlists-exported-title", desc: exportMaps ? "playlist.playlists-exported-desc" : "playlist.playlists-with-maps-exported-desc", duration: 5000 });
progess.hide(true);
progess.hide();
},
deletePlaylists: () => {
const toDelete = selectedPlaylists$.value?.length ? selectedPlaylists$.value : playlists$.value;
@@ -183,6 +183,8 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
return lastValueFrom(obs);
}
useOnUpdate(() => selectedPlaylists$.next([]), [version]);
useOnUpdate(() => {
if(!isActiveOnce){ return noop(); }
@@ -245,7 +247,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
if(exitCode !== ModalExitCode.COMPLETED){ return; }
const progess$ = new BehaviorSubject<ProgressionInterface>({ progression: 0 });
progess.show(progess$, true)
progess.show(progess$)
for(const [i, bpList] of enumerate(bpLists)){
@@ -254,7 +256,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
if(error){
logRenderError("Error occured while deleting playlist", error);
notification.notifyError({ title: "playlist.playlist-delete-error-title", desc: "playlist.playlist-delete-error-desc" });
progess.hide(true);
progess.hide();
return;
}
@@ -268,7 +270,7 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
notification.notifySuccess({ title: "playlist.playlists-deleted-title", desc: "playlist.playlists-deleted-desc", duration: 5000 });
progess.hide(true);
progess.hide();
};
const openPlaylistDetails = (playlistPath: string) => {
@@ -80,9 +80,6 @@ export const PlaylistItem = memo(({ title,
const showNps = minNps !== undefined && maxNps !== undefined;
const durationText = (() => {
console.log("DURATION", duration);
if (!duration) {
return null;
}
@@ -0,0 +1,42 @@
import { BsmButton, BsmButtonType } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { cn } from "renderer/helpers/css-class.helpers";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
type BasicModalOptions = {
title: string;
image: string;
body?: string;
buttons?: {
id: string;
text: string;
type: BsmButtonType,
isCancel?: boolean;
}[];
buttonsLayout?: "row" | "column";
};
export const BasicModal: ModalComponent<BasicModalOptions["buttons"][0]["id"], BasicModalOptions> = ({ resolver, options: {
data: { title, image, body, buttons, buttonsLayout = "column" }
} }) => {
const t = useTranslation();
const handleClick = (button: BasicModalOptions["buttons"][0]) => {
resolver({ exitCode: button.isCancel ? ModalExitCode.CANCELED : ModalExitCode.COMPLETED, data: button.id });
}
return (
<form className="text-gray-900 dark:text-white">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t(title)}</h1>
<BsmImage className="mx-auto h-24" image={image} />
{ body && <p className="w-full">{t(body)}</p> }
<div className={cn("grid gap-2 mt-4")} style={{ gridAutoFlow: buttonsLayout, ...(buttonsLayout === "row" ? { gridTemplateRows: `repeat(${buttons.length}, 1fr)` } : { gridTemplateColumns: `repeat(${buttons.length}, 1fr)` }) }}>
{buttons.map(button => (
<BsmButton key={button.id} typeColor={button.type} className="h-8 rounded-md text-center flex justify-center items-center" onClick={() => handleClick(button)} withBar={false} text={button.text} />
))}
</div>
</form>
);
};
@@ -0,0 +1,109 @@
import { lastValueFrom } from "rxjs";
import { useEffect, useState } from "react";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { useService } from "renderer/hooks/use-service.hook";
import Tippy from "@tippyjs/react";
import { IpcService } from "renderer/services/ipc.service";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
export const AskInstallPathModal: ModalComponent<{ installPath: string }, {}> = ({ resolver }) => {
const t = useTranslation();
const ipcService = useService(IpcService);
const [installPath, setInstallPath] = useState("");
const [installFolder, setInstallFolder] = useState("");
const [defaultInstallPath, setDefaultInstallPath] = useState("");
useEffect(() => {
lastValueFrom(ipcService.sendV2("bs-installer.default-install-path"))
.then(defaultPath => {
setInstallPath(defaultPath);
setDefaultInstallPath(defaultPath);
setInstallFolder(window.electron.path.basename(defaultPath));
});
}, []);
const selectInstallPath = async () => {
const response = await lastValueFrom(ipcService.sendV2("choose-folder"));
if (response.canceled || !response.filePaths?.length) {
return;
}
const path = response.filePaths[0];
setInstallPath(
window.electron.path.basename(path) === installFolder ?
path :
window.electron.path.join(response.filePaths[0], installFolder)
);
}
const onDefaultButtonPressed = () => {
setInstallPath(defaultInstallPath);
}
const onConfirmButtonPressed = () => {
resolver({
data: { installPath },
exitCode: ModalExitCode.COMPLETED
});
}
return (
<form
className="max-w-xl w-max"
onSubmit={event => {
event.preventDefault();
onConfirmButtonPressed();
}}>
<h1 className="tracking-wide w-full uppercase text-3xl text-center">
{t("modals.ask-install-path.title")}
</h1>
<p className="py-3">
{t("modals.ask-install-path.choose-folder-description")}
</p>
<div className="relative rounded-md pl-2 py-1 mb-3 flex items-center justify-between gap-1 w-full h-8 bg-light-main-color- dark:bg-main-color-1">
<span className="text-ellipsis overflow-hidden min-w-0 text-nowrap text-left cursor-help" title={installPath} style={{ direction: "rtl" }}>
{installPath}
</span>
<BsmButton
onClick={selectInstallPath}
className="shrink-0 whitespace-nowrap mr-2 px-2 font-bold italic text-sm rounded-md"
text="modals.ask-install-path.choose-folder"
withBar={false}
/>
</div>
<div className="h-8 grid grid-flow-col grid-cols-2 gap-2">
<Tippy
content={t("modals.ask-install-path.default-tooltip")}
theme="default"
delay={[300, 0]}
arrow={false}
placement="bottom"
>
<BsmButton
typeColor="cancel"
className="rounded-md text-center transition-all flex items-center justify-center"
onClick={onDefaultButtonPressed}
withBar={false}
text="modals.ask-install-path.default"
/>
</Tippy>
<BsmButton
typeColor="primary"
className="rounded-md text-center transition-all"
type="submit"
withBar={false}
text="misc.confirm"
/>
</div>
</form>
)
}
@@ -0,0 +1,29 @@
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png";
import { useConstant } from "renderer/hooks/use-constant.hook";
export const DeleteDuplicateMapsModal: ModalComponent<void, { maps: BsmLocalMap[] }> = ({ resolver, options: {data : { maps }}}) => {
const t = useTranslation();
const multiple = useConstant(() => maps.length > 1);
return (
<form className="text-gray-800 dark:text-gray-200 max-w-sm">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("modals.maps-actions.delete-duplicate-maps.title")}</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict} />
<p>{
multiple
? t("modals.maps-actions.delete-duplicate-maps.desc-plural", { nb: `${maps.length}` })
: t("modals.maps-actions.delete-duplicate-maps.desc", { map: `${maps.at(0).rawInfo._songName}` })
}</p>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4 h-8">
<BsmButton typeColor="cancel" className="rounded-md flex justify-center items-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
<BsmButton typeColor="primary" className="rounded-md flex justify-center items-center transition-all" onClick={() => resolver({ exitCode: ModalExitCode.COMPLETED })} withBar={false} text="misc.delete" />
</div>
</form>
);
};
@@ -67,22 +67,21 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
}, [searchParams]);
useEffect(() => {
const onMapDownloaded = (map: BsmLocalMap, targetVersion: BSVersion) => {
const sub = mapsDownloader.lastDownloadedMap$.subscribe({ next: ({ map, version: targetVersion }) => {
if (!equal(targetVersion, version)) {
return;
}
const downloadedHash = map.hash;
setOwnedMapHashs(prev => [...prev, downloadedHash]);
};
mapsDownloader.addOnMapDownloadedListener(onMapDownloaded);
}});
if (mapsDownloader.isDownloading) {
progressBar.setStyle(mapsDownloader.progressBarStyle);
}
return () => {
mapsDownloader.removeOnMapDownloadedListener(onMapDownloaded);
sub.unsubscribe();
progressBar.setStyle(null);
};
}, []);
@@ -34,8 +34,12 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
const installedMaps = useObservable(() => options.data.installedMaps$, null);
const isMissingMaps$ = useConstant(() => combineLatest([options.data.installedMaps$, options.data.localPlaylist$]).pipe(map(([maps, playlist]) => maps.length !== playlist.songs.length)));
const isPlaylistDownloading$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistDownloading(playlist.path, options.data.version))));
const isPlaylistInQueue$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(playlist => playlistDownloader.$isPlaylistInQueue(playlist.path, options.data.version))));
const isPlaylistDownloading$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(
playlist => playlistDownloader.$isPlaylistDownloading(playlist?.customData?.syncURL ?? playlist.path, options.data.version)
)));
const isPlaylistInQueue$ = useConstant(() => options.data.localPlaylist$.pipe(switchMap(
playlist => playlistDownloader.$isPlaylistInQueue(playlist?.customData?.syncURL ?? playlist.path, options.data.version)
)));
const isInQueue = useObservable(() => isPlaylistInQueue$, false);
@@ -44,7 +48,7 @@ export const LocalPlaylistDetailsModal: ModalComponent<void, Props> = ({resolver
const ignoreSongsHashs = installedMaps?.map(map => map.hash);
const obs$ = playlistDownloader.downloadPlaylist({
downloadSource: localPlaylist.path,
downloadSource: localPlaylist?.customData?.syncURL ?? localPlaylist.path,
dest: localPlaylist.path,
version: options.data.version,
ignoreSongsHashs,
@@ -17,7 +17,7 @@ export function Modal() {
useEffect(() => {
const onEscape = (e: KeyboardEvent) => {
if (e.key !== "Escape") {
if (currentModal.options?.closable === false || e.key !== "Escape") {
return;
}
currentModal.resolver({ exitCode: ModalExitCode.CLOSED });
@@ -34,6 +34,20 @@ export function Modal() {
};
}, [currentModal]);
const renderCloseButton = (modal: ModalObject) => {
return (
<div
className="w-2.5 h-2.5 absolute top-2.5 right-1.5 cursor-pointer"
onClick={e => {
e.stopPropagation();
modal.resolver({ exitCode: ModalExitCode.CLOSED });
}}
>
<BsmIcon className="size-full" icon="cross" />
</div>
)
}
const renderModal = (modal: ModalObject) => {
if (!modal?.modal) { return null; }
@@ -44,23 +58,23 @@ export function Modal() {
return (
<div className="relative p-4 text-gray-800 dark:text-gray-200 rounded-md shadow-lg shadow-black bg-gradient-to-br from-light-main-color-3 to-light-main-color-2 dark:from-main-color-3 dark:to-main-color-2">
<ThemeColorGradientSpliter className="absolute top-0 w-full left-0 h-1 rounded-t-md overflow-hidden"/>
<div
className="w-2.5 h-2.5 absolute top-2.5 right-1.5 cursor-pointer"
onClick={e => {
e.stopPropagation();
modal.resolver({ exitCode: ModalExitCode.CLOSED });
}}
>
<BsmIcon className="size-full" icon="cross" />
</div>
{modal.options?.closable === false ? undefined : renderCloseButton(modal)}
<modal.modal resolver={modal.resolver} options={modal.options} />
</div>
)
}
const onOverlayClicked = () => {
if (currentModal.options?.closable === false) {
return;
}
currentModal.resolver({ exitCode: ModalExitCode.NO_CHOICE });
}
return (
<AnimatePresence>
{currentModal ? <motion.span key={crypto.randomUUID()} onClick={() => currentModal.resolver({ exitCode: ModalExitCode.NO_CHOICE })} className="fixed size-full bg-black z-[90]" initial={{ opacity: 0 }} animate={{ opacity: currentModal && 0.6 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} /> : undefined}
{currentModal ? <motion.span key={crypto.randomUUID()} onClick={onOverlayClicked} className="fixed size-full bg-black z-[90]" initial={{ opacity: 0 }} animate={{ opacity: currentModal && 0.6 }} exit={{ opacity: 0 }} transition={{ duration: 0.2 }} /> : undefined}
{modals?.map(modal => (
<motion.div key={crypto.randomUUID()} className="fixed z-[90] top-1/2 left-1/2" initial={{ y: "100vh", x: "-50%" }} animate={{y: "-50%", scale: modal === currentModal ? 1 : 0, opacity: modal === currentModal ? 1 : 0, display: modal === currentModal ? "block" : ["block", "none"]}} exit={{ y: "100vh" }}>
{renderModal(modal)}
@@ -70,6 +70,8 @@ export const ModelsGrid = forwardRef<unknown, Props>(({ className, version, type
[modelsSelected, models]
);
useOnUpdate(() => setModelsSelected([]), [version]);
useOnUpdate(() => setRenderableModels(() => (
models?.map(model => ({
model,
@@ -0,0 +1,33 @@
import { ToogleSwitch } from "../shared/toogle-switch.component";
type Item = {
text: string;
desc?: string;
checked?: boolean;
onChange?: (isChecked: boolean) => void|Promise<void>;
};
type Props = {
items: Item[];
};
export function SettingToogleSwitchGrid({ items }: Readonly<Props>) {
const handleItemChange = (item: Item, state: boolean) => {
item.onChange?.(state);
};
return (
<div className="flex flex-col gap-1.5">
{items.map((item) => (
<div key={item.text} className="flex justify-between items-center bg-theme-1 py-2 px-3 rounded-md gap-5">
<div className="flex flex-col justify-center gap-px grow">
<h2 className="font-bold">{item.text}</h2>
<p className="text-sm text-gray-600 dark:text-gray-400">{item.desc}</p>
</div>
<ToogleSwitch checked={item.checked} className="shrink-0 h-7 w-12" onChange={checked => handleItemChange(item, checked)}/>
</div>
))}
</div>
)
}
@@ -25,13 +25,13 @@ export const BsContentTabItem: BsContentTabItemComponent = ({ text, icon: Icon,
}
return (
<li className={`relative w-full cursor-pointer flex-1 text-center text-lg font-bold flex justify-center items-center content-center px-7 bg-light-main-color-2 dark:bg-main-color-2 hover:bg-light-main-color-1 dark:hover:bg-main-color-1 ${active && "!bg-light-main-color-1 dark:!bg-main-color-1"}`} onClick={handleClick}>
<li className={`relative w-full cursor-pointer text-center font-bold flex justify-center items-center content-center px-7 bg-theme-2 hover:bg-theme-1 rounded-l-md ${active && "!bg-theme-1"}`} onClick={handleClick}>
<div className="flex flex-col gap-0.5 justify-start items-center text-main-color-1 dark:text-gray-200">
<Icon className="w-7 h-7"/>
<Icon className="size-7"/>
<span className=" font-thin italic text-xs">{t(text)}</span>
</div>
{linkProps && (
<div className="flex items-center absolute top-1.5 left-1.5">
<div className="absolute top-1.5 left-1.5">
<LinkButton
state={linkProps.state}
className={linkProps.className ?? "block w-6 h-6 aspect-square blur-0 cursor-pointer hover:brightness-75"}
@@ -15,8 +15,8 @@ export function BsContentTabPanel({className, tabIndex, tabs, onTabChange, child
const sliderColor = useThemeColor("second-color");
return (
<div className={className ?? "w-full h-full flex flex-row bg-light-main-color-1 dark:bg-main-color-1 rounded-md shadow-black shadow-md overflow-hidden"}>
<nav className="h-full grid grid-flow-row !rounded-none shadow-sm flex-shrink-0">
<div className={className ?? "flex-grow basis-0 min-h-0 w-full flex flex-row bg-light-main-color-1 dark:bg-main-color-1 rounded-md shadow-black shadow-md"}>
<nav className="h-full grid grid-flow-row shadow-sm flex-shrink-0">
{tabs.map((tab, i) => (
<BsContentTabItem
key={`${tab.text}${tab.icon}`}
@@ -26,9 +26,11 @@ export function BsContentTabPanel({className, tabIndex, tabs, onTabChange, child
/>
))}
</nav>
<LaserSlider className="h-full w-1 relative shrink-0" mode="vertical" color={sliderColor} nbSteps={tabs.length} step={tabIndex}/>
<div className="flex-grow h-full flex flex-col transition-all duration-300" style={{ translate: `0 ${0 - tabIndex * 100}%` }}>
{children}
<LaserSlider className="h-full w-1 shrink-0" mode="vertical" color={sliderColor} nbSteps={tabs.length} step={tabIndex}/>
<div className="flex-grow overflow-y-hidden">
<div className="size-full flex flex-col transition-transform duration-300" style={{ transform: `translateY(${0 - tabIndex * 100}%)` }}>
{children}
</div>
</div>
</div>
)
@@ -79,7 +79,7 @@ export const BsmDropdownButton = forwardRef(({ className, classNames, buttonColo
{items?.map(
i =>
i && (
<div key={crypto.randomUUID()} onClick={() => i.onClick?.()} className="flex w-full px-3 py-2 hover:backdrop-brightness-150">
<div key={crypto.randomUUID()} onClick={() => { setExpanded(() => false); i.onClick?.()}} className="flex w-full px-3 py-2 hover:backdrop-brightness-150">
{i.icon && <BsmIcon icon={i.icon} className="h-5 w-5 mr-1 text-inherit" />}
<span className="w-max">{t(i.text)}</span>
</div>
@@ -1,4 +1,5 @@
import { CSSProperties } from "react";
import { cn } from "renderer/helpers/css-class.helpers";
type Props = Readonly<{
mode: "horizontal" | "vertical";
@@ -27,7 +28,7 @@ export function LaserSlider({mode, color, className, nbSteps, step}: Props) {
})();
return (
<div className={className} style={{ color }}>
<div className={cn("relative", className)} style={{ color }}>
<span className="absolute h-full w-full bg-current brightness-50" />
<span className="absolute block bg-current transition-transform duration-300 shadow-center shadow-current" style={sliderStyle} />
</div>
@@ -0,0 +1,83 @@
import { useMemo } from "react"
import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
import { cn } from "renderer/helpers/css-class.helpers";
import { useConstant } from "renderer/hooks/use-constant.hook";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
type Props = {
checked?: boolean;
className?: string;
classNames?: {
container?: string;
dot?: string;
active?: string;
inactive?: string;
}
bgColor?: string;
onChange?: (isChecked: boolean) => void;
}
export function ToogleSwitch({ checked, className, classNames, bgColor, onChange }: Readonly<Props>) {
const uuid = useConstant(() => crypto.randomUUID());
const { firstColor } = useThemeColor();
const backgroundColor = useMemo(() => bgColor ?? firstColor, [bgColor, firstColor]);
const dotColor = useMemo(() => {
return getCorrectTextColor(backgroundColor);
}, [backgroundColor]);
const textColor = useMemo(() => {
return getCorrectTextColor(dotColor)
}, [dotColor]);
const handleCheckboxChange = () => {
onChange?.(!checked);
}
return (
<label className={cn("flex cursor-pointer select-none items-center h-8 w-14", className, classNames?.container)} htmlFor={uuid}>
<div className='relative size-full rounded-full p-1 bg-neutral-500 transition-colors duration-200' style={{ backgroundColor: checked && backgroundColor }}>
<input
id={uuid}
type='checkbox'
checked={checked}
onChange={handleCheckboxChange}
className='sr-only peer'
/>
<div
className={cn("dot top-0 left-0 flex h-full aspect-square items-center justify-center rounded-full transition duration-200 peer-checked:translate-x-full", classNames?.dot)}
style={{ backgroundColor: dotColor }}
>
{checked && <span className="text-current" style={{ color: textColor }}>
<svg
width='11'
height='8'
viewBox='0 0 11 8'
fill='none'
>
<path
d='M10.0915 0.951972L10.0867 0.946075L10.0813 0.940568C9.90076 0.753564 9.61034 0.753146 9.42927 0.939309L4.16201 6.22962L1.58507 3.63469C1.40401 3.44841 1.11351 3.44879 0.932892 3.63584C0.755703 3.81933 0.755703 4.10875 0.932892 4.29224L0.932878 4.29225L0.934851 4.29424L3.58046 6.95832C3.73676 7.11955 3.94983 7.2 4.1473 7.2C4.36196 7.2 4.55963 7.11773 4.71406 6.9584L10.0468 1.60234C10.2436 1.4199 10.2421 1.1339 10.0915 0.951972ZM4.2327 6.30081L4.2317 6.2998C4.23206 6.30015 4.23237 6.30049 4.23269 6.30082L4.2327 6.30081Z'
fill='currentColor'
stroke='currentColor'
strokeWidth='0.4'
/>
</svg>
</span>}
{!checked && <span className="text-current" style={{ color: textColor }}>
<svg
className='h-4 w-4 stroke-current'
fill='black'
viewBox='0 0 24 24'
>
<path
strokeLinecap='round'
strokeLinejoin='round'
strokeWidth='2'
d='M6 18L18 6M6 6l12 12'
/>
</svg>
</span>}
</div>
</div>
</label>
)
}
@@ -61,9 +61,10 @@ import { EyeCrossIcon } from "./icons/eye-cross-icon.component";
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";
import { CleanIcon } from "./icons/clean-icon.component";
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 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" | "clean" | "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
@@ -256,6 +257,11 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
if(icon === "web-site") {
return <WebSiteIcon className={className} style={style} />;
}
if(icon === "clean") {
return <CleanIcon className={className} style={style} />;
}
return <TrashIcon className={className} style={style} />;
@@ -0,0 +1,12 @@
import { createSvgIcon } from "../svg-icon.type";
export const CleanIcon = createSvgIcon((props, ref) => {
return (
<svg ref={ref} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"color="currentColor" fill="none" {...props}>
<path d="m21 3-8 8.5m-3.554-.415c-2.48.952-4.463.789-6.446.003.5 6.443 3.504 8.92 7.509 9.912 0 0 3.017-2.134 3.452-7.193.047-.548.07-.821-.043-1.13-.114-.309-.338-.53-.785-.973-.736-.728-1.103-1.092-1.54-1.184-.437-.09-1.007.128-2.147.565" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M4.5 16.446S7 16.93 9.5 15" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M8.5 7.25a1.25 1.25 0 1 1-2.5 0 1.25 1.25 0 0 1 2.5 0Z" stroke="currentColor" strokeWidth="2"/>
<path d="M11 4v.1" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
);
});
@@ -0,0 +1,9 @@
import { createSvgIcon } from "../svg-icon.type";
export const SpeedIcon = createSvgIcon((props, ref) => {
return (
<svg ref={ref} {...props} xmlns="http://www.w3.org/2000/svg" viewBox="0 -960 960 960" fill="currentColor">
<path d="M416.24-338.24q23.88 24 62.7 24.38 38.82.38 58.82-28.38l160.65-235.78q12.96-19.44-3.24-36.01-16.19-16.58-35.39-3.38L422.24-456q-28.76 20-29.38 56.94-.62 36.94 23.38 60.82ZM480-828.54q35.66 0 70.66 6.02 34.99 6.02 69.27 18.06 21.44 8 26.7 29.32 5.26 21.31-6.22 41.63-12.48 21.31-34.33 28.27-21.86 6.96-45.54-.04-19.38-5.05-39.13-7.57-19.76-2.52-41.41-2.52-129.57 0-223.35 92.54t-93.78 224.59q0 43.76 10.88 80.92 10.88 37.17 30.38 72.69h551.72q20.12-36.23 30.7-74.54 10.58-38.31 10.58-83.07 0-18.66-2.52-39.44-2.52-20.78-8.57-41.1-6.76-23.68.22-46.85 6.98-23.17 28.1-35.43 20.03-12.14 41.47-5.14 21.43 7 29.67 28.44 11.55 32.93 18.18 67.72 6.62 34.8 6.62 69.8 0 60.24-14.74 116.03-14.74 55.8-43.97 106.12-13.2 24.44-38.11 37.54-24.91 13.09-52.44 13.09H198.96q-27.49 0-52.78-13.33-25.29-13.34-37.77-37.3-27.24-47.24-42.98-101.98Q49.7-338.81 49.7-398.5q0-88.46 33.85-166.57 33.86-78.12 92.26-136.9 58.39-58.79 136.81-92.68 78.42-33.89 167.38-33.89Zm20.11 328.43Z"/>
</svg>
);
})
@@ -13,7 +13,6 @@ import { lastValueFrom } from "rxjs";
import { useWindowControls } from "renderer/hooks/use-window-controls.hook";
export default function TitleBar({ template = "index.html" }: { template: AppWindow }) {
const ipcService = useService(IpcService);
const audio = useService(AudioPlayerService);
const windowControls = useWindowControls();
@@ -52,7 +51,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
windowControls.unmaximise();
};
const toogleMaximize = () => {
const toggleMaximize = () => {
if (maximized) {
resetWindow();
} else {
@@ -87,27 +86,28 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
</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>
<div onClick={minimizeWindow} className="text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-[#4F545C] cursor-pointer w-11 h-full shrink-0 flex justify-center items-center" id="min-button">
<button onClick={minimizeWindow} className="text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-[#4F545C] cursor-pointer w-11 h-full shrink-0 flex justify-center items-center" id="min-button">
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12">
<rect fill="currentColor" width="10" height="1" x="1" y="6">
{" "}
</rect>
<rect fill="currentColor" width="10" height="1" x="1" y="6" />
</svg>
</div>
<div onClick={toogleMaximize} className="text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-[#4F545C] cursor-pointer w-11 h-full shrink-0 flex justify-center items-center" id="max-button">
</button>
<button onClick={toggleMaximize} className="text-gray-800 dark:text-gray-200 hover:bg-gray-300 dark:hover:bg-[#4F545C] cursor-pointer w-11 h-full shrink-0 flex justify-center items-center" id="max-button">
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12">
<rect width="9" height="9" x="1.5" y="1.5" fill="none" stroke="currentColor">
{" "}
</rect>
{maximized ? (
<>
<rect width="9" height="9" x="0.5" y="2.5" fill="none" stroke="currentColor" />
<path d="M 2.5 2.5 V 0.5 H 11.5 V 9.5 H 9.5" fill="none" stroke="currentColor" />
</>
) : (
<rect width="9" height="9" x="1.5" y="1.5" fill="none" stroke="currentColor" />
)}
</svg>
</div>
<div onClick={closeWindow} className="text-gray-800 dark:text-gray-200 cursor-pointer w-11 h-full shrink-0 flex justify-center items-center" id="close-button" draggable="false">
</button>
<button onClick={closeWindow} className="text-gray-800 dark:text-gray-200 cursor-pointer w-11 h-full shrink-0 flex justify-center items-center" id="close-button" draggable="false">
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12">
<polygon fill="currentColor" fillRule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1">
{" "}
</polygon>
<polygon fill="currentColor" fillRule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1" />
</svg>
</div>
</button>
</div>
</header>
);
@@ -130,7 +130,7 @@ export default function TitleBar({ template = "index.html" }: { template: AppWin
<div id="window-controls" className="h-full flex shrink-0">
<div onClick={closeWindow} className="text-gray-200 cursor-pointer w-7 h-full shrink-0 flex justify-center items-center rounded-bl-md" id="close-button" draggable="false">
<svg aria-hidden="false" width="12" height="12" viewBox="0 0 12 12">
<polygon fill="currentColor" fillRule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1"/>
<polygon fill="currentColor" fillRule="evenodd" points="11 1.576 6.583 6 11 10.424 10.424 11 6 6.583 1.576 11 1 10.424 5.417 6 1 1.576 1.576 1 6 5.417 10.424 1" />
</svg>
</div>
</div>
@@ -3,22 +3,26 @@ import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { Mod } from "shared/models/mods/mod.interface";
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";
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 };
type Props = {
className?: string;
mod: Mod;
installedVersion: string;
isDependency?: boolean;
isSelected?: boolean;
onChange?: (val: boolean) => void;
wantInfo?: boolean;
onWantInfo?: (mod: Mod) => void;
disabled?: boolean;
onUninstall?: () => void;
};
export function ModItem({ className, mod, installedVersion, isDependency, isSelected, onChange, wantInfo, onWantInfo }: Props) {
const modsManager = useService(BsModsManagerService);
const pageState = useService(PageStateService);
export function ModItem({ className, mod, installedVersion, isDependency, isSelected, onChange, wantInfo, onWantInfo, disabled, onUninstall }: Props) {
const themeColor = useThemeColor("second-color");
const uninstalling = useObservable(() => modsManager.isUninstalling$);
const clickRef = useRef();
const isChecked = useMemo(() => isDependency || isSelected || mod.required, [isDependency, isSelected, mod.required]);
@@ -37,10 +41,6 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
const wantInfoStyle: CSSProperties = wantInfo ? { borderColor: themeColor } : { borderColor: "transparent" };
const isOutDated = installedVersion ? gt(mod.version, installedVersion) : false;
const uninstall = () => {
modsManager.uninstallMod(mod, pageState.getState());
};
const handleWantInfo = (e: MouseEvent) => {
e.preventDefault();
onWantInfo(mod);
@@ -53,7 +53,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
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(!isChecked)} disabled={mod.required || isDependency} checked={isChecked} />
<BsmCheckbox className="h-full aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.required || isDependency || disabled} 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}
@@ -72,11 +72,11 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
<BsmButton
className="z-[1] h-7 w-7 p-[5px] rounded-full group-hover:brightness-90"
icon="trash"
disabled={uninstalling}
disabled={disabled}
withBar={false}
onClick={e => {
e.stopPropagation();
uninstall();
onUninstall?.();
}}
/>
)}
@@ -3,18 +3,22 @@ import { useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { PageStateService } from "renderer/services/page-state.service";
import { Mod } from "shared/models/mods/mod.interface";
import { ModItem } from "./mod-item.component";
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 };
type Props = {
modsMap: Map<string, Mod[]>;
installed: Map<string, Mod[]>;
modsSelected: Mod[];
onModChange: (selected: boolean, mod: Mod) => void;
moreInfoMod?: Mod;
onWantInfos: (mod: Mod) => void
disabled?: boolean;
uninstallMod?: (mods: Mod) => void;
uninstallAllMods?: () => void;
};
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos }: Props) {
const pageState = useService(PageStateService);
const modsManager = useService(BsModsManagerService);
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos, disabled, uninstallMod, uninstallAllMods }: Props) {
const [filter, setFilter] = useState("");
const [filterEnabled, setFilterEnabled] = useState(false);
@@ -33,15 +37,11 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
const isDependency = (mod: Mod): boolean => {
return modsSelected.some(m => {
const deps = m.dependencies.map(dep =>
Array.from(modsMap.values())
.flat()
.find(m => dep.name === m.name)
);
const deps = m.dependencies?.map(dep => Array.from(modsMap.values()).flat().find(m => dep.name === m.name)) ?? [];
if (deps.some(depMod => depMod.name === mod.name)) {
return true;
}
return deps.some(depMod => depMod.dependencies.some(depModDep => depModDep.name === mod.name));
return deps.some(depMod => depMod.dependencies?.some(depModDep => depModDep.name === mod.name));
});
};
@@ -54,10 +54,6 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
setFilterEnabled(b => !b);
};
const handleUninstallAll = () => {
modsManager.uninstallAllMods(pageState.getState());
};
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">
@@ -70,7 +66,7 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 px-2 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.latest")}</span>
<span className="z-10 sticky flex items-center justify-center top-0 bg-inherit border-b-2 border-main-color-1 h-8 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.description")}</span>
<span className="z-10 sticky top-0 bg-inherit border-b-2 border-main-color-1 h-8 flex justify-start items-center py-1 pl-[3px] min-w-[50px]">
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: handleUninstallAll }]} />
<BsmDropdownButton className="h-full aspect-square relative rounded-full bg-light-main-color-1 dark:bg-main-color-3" withBar={false} icon="three-dots" buttonClassName="!rounded-full !p-[2px] !bg-light-main-color-2 dark:!bg-main-color-2 hover:!bg-light-main-color-1 dark:hover:!bg-main-color-3" menuTranslationY="5px" items={[{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: () => uninstallAllMods?.() }]} />
</span>
{Array.from(modsMap.keys()).map(
@@ -78,7 +74,19 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
modsMap.get(key).some(mod => mod.name.toLowerCase().includes(filter)) && (
<ul key={key} className="contents">
<h2 className="col-span-full py-1 font-bold pl-3">{key}</h2>
{modsMap.get(key).map(mod => mod.name.toLowerCase().includes(filter) && <ModItem key={mod.name} className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1 hover:cursor-pointer" mod={mod} installedVersion={installedModVersion(key, mod)} isDependency={isDependency(mod)} isSelected={isSelected(mod)} onChange={val => onModChange(val, mod)} onWantInfo={onWantInfos} wantInfo={mod.name === moreInfoMod?.name} />)}
{modsMap.get(key).map(mod => mod.name?.toLowerCase().includes(filter) && (
<ModItem
key={mod.name}
className="contents bg-light-main-color-3 dark:bg-main-color-1 text-main-color-1 dark:text-light-main-color-1 hover:cursor-pointer"
mod={mod} installedVersion={installedModVersion(key, mod)}
isDependency={isDependency(mod)}
isSelected={isSelected(mod)}
onChange={val => onModChange(val, mod)}
onWantInfo={onWantInfos}
wantInfo={mod.name === moreInfoMod?.name}
disabled={disabled}
onUninstall={() => uninstallMod?.(mod)} />
))}
</ul>
)
)}
@@ -9,8 +9,7 @@ import { BsmButton } from "renderer/components/shared/bsm-button.component";
import BeatWaitingImg from "../../../../../../assets/images/apngs/beat-waiting.png";
import BeatConflictImg from "../../../../../../assets/images/apngs/beat-conflict.png";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { skip, filter } from "rxjs/operators";
import { Subscription, lastValueFrom, noop } from "rxjs";
import { lastValueFrom } from "rxjs";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { LinkOpenerService } from "renderer/services/link-opener.service";
import { useInView } from "framer-motion";
@@ -20,6 +19,8 @@ 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";
import { noop } from "shared/helpers/function.helpers";
import { UninstallAllModsModal } from "renderer/components/modal/modal-types/uninstall-all-mods-modal.component";
export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; onDisclamerDecline: () => void }) {
const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer";
@@ -39,7 +40,8 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
const [moreInfoMod, setMoreInfoMod] = useState(null as Mod);
const [reinstallAllMods, setReinstallAllMods] = useState(false);
const isOnline = useObservable(() => os.isOnline$);
const installing = useObservable(() => modsManager.isInstalling$);
const [installing, setInstalling] = useState(false);
const [uninstalling, setUninstalling] = useState(false);
const downloadRef = useRef(null);
const [downloadWith, setDownloadWidth] = useState(0);
@@ -98,16 +100,39 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
});
if (!modsToInstall.length) {
notification.notifyInfo({ title: "Mods déjà installées", desc: "Tous les mods séléctionnées sont déjà installées" });
notification.notifyInfo({ title: "pages.version-viewer.mods.notifications.all-mods-already-installed.title", desc: "pages.version-viewer.mods.notifications.all-mods-already-installed.description" });
loadMods();
return;
}
modsManager.installMods(modsToInstall, version).then(() => {
setInstalling(() => true)
lastValueFrom(modsManager.installMods(modsToInstall, version)).then(() => {
loadMods();
}).catch(noop).finally(() => setInstalling(() => false));
};
const uninstallMod = (mod: Mod): void => {
setUninstalling(() => true);
lastValueFrom(modsManager.uninstallMod(mod, version)).catch(noop).finally(() => {
loadMods();
setUninstalling(() => false);
});
};
const uninstallAllMods = async () => {
const res = await modals.openModal(UninstallAllModsModal, {data: version});
if (res.exitCode !== ModalExitCode.COMPLETED) {
return;
}
setUninstalling(() => true);
lastValueFrom(modsManager.uninstallAllMods(version)).catch(noop).finally(() => {
loadMods();
setUninstalling(() => false);
})
};
const loadMods = (): Promise<void> => {
if (os.isOffline) {
return Promise.resolve();
@@ -122,13 +147,12 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
return promise().then(([available, installed]) => {
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)));
setModsSelected(() => available.filter(m => m.required || defaultMods.some(d => m.name?.toLowerCase() === d?.toLowerCase()) || installed.some(i => m.name === i.name)));
setModsInstalled(() => modsToCategoryMap(installed));
});
};
useEffect(() => {
const subs: Subscription[] = [];
if(!isVisible || !isOnline){
return noop();
@@ -153,22 +177,12 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
}
loadMods();
subs.push(
modsManager.isUninstalling$.pipe(
skip(1),
filter(uninstalling => !uninstalling)
).subscribe(() => {
loadMods();
})
);
});
return () => {
setMoreInfoMod(null);
setModsAvailable(null);
setModsInstalled(null);
subs.forEach(s => s.unsubscribe());
};
}, [isVisible, isOnline, version]);
@@ -195,7 +209,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
return (
<>
<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} />
<ModsGrid modsMap={modsAvailable} installed={modsInstalled} modsSelected={modsSelected} onModChange={handleModChange} moreInfoMod={moreInfoMod} onWantInfos={handleMoreInfo} disabled={uninstalling || installing} uninstallMod={uninstallMod} uninstallAllMods={uninstallAllMods}/>
</div>
<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 }}/>
+1 -1
View File
@@ -9,5 +9,5 @@ export function useConstant<T>(fn: () => T): T {
ref.current = { v: fn() };
}
return ref.current.v;
return ref.current?.v;
}
+92 -5
View File
@@ -41,8 +41,14 @@ import { OculusIcon } from "renderer/components/svgs/icons/oculus-icon.component
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
import { AutoUpdaterService } from "renderer/services/auto-updater.service";
import BeatWaitingImg from "../../../assets/images/apngs/beat-waiting.png";
import BeatConflict from "../../../assets/images/apngs/beat-conflict.png";
import { logRenderError } from "renderer";
import { BSLauncherService } from "renderer/services/bs-launcher.service";
import { SettingToogleSwitchGrid } from "renderer/components/settings/setting-toogle-switch-grid.component";
import { BasicModal } from "renderer/components/modal/basic-modal.component";
import { StaticConfigurationService } from "renderer/services/static-configuration.service";
import { tryit } from "shared/helpers/error.helpers";
import { InstallationLocationService } from "renderer/services/installation-location.service";
export function SettingsPage() {
@@ -62,6 +68,8 @@ export function SettingsPage() {
const modelsManager = useService(ModelsManagerService);
const versionLinker = useService(VersionFolderLinkerService);
const autoUpdater = useService(AutoUpdaterService);
const staticConfig = useService(StaticConfigurationService);
const installationLocationService = useService(InstallationLocationService);
const { firstColor, secondColor } = useThemeColor();
@@ -92,6 +100,8 @@ export function SettingsPage() {
const [playlistsDeepLinkEnabled, setPlaylistsDeepLinkEnabled] = useState(false);
const [modelsDeepLinkEnabled, setModelsDeepLinkEnabled] = useState(false);
const [hasDownloaderSession, setHasDownloaderSession] = useState(false);
const [hardwareAccelerationEnabled, setHardwareAccelerationEnabled] = useState(true);
const [useSymlink, setUseSymlink] = useState(false);
const appVersion = useObservable(() => ipcService.sendV2("current-version"));
const [isChangelogAvailable, setIsChangelogAvailable] = useState(true);
@@ -103,6 +113,9 @@ export function SettingsPage() {
mapsManager.isDeepLinksEnabled().then(enabled => setMapDeepLinksEnabled(() => enabled));
playlistsManager.isDeepLinksEnabled().then(enabled => setPlaylistsDeepLinkEnabled(() => enabled));
modelsManager.isDeepLinksEnabled().then(enabled => setModelsDeepLinkEnabled(() => enabled));
staticConfig.get("disable-hadware-acceleration").then(disabled =>setHardwareAccelerationEnabled(() => disabled !== true));
staticConfig.get("use-symlinks").then(useSymlinks => setUseSymlink(() => useSymlinks));
}, []);
const allDeepLinkEnabled = mapDeepLinksEnabled && playlistsDeepLinkEnabled && modelsDeepLinkEnabled;
@@ -113,8 +126,7 @@ export function SettingsPage() {
};
const loadInstallationFolder = () => {
steamDownloader.getInstallationFolder().then(res => {
console.log("AAAAA", res);
installationLocationService.getInstallationFolder().then(res => {
setInstallationFolder(res);
});
};
@@ -124,7 +136,14 @@ export function SettingsPage() {
}
const clearDownloadersSession = () => {
if (!hasDownloaderSession) {
return;
}
steamDownloader.deleteSteamSession();
notificationService.notifyInfo({
title: "pages.settings.steam-and-oculus.logout-success",
});
loadDownloadersSession();
}
@@ -192,10 +211,10 @@ export function SettingsPage() {
notificationService.notifySuccess({ title: "notifications.settings.move-folder.success.titles.transfer-started", desc: "notifications.settings.move-folder.success.descs.transfer-started" });
lastValueFrom(steamDownloader.setInstallationFolder(fileChooserRes.filePaths[0])).then(res => {
lastValueFrom(installationLocationService.setInstallationFolder(fileChooserRes.filePaths[0], true)).then(res => {
progressBarService.complete();
progressBarService.hide(true);
progressBarService.hide();
setInstallationFolder(res);
@@ -207,7 +226,7 @@ export function SettingsPage() {
});
}).catch((err: BsmException) => {
progressBarService.hide(true);
progressBarService.hide();
if (err?.code === "COPY_TO_SUBPATH") {
notificationService.notifyError({ title: "notifications.settings.move-folder.errors.titles.transfer-failed", desc: "notifications.settings.move-folder.errors.descs.COPY_TO_SUBPATH", duration: 10_000 });
@@ -220,6 +239,66 @@ export function SettingsPage() {
});
};
const onChangeHardwareAcceleration = async (newHardwareAccelerationEnabled: boolean) => {
if(newHardwareAccelerationEnabled === hardwareAccelerationEnabled){ return; }
const res = await modalService.openModal(BasicModal, { data: {
title: "pages.settings.advanced.hardware-acceleration.modal.title",
body: "pages.settings.advanced.hardware-acceleration.modal.body",
image: BeatConflict,
buttons: [
{ id: "cancel", text: "misc.cancel", type: "cancel", isCancel: true },
{ id: "confirm", text: "pages.settings.advanced.hardware-acceleration.modal.confirm-btn", type: "error" }
]
}});
if(res.exitCode !== ModalExitCode.COMPLETED || res.data !== "confirm"){ return; }
const { error } = await tryit(() => staticConfig.set("disable-hadware-acceleration", !newHardwareAccelerationEnabled));
if(error){
notificationService.notifyError({ title: "notifications.types.error", desc: "pages.settings.advanced.hardware-acceleration.error-notification.message" });
setHardwareAccelerationEnabled(() => !newHardwareAccelerationEnabled);
return;
}
setHardwareAccelerationEnabled(() => newHardwareAccelerationEnabled);
if(!progressBarService.require()){
return;
}
await lastValueFrom(ipcService.sendV2("restart-app"));
};
const onChangeUseSymlinks = async (newUseSymlink: boolean) => {
if(newUseSymlink === useSymlink){ return; }
if(newUseSymlink){
const res = await modalService.openModal(BasicModal, { data: {
title: "pages.settings.advanced.use-symlinks.modal.title",
body: "pages.settings.advanced.use-symlinks.modal.body",
image: BeatConflict,
buttons: [
{ id: "cancel", text: "misc.cancel", type: "cancel", isCancel: true },
{ id: "confirm", text: "pages.settings.advanced.use-symlinks.modal.confirm-btn", type: "error" }
]
}});
if(res.exitCode !== ModalExitCode.COMPLETED || res.data !== "confirm"){ return; }
}
const { error } = await tryit(() => staticConfig.set("use-symlinks", newUseSymlink));
if(error){
notificationService.notifyError({ title: "notifications.types.error", desc: "pages.settings.advanced.use-symlinks.error-notification.message" });
return;
}
setUseSymlink(() => newUseSymlink);
}
const toogleShowSupporters = () => {
setShowSupporters(show => !show);
};
@@ -501,6 +580,14 @@ export function SettingsPage() {
</div>
</SettingContainer>
</SettingContainer>
<SettingContainer title="pages.settings.advanced.title" description="pages.settings.advanced.description">
<SettingToogleSwitchGrid items={[
{ checked: hardwareAccelerationEnabled, text: t("pages.settings.advanced.hardware-acceleration.title"), desc: t("pages.settings.advanced.hardware-acceleration.description"), onChange: onChangeHardwareAcceleration },
{ checked: useSymlink, text: t("pages.settings.advanced.use-symlinks.title"), desc: t("pages.settings.advanced.use-symlinks.description"), onChange: onChangeUseSymlinks },
]}/>
</SettingContainer>
<Tippy content={isChangelogAvailable ? t("pages.settings.changelogs.open") : t("pages.settings.changelogs.not-founds")} placement="left" className="font-bold bg-main-color-3">
<div className="!bg-light-main-color-1 dark:!bg-main-color-1 rounded-md py-1 px-2 font-bold float-right mb-5 hover:brightness-125 h-auto w-auto">
<BsmButton onClick={handleVersionClick} text={`v${appVersion}`} withBar={false} typeColor="none"/>
+1
View File
@@ -10,6 +10,7 @@ declare global {
};
path: {
sep: "/"|"\\";
basename: (path: string) => string;
join: (...args: string[]) => string;
};
};
+19 -11
View File
@@ -1,10 +1,10 @@
import { map } from "rxjs/operators";
import { IpcService } from "./ipc.service";
import { ProgressBarService } from "./progress-bar.service";
import { ModalService } from "renderer/services/modale.service";
import { ChangelogModal } from "renderer/components/modal/modal-types/chabgelog-modal/changelog-modal.component";
import { ConfigurationService } from "./configuration.service";
import { Observable, lastValueFrom } from "rxjs";
import { Progression } from "main/helpers/fs.helpers";
export interface Changelog {
@@ -22,8 +22,6 @@ export class AutoUpdaterService {
private progressService: ProgressBarService;
private ipcService: IpcService;
private downloadProgress$: Observable<number>;
private modal: ModalService;
private configurationService: ConfigurationService;
@@ -42,22 +40,28 @@ export class AutoUpdaterService {
this.ipcService = IpcService.getInstance();
this.modal = ModalService.getInstance();
this.configurationService = ConfigurationService.getInstance();
this.downloadProgress$ = this.ipcService.watch<number>("update-download-progress").pipe(map(res => (res.success ? res.data : 0)));
}
public isUpdateAvailable(): Promise<boolean> {
return lastValueFrom(this.ipcService.sendV2("check-update")).catch(() => false);
}
public downloadUpdate(): Promise<boolean> {
const promise = lastValueFrom(this.ipcService.sendV2("download-update")).then(() => true).catch(() => false);
this.progressService.show(this.downloadProgress$, true);
return promise;
public downloadUpdate(): Observable<Progression> {
return new Observable<Progression>(obs => {
const download$ = this.ipcService.sendV2("download-update");
this.progressService.show(download$);
const sub = download$.subscribe(obs);
return () => {
sub.unsubscribe();
this.progressService.hide();
}
});
}
public quitAndInstall() {
lastValueFrom(this.ipcService.sendV2("install-update"));
public quitAndInstall(): Promise<void> {
return lastValueFrom(this.ipcService.sendV2("install-update"));
}
public getLastAppVersion(): string {
@@ -104,6 +108,10 @@ export class AutoUpdaterService {
}
public async showChangelog(version:string): Promise<void>{
if (version.toLowerCase().includes("alpha")) {
throw new Error("Alpha version, skipping changelog");
}
const changelog = await this.getChangelogVersion(version);
this.modal.openModal(ChangelogModal, {data: changelog});
@@ -1,17 +1,11 @@
import { UninstallAllModsModal } from "renderer/components/modal/modal-types/uninstall-all-mods-modal.component";
import { UninstallModModal } from "renderer/components/modal/modal-types/uninstall-mod-modal.component";
import { Observable, BehaviorSubject, lastValueFrom } from "rxjs";
import { map } from "rxjs/operators";
import { Observable, BehaviorSubject, throwError, of } from "rxjs";
import { catchError, tap } from "rxjs/operators";
import { BSVersion } from "shared/bs-version.interface";
import { Mod, ModInstallProgression } from "shared/models/mods";
import { ProgressionInterface } from "shared/models/progress-bar";
import { Mod } from "shared/models/mods";
import { IpcService } from "./ipc.service";
import { ModalExitCode, ModalService } from "./modale.service";
import { NotificationType } from "../../shared/models/notification/notification.model";
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";
import { Progression } from "main/helpers/fs.helpers";
export class BsModsManagerService {
private static instance: BsModsManagerService;
@@ -20,11 +14,8 @@ export class BsModsManagerService {
private readonly ipcService: IpcService;
private readonly progressBar: ProgressBarService;
private readonly modals: ModalService;
private readonly notifications: NotificationService;
private readonly os: OsDiagnosticService;
public readonly isInstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
public readonly isUninstalling$: BehaviorSubject<boolean> = new BehaviorSubject(false);
public static getInstance(): BsModsManagerService {
@@ -37,9 +28,7 @@ export class BsModsManagerService {
private constructor() {
this.ipcService = IpcService.getInstance();
this.progressBar = ProgressBarService.getInstance();
this.modals = ModalService.getInstance();
this.notifications = NotificationService.getInstance();
this.os = OsDiagnosticService.getInstance();
}
public getAvailableMods(version: BSVersion): Observable<Mod[]> {
@@ -50,91 +39,98 @@ export class BsModsManagerService {
return this.ipcService.sendV2("get-installed-mods", version);
}
public installMods(mods: Mod[], version: BSVersion): Promise<void> {
if (this.os.isOffline) {
this.notifications.notifyError({
title: "notifications.shared.errors.titles.no-internet",
desc: "notifications.shared.errors.msg.no-internet",
});
return Promise.resolve();
}
public installMods(mods: Mod[], version: BSVersion): Observable<Progression> {
if (!this.progressBar.require()) {
return Promise.resolve();
return throwError(() => new Error("Action already in progress"));
}
const progress$: Observable<ProgressionInterface> = this.ipcService.watch<ModInstallProgression>("mod-installed").pipe(
map(res => {
return { progression: res.data.progression, label: res.data.name } as ProgressionInterface;
})
);
return new Observable<Progression>(obs => {
const install$ = this.ipcService.sendV2("install-mods", { mods, version });
this.progressBar.show(install$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
this.progressBar.show(progress$, true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
const sub = install$.pipe(
tap({
error: err => {
if(err?.code){
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.install-mods.msg.errors.${err?.code}`, duration: this.NOTIFICATION_DURATION });
} else {
this.notifications.notifyError({ title: "notifications.types.error", desc: err?.message ?? err, duration: this.NOTIFICATION_DURATION });
}
},
complete: () => {
this.notifications.notifySuccess({ title: "notifications.mods.install-mods.titles.success", desc: "notifications.mods.install-mods.msg.success", duration: this.NOTIFICATION_DURATION });
},
})
).subscribe(obs);
this.isInstalling$.next(true);
return lastValueFrom(this.ipcService.sendV2("install-mods", { mods, version })).then(res => {
const isFullyInstalled = res.nbInstalledMods === res.nbModsToInstall;
const title = `notifications.mods.install-mods.titles.${isFullyInstalled ? "success" : "warning"}`;
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: 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();
})
}
public async uninstallMod(mod: Mod, version: BSVersion): Promise<void> {
if (!this.progressBar.require()) {
return;
}
const modalRes = await this.modals.openModal(UninstallModModal, {data: mod});
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
return;
}
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
this.progressBar.show(progress$, true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
this.isUninstalling$.next(true);
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: 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();
})
return () => {
sub.unsubscribe();
this.progressBar.hide();
}
});
}
public async uninstallAllMods(version: BSVersion) {
public uninstallMod(mod: Mod, version: BSVersion): Observable<Progression> {
if (!this.progressBar.require()) {
return;
return throwError(() => new Error("Action already in progress"));
}
const modalRes = await this.modals.openModal(UninstallAllModsModal, {data: version});
return new Observable<Progression>(obs => {
const uninstall$ = this.ipcService.sendV2("uninstall-mods", { mods: [mod], version });
this.progressBar.show(uninstall$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
if (modalRes.exitCode !== ModalExitCode.COMPLETED) {
return;
const sub = uninstall$.pipe(
tap({
error: err => {
if(err?.code){
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-mod.msg.errors.${err?.code}`, duration: this.NOTIFICATION_DURATION });
} else {
this.notifications.notifyError({ title: "notifications.types.error", desc: err?.message ?? err, duration: this.NOTIFICATION_DURATION });
}
},
complete: () => {
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-mod.titles.success", duration: this.NOTIFICATION_DURATION });
},
})
).subscribe(obs);
return () => {
sub.unsubscribe();
this.progressBar.hide();
}
});
}
public uninstallAllMods(version: BSVersion): Observable<Progression> {
if (!this.progressBar.require()) {
return throwError(() => new Error("Action already in progress"));
}
const progress$ = this.ipcService.watch<ModInstallProgression>("mod-uninstalled").pipe(map(res => res.data.progression));
this.progressBar.show(progress$, true, { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
return new Observable<Progression>(obs => {
const uninstall$ = this.ipcService.sendV2("uninstall-all-mods", version);
this.progressBar.show(uninstall$.pipe(catchError(() => of({ current: 0, total: 0} as Progression))), { paddingLeft: "190px", paddingRight: "190px", bottom: "20px" });
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: 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();
})
const sub = uninstall$.pipe(
tap({
error: err => {
if(err?.code){
this.notifications.notifyError({ title: "notifications.types.error", desc: `notifications.mods.uninstall-all-mods.msg.errors.${err?.code}`, duration: this.NOTIFICATION_DURATION });
} else {
this.notifications.notifyError({ title: "notifications.types.error", desc: err?.message ?? err, duration: this.NOTIFICATION_DURATION });
}
},
complete: () => {
this.notifications.notifySuccess({ title: "notifications.mods.uninstall-all-mods.titles.success", desc: "notifications.mods.uninstall-all-mods.msg.success", duration: this.NOTIFICATION_DURATION });
},
})
).subscribe(obs);
return () => {
sub.unsubscribe();
this.progressBar.hide();
}
});
}
}
@@ -50,7 +50,7 @@ export class OculusDownloaderService extends AbstractBsDownloaderService impleme
private handleDownload(download: Observable<Progression<BSVersion>>, ingoreErrorCodes?: string[]): Observable<Progression<BSVersion>> {
const progress$ = download.pipe(map(progress => (progress.current / progress.total) * 100), catchError(() => of(0)));
this.progressBar.show(progress$, true);
this.progressBar.show(progress$);
const subs: Subscription[] = [];
@@ -106,7 +106,7 @@ export class OculusDownloaderService extends AbstractBsDownloaderService impleme
}
return bsVersion;
}).finally(() => this.progressBar.hide(true));
}).finally(() => this.progressBar.hide());
}
public downloadBsVersion(version: BSVersion): Promise<BSVersion> {
@@ -8,7 +8,6 @@ import { NotificationService } from "../notification.service";
import { ProgressBarService } from "../progress-bar.service";
import { LoginToSteamModal } from "renderer/components/modal/modal-types/bs-downgrade/login-to-steam-modal.component";
import { SteamGuardModal } from "renderer/components/modal/modal-types/bs-downgrade/steam-guard-modal.component";
import { LinkOpenerService } from "../link-opener.service";
import { DepotDownloaderErrorEvent, DepotDownloaderEvent, DepotDownloaderEventType, DepotDownloaderInfoEvent, DepotDownloaderWarningEvent } from "../../../shared/models/bs-version-download/depot-downloader.model";
import { SteamMobileApproveModal } from "renderer/components/modal/modal-types/bs-downgrade/steam-mobile-approve-modal.component";
import { DownloaderServiceInterface } from "./bs-store-downloader.interface";
@@ -30,7 +29,6 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
private readonly ipcService: IpcService;
private readonly progressBarService: ProgressBarService;
private readonly notificationService: NotificationService;
private readonly linkOpener: LinkOpenerService;
private readonly STEAM_SESSION_USERNAME_KEY = "STEAM-USERNAME";
@@ -42,7 +40,6 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
this.modalService = ModalService.getInstance();
this.progressBarService = ProgressBarService.getInstance();
this.notificationService = NotificationService.getInstance();
this.linkOpener = LinkOpenerService.getInstance();
}
private setSteamSession(username: string): void { localStorage.setItem(this.STEAM_SESSION_USERNAME_KEY, username); }
@@ -50,14 +47,6 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
public deleteSteamSession(): void { localStorage.removeItem(this.STEAM_SESSION_USERNAME_KEY); }
public sessionExist(): boolean { return !!localStorage.getItem(this.STEAM_SESSION_USERNAME_KEY); }
public async getInstallationFolder(): Promise<string> {
return lastValueFrom(this.ipcService.sendV2("bs-download.installation-folder"));
}
public setInstallationFolder(path: string): Observable<string> {
return this.ipcService.sendV2("bs-download.set-installation-folder", path);
}
// ### Downloading
private handleInfoEvents(events$: Observable<DepotDownloaderEvent>): Subscription[] {
@@ -200,7 +189,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
return Promise.resolve();
}
this.progressBarService.show(this.downloadProgress$, true);
this.progressBarService.show(this.downloadProgress$);
const downloadPromise = (async () => {
@@ -236,7 +225,7 @@ export class SteamDownloaderService extends AbstractBsDownloaderService implemen
return downloadPromise.then(() => {}).finally(() => {
this.downloadProgress$.next(0);
this.progressBarService.hide(true);
this.progressBarService.hide();
});
}
@@ -8,6 +8,7 @@ import { EditVersionModal } from "renderer/components/modal/modal-types/edit-ver
import { popElement } from "shared/helpers/array.helpers";
import { ImportVersionModal } from "renderer/components/modal/modal-types/import-version-modal.component";
import { Progression } from "main/helpers/fs.helpers";
import { CustomError } from "shared/models/exceptions/custom-error.class";
export class BSVersionManagerService {
private static instance: BSVersionManagerService;
@@ -77,10 +78,19 @@ export class BSVersionManagerService {
this.askInstalledVersions();
return res;
}).catch(e => {
this.notification.notifyError({
title: `notifications.custom-version.errors.titles.${e.error.title}`,
...(e.error.message && { desc: `notifications.custom-version.errors.msg.${e.error.message}` }),
});
const knownErrorTitlesCodes = ["CantEditSteam", "VersionAlreadExist", "CantRename"];
const knownErrorMessagesCodes = ["CantEditSteam"];
if(knownErrorTitlesCodes.includes(e.code)){
this.notification.notifyError({
title: `notifications.custom-version.errors.titles.${e.code}`,
desc: knownErrorMessagesCodes.includes(e.code) ? `notifications.custom-version.errors.msg.${e.code}` : null
});
} else {
this.notification.notifyError({ title: `notifications.custom-version.errors.titles.UnknownError` });
}
return null;
})
}
@@ -103,14 +113,19 @@ export class BSVersionManagerService {
this.notification.notifySuccess({ title: "notifications.custom-version.success.titles.CloningFinished" });
this.askInstalledVersions();
return res;
}).catch(e => {
this.notification.notifyError({
title: `notifications.custom-version.errors.titles.${e.error.title}`,
...(e.error.message && { desc: `notifications.custom-version.errors.msg.${e.error.message}` }),
});
}).catch((e: CustomError) => {
const knownErrorTitlesCodes = ["VersionAlreadExist", "CantClone"];
if(knownErrorTitlesCodes.includes(e.code)){
this.notification.notifyError({ title: `notifications.custom-version.errors.titles.${e.code}` });
} else {
this.notification.notifyError({ title: `notifications.custom-version.errors.titles.UnknownError` });
}
return null;
}).finally(() => {
this.progressBar.hide(true)
this.progressBar.hide()
});
}
@@ -152,7 +167,7 @@ export class BSVersionManagerService {
obs.error(err)
}).finally(() => {
this.askInstalledVersions();
this.progressBar.hide(true);
this.progressBar.hide();
});
return () => {
@@ -162,7 +177,7 @@ export class BSVersionManagerService {
shareReplay({ bufferSize: 1, refCount: true })
);
this.progressBar.show(obs$, true);
this.progressBar.show(obs$);
return obs$;
}
@@ -0,0 +1,35 @@
import { Observable, lastValueFrom } from "rxjs";
import { IpcService } from "./ipc.service";
export class InstallationLocationService {
private static instance: InstallationLocationService;
private readonly ipcService: IpcService;
public static getInstance(): InstallationLocationService {
if (!InstallationLocationService.instance) {
InstallationLocationService.instance = new InstallationLocationService();
}
return InstallationLocationService.instance;
}
private constructor() {
this.ipcService = IpcService.getInstance();
}
public async getInstallationFolder(): Promise<string> {
return lastValueFrom(this.ipcService.sendV2("bs-installer.install-path"));
}
/**
* @param move - if true, move the old installation path to the path param
*/
public setInstallationFolder(path: string, move: boolean): Observable<string> {
return this.ipcService.sendV2(
"bs-installer.set-install-path",
{ path, move }
);
}
}
+2 -45
View File
@@ -1,6 +1,6 @@
import { defaultIfEmpty, share } from "rxjs/operators";
import { Observable, ReplaySubject, identity } from "rxjs";
import { IpcRequest, IpcResponse } from "shared/models/ipc";
import { IpcRequest } from "shared/models/ipc";
import { deserializeError } from 'serialize-error';
import { IpcCompleteChannel, IpcErrorChannel, IpcTearDownChannel } from "shared/models/ipc/ipc-response.interface";
import { IpcChannels, IpcRequestType, IpcResponseType } from "shared/models/ipc/ipc-routes";
@@ -8,8 +8,6 @@ import { IpcChannels, IpcRequestType, IpcResponseType } from "shared/models/ipc/
export class IpcService {
private static instance: IpcService;
private readonly channelObservables: Map<string, Observable<unknown>>;
public static getInstance(): IpcService {
if (!IpcService.instance) {
IpcService.instance = new IpcService();
@@ -17,53 +15,12 @@ export class IpcService {
return IpcService.instance;
}
private constructor() {
this.channelObservables = new Map<string, Observable<IpcResponse<unknown>>>();
}
/**
* @deprecated use sendV2 instead
*/
public send<T, U = unknown>(channel: string, request?: IpcRequest<U>): Promise<IpcResponse<T>> {
if (!request) {
request = { args: null, responceChannel: null };
}
if (!request.responceChannel) {
request.responceChannel = `${channel}_responce_${new Date().getTime()}`;
}
const promise = new Promise<IpcResponse<T>>(resolve => {
window.electron.ipcRenderer.once(request.responceChannel, (response: IpcResponse<T>) => resolve(response));
});
window.electron.ipcRenderer.sendMessage(channel, request);
return promise;
}
private constructor() {}
public sendLazy<T = unknown>(channel: string, request?: IpcRequest<T>): void {
window.electron.ipcRenderer.sendMessage(channel, request);
}
// Also need a rework
public watch<T>(channel: string): Observable<T> {
if (this.channelObservables.has(channel)) {
return this.channelObservables.get(channel) as Observable<T>;
}
const obs = new Observable<T>(observer => {
window.electron.ipcRenderer.on(channel, (res) => {
observer.next(res);
});
});
this.channelObservables.set(channel, obs);
return obs;
}
// TODO : Convert all IPCs calls to V2
public sendV2<C extends IpcChannels>(channel: C, data?: IpcRequestType<C>, defaultValue?: IpcResponseType<C>): Observable<IpcResponseType<C>> {
const request: IpcRequest<IpcRequestType<C>> = {
@@ -1,5 +1,5 @@
import { DownloadMapsModal } from "renderer/components/modal/modal-types/download-maps-modal.component";
import { map, filter } from "rxjs/operators";
import { map, filter, share } from "rxjs/operators";
import { BehaviorSubject, timer, Observable, lastValueFrom } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
import { ModalResponse, ModalService } from "./modale.service";
@@ -30,9 +30,10 @@ export class MapsDownloaderService {
private readonly mapsQueue$: BehaviorSubject<MapDownload[]> = new BehaviorSubject([]);
private readonly currentDownload$: BehaviorSubject<MapDownload> = new BehaviorSubject(null);
private queueMaxLenght = 0;
private downloadedListerners: ((map: BsmLocalMap, version: BSVersion) => void)[] = [];
public readonly progressBarStyle: CSSProperties = { zIndex: 100000, position: "fixed", bottom: "10px", right: 0 };
private _lastDownloadedMap$: Observable<{map: BsmLocalMap, version?: BSVersion}>;
private constructor() {
this.modals = ModalService.getInstance();
this.progressBar = ProgressBarService.getInstance();
@@ -43,14 +44,10 @@ export class MapsDownloaderService {
this.mapsQueue$.pipe(filter(queue => queue.length === 0)).subscribe(() => {
this.queueMaxLenght = 0;
});
this.ipc.watch<{map: BsmLocalMap, version?: BSVersion}>("map-downloaded").subscribe((data) => {
this.downloadedListerners.forEach(func => func(data.map, data.version));
});
}
private async startDownloadMaps() {
this.progressBar.show(this.downloadProgress$, true, this.progressBarStyle);
this.progressBar.show(this.downloadProgress$, this.progressBarStyle);
await timer(2000).toPromise();
@@ -67,7 +64,7 @@ export class MapsDownloaderService {
await timer(500).toPromise();
this.currentDownload$.next(null);
this.progressBar.hide(true);
this.progressBar.hide();
}
private downloadMap(map: BsvMapDetail, version: BSVersion): Observable<BsmLocalMap> {
@@ -119,16 +116,19 @@ export class MapsDownloaderService {
return this.mapsQueue$.asObservable();
}
public addOnMapDownloadedListener(func: (map: BsmLocalMap, version: BSVersion) => void) {
this.downloadedListerners.push(func);
}
public get lastDownloadedMap$(): Observable<{map: BsmLocalMap, version?: BSVersion}> {
if(!this._lastDownloadedMap$){
this._lastDownloadedMap$ = new Observable<{map: BsmLocalMap, version?: BSVersion}>(observer => {
const sub = this.ipc.sendV2("last-downloaded-map").subscribe(observer);
public removeOnMapDownloadedListener(func: (map: BsmLocalMap, version: BSVersion) => void) {
const funcIndex = this.downloadedListerners.indexOf(func);
if (funcIndex < 0) {
return;
return () => {
this._lastDownloadedMap$ = null;
sub.unsubscribe();
}
}).pipe(share());
}
this.downloadedListerners.splice(funcIndex, 1);
return this._lastDownloadedMap$;
}
public async oneClickInstallMap(map: BsvMapDetail): Promise<void> {
@@ -145,3 +145,5 @@ export interface MapDownload {
map: BsvMapDetail;
version: BSVersion;
}
export type MapsDownloadedListener = (map: BsmLocalMap, version: BSVersion) => void;
+55 -5
View File
@@ -1,6 +1,6 @@
import { LinkMapsModal } from "renderer/components/modal/modal-types/link-maps-modal.component";
import { UnlinkMapsModal } from "renderer/components/modal/modal-types/unlink-maps-modal.component";
import { Subject, Observable, of, lastValueFrom } from "rxjs";
import { Subject, Observable, of, lastValueFrom, Subscription, throwError } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
import { BsmLocalMapsProgress, BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { IpcService } from "./ipc.service";
@@ -13,6 +13,8 @@ import { map, last, catchError } from "rxjs/operators";
import { ProgressionInterface } from "shared/models/progress-bar";
import { FolderLinkState, VersionFolderLinkerService } from "./version-folder-linker.service";
import { SongDetails } from "shared/models/maps";
import { Progression } from "main/helpers/fs.helpers";
import { DeleteDuplicateMapsModal } from "renderer/components/modal/modal-types/delete-duplicate-maps-modal.component";
export class MapsManagerService {
private static instance: MapsManagerService;
@@ -99,10 +101,10 @@ export class MapsManagerService {
const progress$ = this.ipcService.sendV2("delete-maps", maps ).pipe(map(progress => (progress.deleted / progress.total) * 100));
if (showProgressBar) {
this.progressBar.show(progress$, true);
this.progressBar.show(progress$);
}
progress$.toPromise().finally(() => this.progressBar.hide(true));
progress$.toPromise().finally(() => this.progressBar.hide());
return progress$
.pipe(
@@ -131,7 +133,7 @@ export class MapsManagerService {
})
);
this.progressBar.show(exportProgress$, true);
this.progressBar.show(exportProgress$);
await exportProgress$
.toPromise()
@@ -142,10 +144,58 @@ export class MapsManagerService {
// TODO TRANSLATE
this.notifications.notifySuccess({ title: "Export terminé 🎉", duration: 3000 });
this.progressBar.complete();
this.progressBar.hide(true);
this.progressBar.hide();
});
}
public deleteDuplicateMaps(maps: BsmLocalMap[]): Observable<Progression> {
const hashMap = Object.groupBy(maps, m => m.hash);
const mapsToDelete: BsmLocalMap[] = [];
for (const maps of Object.values(hashMap)) {
if(!Array.isArray(maps)){ continue; }
maps.sort((a, b) => b.path.length - a.path.length).pop(); // The longest paths are most likely to be the duplicate ones (path: '../Awesome maps (copy)')
mapsToDelete.push(...maps);
}
if(mapsToDelete.length === 0){
this.notifications.notifySuccess({ title: "notifications.maps.no-duplicates-maps.title", desc: "notifications.maps.no-duplicates-maps.msg", duration: 4000 });
return of({ current: 0, total: 0 });
}
if(!this.progressBar.require()){
return throwError(() => new Error("Progress bar is required to delete duplicate maps"));
}
return new Observable<Progression>(obs => {
let sub: Subscription;
(async () => {
const modalRes = await this.modal.openModal(DeleteDuplicateMapsModal, { data: { maps: mapsToDelete } });
if(modalRes.exitCode !== ModalExitCode.COMPLETED){
throw new Error("Operation to delete duplicate maps as been canceled by the user");
}
const progress$: Observable<Progression> = this.ipcService.sendV2("delete-maps", mapsToDelete).pipe(map(p => ({ total: p.total, current: p.deleted })));
this.progressBar.show(progress$);
sub = progress$.subscribe(obs);
await lastValueFrom(progress$);
})()
.then(() => this.notifications.notifySuccess({ title: "notifications.maps.duplicates-maps-deleted.title", desc: "notifications.maps.duplicates-maps-deleted.msg", duration: 4000 }))
.catch(e => obs.error(e))
.finally(() => obs.complete());
return () => {
if(sub){
this.progressBar.hide();
sub.unsubscribe();
}
}
})
}
public getMapsInfoFromHashs(hashs: string[]): Observable<SongDetails[]> {
return this.ipcService.sendV2("get-maps-info-from-cache", hashs);
}
+1 -1
View File
@@ -39,7 +39,7 @@ export class ModalService {
}
}
export type ModalOptions<T = unknown> = { readonly data?: T, readonly noStyle?: boolean }
export type ModalOptions<T = unknown> = { readonly data?: T, readonly noStyle?: boolean, readonly closable?: boolean }
export type ModalComponent<Return = unknown, Receive = unknown> = ({ resolver, options }: { readonly resolver: (x: ModalResponse<Return>) => void; readonly options?: ModalOptions<Receive> }) => JSX.Element;
export type ModalObject = {modal: ModalComponent, resolver: (value: ModalResponse | PromiseLike<ModalResponse>) => void, options: ModalOptions};
@@ -53,13 +53,13 @@ export class ModelsDownloaderService {
}),
startWith({ progression: 0.1, label: download.model.name })
);
this.progress.show(progress$, true);
this.progress.show(progress$);
}
const downloaded = await lastValueFrom(download$);
this.lastDownload$.next(downloaded.data);
this.progress.hide(true);
this.progress.hide();
this.queue$.next(this.queue$.value.filter(m => m.model.hash !== download.model.hash));
}
@@ -127,7 +127,7 @@ export class ModelsDownloaderService {
this.progress.complete();
await lastValueFrom(timer(500));
this.progress.hide(true);
this.progress.hide();
return res;
}
@@ -123,7 +123,7 @@ export class ModelsManagerService {
})
);
this.progressBar.show(exportProgress$, true);
this.progressBar.show(exportProgress$);
lastValueFrom(exportProgress$)
.then(() => {
@@ -133,7 +133,7 @@ export class ModelsManagerService {
this.notifications.notifyError({ title: "notifications.types.error", desc: "notifications.common.msg.error-occurred", duration: 3000 });
})
.finally(() => {
this.progressBar.hide(true);
this.progressBar.hide();
});
}
@@ -182,7 +182,7 @@ export class ModelsManagerService {
this.notifications.notifyError({ title: "notifications.types.error", desc: "notifications.common.msg.error-occurred", duration: 3000 });
return [];
})
.finally(() => this.progressBar.hide(true));
.finally(() => this.progressBar.hide());
}
public isDeepLinksEnabled(): Promise<boolean> {

Some files were not shown because too many files have changed in this diff Show More