Compare commits

...

252 Commits

Author SHA1 Message Date
Zagrios 9bbc57008a bumb to 1.5.3 2025-04-27 16:33:01 +02:00
Zagrios 34dbfe4cf8 [bugfix] Launch mods weren't working 2025-04-27 14:15:54 +02:00
Zagrios bd5d29f5db bumb to 1.5.3-alpha.1 2025-04-25 14:44:32 +02:00
MathieuG-P faf6c0f121 [bugfix] Fix uninstall mod error (#862) 2025-04-25 14:42:45 +02:00
silentrald 79be1a842c [feat] added a naive way to parse mod metadata (#855) 2025-04-25 13:59:09 +02:00
MathieuG-P 266feae483 [feature] we can now create custom launch options (#861) 2025-04-24 15:43:42 +02:00
silentrald cc4061fd38 [bugfix] fix mod loading by removing side effects (#860)
* added more loading when loading mods
2025-04-24 15:09:53 +02:00
Zagrios 2714a125f2 Merge branch 'master' of https://github.com/Zagrios/bs-manager 2025-04-22 11:01:23 +02:00
Zagrios 2d1dc21942 Add patreon 2025-04-22 11:01:18 +02:00
silentrald e842cfa7eb [bugfix] allow env overriding with launch options (#854) 2025-04-19 19:28:43 +02:00
Zagrios b2357cd57f 1.40.4 is now the recommended version 2025-04-15 10:44:51 +02:00
MathieuG-P 7ffeb5e9a6 [chore] update GlowEffect visibility logic in LaunchSlide component (#852)
[bugfix] update GlowEffect visibility logic in LaunchSlide component
2025-04-12 10:11:06 +02:00
MathieuG-P b2f855bec0 [chore] add logs in oculus downloader (#851) 2025-04-12 09:57:41 +02:00
silentrald 3a4ddc13c1 [bugfix] add system env to beat saber exe launch command for oculus (#848) 2025-04-11 06:48:23 +02:00
MathieuG-P f8a62c3e4e Merge pull request #844 from Zagrios/bugfix/remove-os-diagnostic-service
[bugfix] remove OsDiagnosticService
2025-04-07 07:36:12 +02:00
Zagrios f380522eac [bugfix] remove OsDiagnosticService 2025-04-07 07:32:16 +02:00
Zagrios f81aa82b30 Update changelog.md 2025-04-07 07:15:21 +02:00
Zagrios edc4f1cbf4 Fix lint errors 2025-04-07 07:03:53 +02:00
MathieuG-P c058b29175 Merge branch 'master' into master 2025-04-07 07:02:26 +02:00
MathieuG-P 63205b9eaf Merge pull request #842 from Zagrios/bugfix/browsing-for-maps-in-fullscreen-was-broken
[bugfix] In fullscreen, only the first page of downloadable maps coul…
2025-04-06 15:05:44 +02:00
Zagrios 64abf3ae82 [bugfix] In fullscreen, only the first page of downloadable maps could be loaded 2025-04-06 14:58:19 +02:00
MathieuG-P 135bff8192 Merge branch 'master' into master 2025-04-06 14:32:03 +02:00
Zagrios e41737decf Update deps 2025-04-06 14:31:15 +02:00
MathieuG-P d552d0a011 Merge pull request #839 from Zagrios/bugfix/remove-ipv6-ipv4-stuff
[bugfix] Remove the ipv4/ipv6 stuff that cause more issue than is solves
2025-04-03 15:42:15 +02:00
Zagrios cb32765c50 Merge branch 'master' of https://github.com/Zagrios/bs-manager 2025-04-03 15:37:45 +02:00
Zagrios baa1ec60a1 Include download url of the map in the log 2025-04-03 15:37:39 +02:00
Zagrios 188aa68b86 [bugfix] use URL to join url parts 2025-04-03 15:37:17 +02:00
Zagrios c763e0fefc [bugfix] Remove the ipv4/ipv6 stuff that cause more issue than is solves 2025-04-03 15:28:24 +02:00
Iluhadesu 5ff28cac90 Add v1.40.4 to bs-versions.json 2025-04-01 14:17:50 +00:00
MathieuG-P 4bd471f51c Merge pull request #805 from silentrald/bugfix/map-extraction
[bugfix] fix issue with extracting root map files
2025-04-01 13:52:43 +02:00
silentrald aba9a7de6d [bugfix] fix issue with extracting root map files 2025-04-01 10:22:05 +08:00
Arimodu ba0b40e0ac Fix SteamVR opening in FPFC
- Wait for the game to either close (keep it refed) or the user closes the app (unref and restore)
2025-04-01 03:27:40 +02:00
MathieuG-P 273d9532a2 Merge pull request #836 from silentrald/bugfix/nixos-wine
[bugfix] use system wine for nixos
2025-03-30 12:03:31 +02:00
silentrald 6b07fdebe0 [bugfix] use system wine for nixos 2025-03-30 16:38:36 +08:00
MathieuG-P 02ef273722 Merge pull request #834 from Zagrios/bugfix/bug-instance-uninstall-fails-silently/821
[bugfix-821] Add error messages for Beat Saber uninstallation
2025-03-22 20:33:14 +01:00
Zagrios 4435df2fd7 [bugfix-821] Add error messages for Beat Saber uninstallation 2025-03-22 20:29:20 +01:00
Zagrios 011e9d1b04 Update framer-motion dependency from version 11.2.6 to 12.5.0 2025-03-14 14:58:20 +01:00
Zagrios 0a067303a2 Update rxjs dependency from version 7.8.1 to 7.8.2 2025-03-14 14:49:18 +01:00
Zagrios 6ab5b4f1a1 Update Node.js version in package.json from 22.11.0 to 22.14.0 2025-03-14 14:44:16 +01:00
Zagrios 3c818598bf Update package dependencies to latest versions, including color, dot-prop, fs-extra, got, node-abi, semver, and tailwind-merge. 2025-03-14 14:42:50 +01:00
Zagrios ab68d5a0e0 Update electron and electronmon dependencies to latest versions 2025-03-14 14:16:38 +01:00
Zagrios 93e12af0c4 Add link to claim Oculus Desktop version of Beat Saber in sidebar 2025-03-14 13:31:56 +01:00
Zagrios caf79898cd Add patreon 2025-03-09 17:27:58 +01:00
MathieuG-P c4a703c214 Merge pull request #828 from Zagrios/bugfix/fix-beatleader-out-of-control
Fix BeatLeader out of control (UI)
2025-03-09 16:45:02 +01:00
Zagrios 409267b105 Fix BeatLeader out of control 2025-03-09 16:40:07 +01:00
MathieuG-P 8f1a7f86ac Merge pull request #817 from Mistyttm/patch-1
Add NixOS/Nix installation section in wiki
2025-03-09 11:10:10 +01:00
MathieuG-P 6ddf62c81e Merge pull request #827 from Zagrios/823-bug-popup-modal-for-linkingunlinking-blocks-shows-miscbloqs-missing-translation
[bugfix-823] fix blocks translations
2025-03-09 11:08:30 +01:00
Zagrios f69f937197 [bugfix-823] fix blocks translations 2025-03-09 11:05:12 +01:00
Iluhadesu c00051e8a9 Add v1.40.3 to bs-versions.json 2025-02-27 16:20:37 +00:00
MathieuG-P 7c1b71cd0b Merge pull request #818 from Zagrios/chore/add-beatsaber-desktop-version-info
[Chore] add warnings: Users must own desktop version of Beat Saber to use oculus download
2025-02-25 17:04:59 +01:00
Zagrios 7a86016e8e Translations 2025-02-25 15:48:14 +01:00
Zagrios abfd0cf4dd Add how to claim desktop version of beat saber guide 2025-02-25 15:03:47 +01:00
Zagrios ea7527d952 Merge branch 'master' into chore/add-beatsaber-desktop-version-info 2025-02-25 14:42:24 +01:00
Zagrios 3f10d5917f Add patreons 2025-02-24 14:16:05 +01:00
Emmey aa8ba4ffc2 Add NixOS/Nix installation section 2025-02-24 22:27:06 +10:00
MathieuG-P 5dd80efa8c Merge pull request #791 from TheUnifox/feature/fileSizes+OversizeWarning
[feat] Add file sizes+oversize warning
2025-02-23 16:49:17 +01:00
Zagrios 2637726f5a add missing translations 2025-02-23 16:42:16 +01:00
Zagrios e7b3ca4fdf Handle zero file size in FileSizeText component 2025-02-22 19:51:21 +01:00
Unifox a74be4ba9d move readonly 2025-02-21 09:24:38 -07:00
Unifox 4615a4a45a Merge branch 'master' into feature/fileSizes+OversizeWarning 2025-02-21 09:21:17 -07:00
Unifox 092f2af065 One last bot issue 2025-02-21 09:21:07 -07:00
Unifox 2c9e02fd2d Fix bot issues
basically just pulling nested ternarys out into functions
2025-02-21 09:16:15 -07:00
Zagrios 14f079b50c Add information to inform the users that they must have the desktop version of Beat Saber 2025-02-21 16:55:11 +01:00
MathieuG-P fa2f69f800 Merge pull request #810 from silentrald/chore/lint-error-fixes
[chore] fixed some lint issues
2025-02-21 16:39:23 +01:00
Unifox c0aea5bdc3 Add Filipino and Ukrainian translations 2025-02-21 08:33:28 -07:00
silentrald b525cac250 [chore] removed log decorators 2025-02-21 23:32:11 +08:00
Unifox 941cd96801 Merge branch 'master' into feature/fileSizes+OversizeWarning 2025-02-21 04:33:33 -07:00
MathieuG-P f800118d10 Merge pull request #807 from LiamillionSS/bugfix/806
bugfix/806 fix linux UUID folders not deleting
2025-02-20 18:28:47 +01:00
LiamillionSS e8c7315bc3 Merge remote-tracking branch 'origin/bugfix/806' into bugfix/806 2025-02-20 18:25:36 +01:00
LiamillionSS df8c7f2376 bugfix/806 fix linux UUID folders not deleting, non async 2025-02-20 19:37:59 +11:00
MathieuG-P 0594b7eca0 Merge pull request #804 from Scrumplex/chore/update-lockfile-integrity
[chore] Update lockfile integrity hashes
2025-02-19 19:31:59 +01:00
MathieuG-P 5f46faf6ee Merge pull request #809 from silentrald/bugfix/oculus-path-linux
[bugfix-801] fixed issue when downloading oculus version in linux
2025-02-19 19:15:29 +01:00
Zagrios a7d00f22c5 Convert throwed object to CustomError 2025-02-19 19:07:45 +01:00
MathieuG-P 4f18ba3a42 Merge pull request #811 from silentrald/chore/readable-log-timestamp
[chore] updated log names to readable timestamp
2025-02-19 18:54:46 +01:00
MathieuG-P 770a797f6f Merge pull request #812 from silentrald/bugfix/delete-linked-folder
[bugfix] use deleteFolder instead of deleteFile for linked folder paths
2025-02-19 18:51:26 +01:00
silentrald 8c21a0f3f2 [bugfix] use deleteFolder instead of deleteFile for linked folder paths 2025-02-19 21:01:55 +08:00
silentrald c4a56d7036 [chore] updated log names to readable timestamp 2025-02-19 09:18:22 +08:00
silentrald 701b3bf141 [chore] fixed some lint issues 2025-02-16 10:39:44 +08:00
silentrald 60af42fe1d [bugfix-801] fixed issue when downloading oculus version in linux 2025-02-16 10:28:13 +08:00
LiamillionSS b38b1e87d3 bugfix/806 fix linux UUID folders not deleting 2025-02-15 00:26:15 +11:00
Sefa Eyeoglu 2c38965d86 [chore] Update lockfile integrity hashes
The lockfile was missing some integrity hashes. The tool
npm-package-lock-add-resolved can help here.

See https://github.com/npm/cli/issues/4460
2025-02-14 11:25:07 +01:00
MathieuG-P 8b9b433f64 Merge pull request #795 from silentrald/bugfix/beatmods-connection
[bugfix] show error when BSManager can't connect to beatmods
2025-02-13 16:26:18 +01:00
Zagrios 307bd9f205 Add tl and uk translations 2025-02-13 16:20:16 +01:00
Zagrios ed3a09e4a8 Merge branch 'master' into bugfix/beatmods-connection 2025-02-13 16:14:33 +01:00
MathieuG-P d01dd2f34a Merge pull request #796 from Zagrios/feature/move-github-button
move GitHub button in settings page
2025-02-13 15:54:05 +01:00
Unifox 21762a9d94 Merge branch 'master' into feature/fileSizes+OversizeWarning 2025-02-12 13:17:38 -07:00
MathieuG-P b0a5e806e3 Merge pull request #793 from silentrald/bugfix/linux-wineprefix-fallback
[bugfix] Don't fallback to system WINEPREFIX
2025-02-10 23:06:04 +01:00
Zagrios 7b0bcf5041 Rename getAcceptDisclaimerKey to ensureDisclaimerAccepted for clarity 2025-02-10 23:00:46 +01:00
Zagrios c3c3be0823 Add Philipino and Ukrainian translations 2025-02-10 22:53:06 +01:00
MathieuG-P 2af84326c1 Merge branch 'master' into bugfix/linux-wineprefix-fallback 2025-02-10 22:40:38 +01:00
Zagrios 41628d4e6e Backend should be as frontend-agnostic as possible 2025-02-10 22:37:32 +01:00
MathieuG-P 51b4329cfd Merge pull request #799 from Zagrios/bugfix/translation-error/798
add missing translation in chinese (simplified)
2025-02-09 15:27:05 +01:00
MathieuG-P c2b288089b Merge pull request #784 from silentrald/translation/tl
[translation] added filipino translation
2025-02-09 15:24:46 +01:00
Shidorien b831560b52 add missing translation in chinese (simplified) 2025-02-08 19:34:34 +01:00
Shidorien 3e61a14ca3 reorder GitHub button position and update its color in settings page 2025-02-08 18:32:17 +01:00
Shidorien 6be5c776fa move GitHub button in settings page 2025-02-08 18:23:46 +01:00
Zagrios 0130334a74 Update PhilippineIcon 2025-02-09 00:50:44 +08:00
silentrald 2536b0c4d3 translation: created tl.json 2025-02-09 00:50:32 +08:00
MathieuG-P 3793edfea1 Merge pull request #767 from HighError/add-ua-locale
[Translate] Add Ukrainian translation
2025-02-08 16:48:22 +01:00
MathieuG-P 750c372a26 Merge branch 'master' into add-ua-locale 2025-02-08 16:39:10 +01:00
Zagrios 7600ead380 fix map translations + update ukraine flag 2025-02-08 16:38:41 +01:00
MathieuG-P a98c5a14a3 Merge pull request #792 from Zagrios/feature/update-docs
Update CONTRIBUTING.md
2025-02-08 16:18:18 +01:00
MathieuG-P d839204492 Merge pull request #789 from silentrald/bugfix/null-folder-version
[bugfix] fixed issue with null folderVersion
2025-02-08 16:15:23 +01:00
Iluhadesu 913ef5c281 Update bs-versions.json according to the late announcement 2025-02-08 14:26:40 +00:00
silentrald 933b642985 [bugfix] added hyperlinks for can't connect to beatmods 2025-02-08 21:47:09 +08:00
silentrald b97484ace0 [bugfix] added translation for can't connect to beatmods 2025-02-08 20:54:52 +08:00
silentrald 7fafbb316e [bugfix] show error when BSManager can't connect to beatmods 2025-02-08 20:44:09 +08:00
silentrald a54809ffb5 [bugfix] changed error values 2025-02-08 20:23:18 +08:00
Unifox 3a2c92614c Move file-size-text.tsx into mod-item
Since FileSizeText will only ever be used on ModItems
2025-02-07 23:14:50 -07:00
Unifox 187174aa5e Use Tippy for tooltip 2025-02-07 22:40:15 -07:00
Unifox 37538f60b5 A bit more refactoring 2025-02-07 21:25:29 -07:00
silentrald 35772768af [bugfix] added translation for no wineprefix errors 2025-02-08 11:29:41 +08:00
silentrald 6fc3fed6dd [bugfix] block mods grid if wineprefix is not yet setup 2025-02-08 11:29:41 +08:00
silentrald ff3d82238f [bugfix] don't fallback on wineprefix and show error on mod install 2025-02-08 11:29:17 +08:00
Unifox 28407c5cdd Merge branch 'master' into feature/fileSizes+OversizeWarning 2025-02-07 20:25:06 -07:00
Unifox af6e2053e1 Refactor + move css to tailwind
Refactored the code for checking and showing mod size.
Also redid manual css in tailwind css
2025-02-07 20:24:44 -07:00
Iluhadesu 04f1b00a0b Add 1.40.2 to bs-version.json 2025-02-07 22:17:15 +00:00
GaetanGrd 1f5904d135 Update CONTRIBUTING.md 2025-02-07 17:33:22 +01:00
GaetanGrd 3f8b3a0594 Update CONTRIBUTING.md 2025-02-07 17:22:58 +01:00
GaetanGrd 3e1c595cd7 Update CONTRIBUTING.md 2025-02-07 17:16:31 +01:00
GaetanGrd c83b630e1a Update CONTRIBUTING.md 2025-02-07 17:15:55 +01:00
Unifox 07f2455cce Add translations for Size
Should probably add the other translations for size as well. I've checked that it's the right translation when talking about file size specifically
2025-02-06 18:19:49 -07:00
Unifox 1e7dde613e Revert testing changes
Forgor to set these back before committing... oops
2025-02-06 17:42:49 -07:00
Unifox 258931dddd Revert "Add build_start script"
This reverts commit 28c71be051.
2025-02-06 17:38:04 -07:00
Unifox 28c71be051 Add build_start script
It's just easier to test when rebuilding is needed to have one command for build and start. It literally just calls build and start one after the other lol
2025-02-06 01:48:23 -07:00
Unifox 9225f2cd77 Show mod file size in mods list
This implements the changes made in PR #48 of the beatmods backend to add file sizes to mods.
The change also allows for warning if a mod is oversized. Most mods shouldn't have a warning, but should a mod go over a limit, it'll warn the user.

THIS WILL REQUIRE FURTHER TESTING ONCE THE BACKEND UPDATES
2025-02-06 01:46:52 -07:00
silentrald 7aae6ae6bc [bugfix] fixed issue with null folderVersion 2025-02-06 12:49:18 +08:00
HighError 55ff418f4a renamed icon name 2025-02-05 17:26:34 +02:00
HighError 00a87d06b2 Merge branch 'master' into add-ua-locale 2025-02-05 17:19:13 +02:00
MathieuG-P c10c3713c5 Update build.yaml 2025-02-04 00:47:43 +01:00
MathieuG-P 0f4738b262 Update release-linux.yaml 2025-02-03 20:25:54 +01:00
MathieuG-P 31a8438c8f Update release-linux.yaml 2025-02-03 20:07:31 +01:00
Zagrios a076ac9db4 Merge branch 'master' of https://github.com/Zagrios/bs-manager 2025-02-03 19:47:26 +01:00
Zagrios 911e3ab50e bumb to 1.5.2 2025-02-03 19:47:25 +01:00
MathieuG-P d8f4a7f883 Merge pull request #786 from Zagrios/hotfix/unable-to-load-mods-from-beatmods
[hotfix] unable to load mods from beatmods
2025-02-03 19:36:09 +01:00
Zagrios 59432da5bf Add base headers to requests in RequestService 2025-02-03 19:31:24 +01:00
Zagrios 4606b368a0 remove name for certificate verification 2025-02-03 14:12:34 +01:00
HighError 7ba873fb67 Merge branch 'master' into add-ua-locale 2025-01-31 19:55:45 +02:00
Zagrios 80167a7f3f bumb to v1.5.1 2025-01-31 18:03:52 +01:00
MathieuG-P 91cb80bbe2 Merge pull request #774 from Zagrios/bugfix/bug-duplicate-maps-deletion-modal-does-not-display-translated-text/771
[bugfix-771] Add missing translations
2025-01-31 18:00:52 +01:00
Zagrios df1c7ca9d2 [bugfix-771] Add missing translations 2025-01-31 17:56:32 +01:00
MathieuG-P 0f96db8d29 Merge pull request #772 from silentrald/feat/delete-guard-rails
[feat] added logs when deleting files/folders
2025-01-31 16:24:46 +01:00
MathieuG-P 8042f1b9f6 Merge pull request #769 from silentrald/bugfix/playlist-keep-content
[bugfix] fix issue where playlist gets deleted when linked
2025-01-31 16:00:30 +01:00
Zagrios 757eb1f655 changed undefined to void 2025-01-31 15:53:16 +01:00
silentrald 49fb694aaf [feat] added logs when deleting files/folders 2025-01-31 21:53:39 +08:00
silentrald 508e290a24 [bugfix] fix issue where playlist gets deleted when linked 2025-01-31 08:47:56 +08:00
HighError 5e0b044b36 Merge branch 'master' into add-ua-locale 2025-01-30 21:15:37 +02:00
MathieuG-P 413996f9f3 Merge pull request #760 from Zagrios/chore/update-changelog
[Chore] update changelog
2025-01-30 19:46:58 +01:00
MathieuG-P cda4e000ee Merge pull request #759 from GaetanGrd/master
[Chore]Update Wiki for 1.5
2025-01-30 19:46:23 +01:00
HighError 2262f1d2ce renamed list to playlist 2025-01-30 16:47:50 +02:00
HighError a2a0c4e00e fixed some mistypes 2025-01-30 16:25:28 +02:00
HighError abacf5f628 Fixed rounded flag 2025-01-30 16:15:14 +02:00
HighError dd70df19d0 Added all strings 2025-01-30 15:58:03 +02:00
HighError f4fb6cfafd Added some strings and icon 2025-01-30 15:14:05 +02:00
Zagrios 2e9f62d415 bumb to v1.5.0 2025-01-29 22:51:21 +01:00
Zagrios 4ef821adc9 Fix angry TS 2025-01-29 22:40:59 +01:00
Zagrios 21f4eacefb [hotfix] Fix no mods available if an error occurs while resolving installed mods 2025-01-29 22:37:04 +01:00
Zagrios 72738a256b [chore] this error is already logged by request.service 2025-01-29 22:21:34 +01:00
Zagrios a54c146772 [hotfix] show skip steam only if the lauch option is enabled 2025-01-29 22:15:27 +01:00
Zagrios 43a4cdfe45 [hotfix] Fix wrong translation keys in brazilian translation 2025-01-29 21:58:37 +01:00
Zagrios 39d481467a [hotfix] Fix false error when deleting a playlist with no maps installed 2025-01-29 21:43:59 +01:00
Zagrios 3afcf1c12e Merge branch 'master' of https://github.com/Zagrios/bs-manager 2025-01-29 21:34:09 +01:00
Zagrios da140848ba [hotfix] wrong translation keys in brazilian translation 2025-01-29 21:34:08 +01:00
MathieuG-P 288853a6cc Merge pull request #763 from silentrald/hotfix/inject-env-vars
[hotfix] properly inject the env variable to the launch command
2025-01-29 12:14:57 +01:00
MathieuG-P 365e251408 Merge pull request #761 from silentrald/bugfix/uninstall-null-mod
[hotfix] fixed handling for filtering out mods to uninstall
2025-01-29 12:12:52 +01:00
Iluhadesu eaa5eccb98 Add v1.40.1 to bs-versions.json 2025-01-29 11:07:03 +00:00
silentrald eabeae00d6 [hotfix] properly inject the env variable to the launch command 2025-01-29 18:00:52 +08:00
silentrald d209d6c336 [hotfix] fixed handling for filtering out mods to uninstall 2025-01-29 09:31:55 +08:00
Zagrios eff5d5728f mods in pending folders must be loaded first 2025-01-28 22:44:04 +01:00
GaetanGrd 2ee7a904d6 Merge branch 'master' into master 2025-01-28 19:52:54 +01:00
GaetanGrd f477870f74 Update README.md 2025-01-28 19:51:28 +01:00
Shidorien 2ccb3d469f Update readme for 1.5 (no image update) 2025-01-28 19:47:01 +01:00
Zagrios 37ad1d2d19 bumb to v1.5.0-alpha.10 2025-01-28 18:39:53 +01:00
Zagrios 88f3467882 Do not use multiple hashes yet, its broken 2025-01-28 18:38:45 +01:00
GaetanGrd c63b39ff01 Merge pull request #5 from Zagrios/master
update master
2025-01-28 18:17:52 +01:00
Shidorien 4bb01146e6 fix: correct spelling and improve headings in installation guides 2025-01-28 18:14:32 +01:00
Shidorien 35a476f6ab fix: update section title from "Import Maps" to "Import Mods" in installation guide 2025-01-28 18:06:10 +01:00
Shidorien 1744ae1dcc add instructions for importing mods with BSManager 2025-01-28 18:05:46 +01:00
Shidorien 7a38bb22f1 fix: remove manual installation section from the model modification guide 2025-01-28 18:01:06 +01:00
Shidorien b325bf2a39 fix: update headings and add manual installation section in model modification guide 2025-01-28 18:00:57 +01:00
MathieuG-P 43ddfc652d Merge pull request #758 from Zagrios/bugfix/min-nps-playlist-is-always-zero
[bugfix] Min nps of playlists was always zero
2025-01-28 17:41:01 +01:00
Shidorien 52d1049c98 update shortcut creation instructions for clarity and add visual 2025-01-28 17:39:47 +01:00
Zagrios 4c4b783aa7 [bugfix] Min nps of playlists was always zero 2025-01-28 16:08:17 +01:00
Zagrios 2c1c94640f bump version to 1.5.0-alpha.9 and update license to GPL-3.0-only 2025-01-28 15:18:44 +01:00
Zagrios 8476f869b4 Bumb to v1.5.0-alpha.9 2025-01-28 15:16:44 +01:00
Zagrios ad87874e09 update changelog 2025-01-28 15:10:07 +01:00
Zagrios fd299e1267 fix z index 2025-01-28 14:54:42 +01:00
MathieuG-P 374bfe6834 Merge pull request #751 from arthurmluz/feature/pt-br-language
[TRANSLATION] brazilian portuguese language
2025-01-28 14:39:01 +01:00
Zagrios 47158a264a Add missing translations 2025-01-28 14:30:14 +01:00
Zagrios 812600a3ec Fix brazil flag icon 2025-01-28 14:23:21 +01:00
MathieuG-P 8efba693b3 Merge branch 'master' into feature/pt-br-language 2025-01-28 14:10:56 +01:00
MathieuG-P 39afc09df9 Merge pull request #749 from silentrald/bugfix/linux-env-parser
[feat] inject env vars to launch options with steam format %command%
2025-01-28 14:10:04 +01:00
silentrald b94fe0a053 [feat] follow launch command string even if its hidden 2025-01-28 20:58:24 +08:00
silentrald 1c2321b438 [feat] support %command% env args to windows and oculus
* refactored additionalArgs to command and change its type to string instead of string array
2025-01-28 13:41:16 +08:00
MathieuG-P e6d96088fb Merge pull request #756 from Zagrios/bugfix/add-file-size-check-when-moving-folder-content
[chore] Enhance folder linking reliability by ensuring file size consistency during content movement
2025-01-27 16:03:48 +01:00
Zagrios 5466ad61c3 Ensure file size consistency during folder content movement 2025-01-26 15:57:45 +01:00
MathieuG-P 0a2e2a2f47 Merge pull request #755 from Zagrios/feature/outdated-mods-notification
[feature] Outdated mods notification
2025-01-26 15:21:21 +01:00
MathieuG-P 950ca71a0a [feature] Outdated mod notification 2025-01-26 04:04:07 +01:00
silentrald 889e792b3c [feat] added translations for env parser error 2025-01-24 20:37:46 +08:00
silentrald c6ff848bfa [feat] inject env vars to launch options with steam format %command% 2025-01-24 20:27:36 +08:00
silentrald 5d4b79c041 [feat] added a way to parse env strings 2025-01-24 20:27:36 +08:00
MathieuG-P 28be0da96e Merge pull request #747 from silentrald/build/rpm
[chore] added rpm build for linux
2025-01-23 21:46:12 +01:00
MathieuG-P a74c6bba10 Merge branch 'master' into build/rpm 2025-01-23 20:26:33 +01:00
MathieuG-P ebd4d4115a Merge pull request #745 from silentrald/bugfix/flatpak-stuff
[bugfix] fixed flatpack issues
2025-01-23 20:22:20 +01:00
MathieuG-P b91f0b06ab Merge pull request #750 from silentrald/feat/change-log-file-behavior
[feat] change how file log names are created
2025-01-22 17:05:14 +01:00
MathieuG-P 41af98dbfb Merge pull request #752 from FliegendeWurst/patch-1
package.json: correct license value based on ./LICENSE
2025-01-22 11:17:30 +01:00
Arne Keller cf0e092705 package.json: correct license value based on ./LICENSE 2025-01-22 09:36:42 +01:00
silentrald 93bcd358cb [feat] just set the deleteLogFolders than appending the new list 2025-01-22 08:35:47 +08:00
Arthur Luz 3cb744bf6c Merge branch 'master' into feature/pt-br-language 2025-01-21 17:51:12 -03:00
arthurmluz 53c9f92344 feat: adds brazilian portuguese language 2025-01-21 17:50:17 -03:00
MathieuG-P 5f6cf77f53 Add patreon 2025-01-21 20:51:24 +01:00
Shidorien 7caca61c1e fix: simplify installation instructions by removing redundant steps for downloading playlists, maps, and models 2025-01-21 20:31:39 +01:00
Shidorien 3f5b5ca4a0 fix: reorder download sources for BSManager in installation guides 2025-01-21 20:29:42 +01:00
silentrald ec0ae76d70 [feat] delete obsolete log files from previous handling 2025-01-21 12:19:05 +08:00
silentrald 13984f886f [feat] change how file log names are created 2025-01-21 12:07:22 +08:00
Shidorien 29e5b1946d fix: streamline instructions for searching and installing playlists and maps in the installation guide 2025-01-20 18:37:02 +01:00
Shidorien 09ffdcae32 enhance installation guide with detailed steps for installing and importing maps using BSManager 2025-01-20 18:35:22 +01:00
Shidorien 7fc6ffdda9 fix: reorder steps for installing playlists in the installation guide 2025-01-20 18:34:35 +01:00
Shidorien 0085d1e52e fix: update link for importing playlists in installation guide 2025-01-20 18:28:17 +01:00
Shidorien ccd36913de enhance installation guide with detailed steps for importing playlists using BSManager 2025-01-20 18:27:47 +01:00
Shidorien 4dcc4e92a2 fix: correct formatting in installation guide for playlist installation steps 2025-01-20 18:20:26 +01:00
Shidorien a4dab93c36 fix: update installation guide to improve clarity on playlist installation steps 2025-01-20 18:20:16 +01:00
Shidorien 39adbc42f0 update installation guide for playlists with BSManager 2025-01-20 18:16:30 +01:00
GaetanGrd 71da1d9484 Merge pull request #4 from Zagrios/master
update master
2025-01-20 18:00:04 +01:00
silentrald 8020084e77 [chore] updated contributor title to add rpm maintainer 2025-01-20 09:00:44 +08:00
MathieuG-P 0b4331bef2 bsm icon as default playlist cover 2025-01-19 00:09:53 +01:00
silentrald b284b33669 [chore] added rpm build for linux 2025-01-18 22:29:39 +08:00
silentrald effe0608e1 [bugfix] fix light theming pin icon color 2025-01-18 22:25:02 +08:00
silentrald 773b54ca08 [bugfix] recursively create the compatdata folder 2025-01-18 22:14:07 +08:00
silentrald 0c3a5b9e75 [bugfix] use static config for bs-version writing/reading for linux 2025-01-18 22:14:07 +08:00
silentrald dc3ae1971d [bugfix-742] launch bs version directly with steam shortcut 2025-01-18 22:14:07 +08:00
silentrald 350dff37cf [bugfix] refactored bs launch + launch bs version directly with shortcut 2025-01-18 22:14:07 +08:00
silentrald a47616086c [bugfix] fixed issue when proton logs with flatpak 2025-01-18 22:14:07 +08:00
silentrald 96fb2ed8c7 [bugfix-738] fixed issue when creating Beat Saber shortcuts 2025-01-18 22:14:07 +08:00
MathieuG-P bacead72ae Merge pull request #746 from Zagrios/bugfix/clear-ipa-folder-when-installing-bsipa
[bugfix] clear the IPA folder when installing BSIPA
2025-01-18 01:05:18 +01:00
MathieuG-P 56450f73c8 [bugfix] clear the IPA folder when installing BSIPA 2025-01-16 23:16:27 +01:00
MathieuG-P 68e260f831 Merge pull request #744 from silentrald/bugfix/light-launch-mods
[bugfix] fix light theming for launch mod buttons
2025-01-15 23:45:37 +01:00
silentrald 1559224ff9 [bugfix] fix light theming for launch mod buttons 2025-01-15 13:19:24 +08:00
MathieuG-P 4b8004ea80 Merge pull request #739 from Zagrios/bugfix/check-if-symlink-is-supported-before-linking
[bugfix] Check if symlink is supported on the current filesystem before linking
2025-01-14 16:38:10 +01:00
MathieuG-P e3fef23ef0 [bugfix] check if symlink is supported on current filesystem before linking 2025-01-14 16:23:45 +01:00
MathieuG-P 5f43a8fd4f Merge pull request #736 from silentrald/bugfix/733
[bugfix-733] custom compatdata folder for specific BSManager usage
2025-01-13 16:05:31 +01:00
MathieuG-P e4766f8a25 minor ui fixes 2025-01-12 16:31:17 +01:00
MathieuG-P 17deb2658e Merge pull request #737 from Zagrios/chore/move-import-mods-button
[chore] move import mods button
2025-01-12 15:37:26 +01:00
MathieuG-P b4d934f22a [chore] move import mods button 2025-01-12 12:45:30 +01:00
silentrald 11e61b72b3 [bugfix-733] custom compatdata folder for specific BSManager usage 2025-01-12 10:44:31 +08:00
Shidorien 989b045be7 Fix URL encoding in Linux sidebar links 2025-01-06 18:30:45 +01:00
Shidorien bbe708b767 Remove redundant line in installation guide 2025-01-06 00:22:19 +01:00
Shidorien b6513fb096 Merge branch 'master' of https://github.com/GaetanGrd/bs-manager 2025-01-06 00:21:46 +01:00
Shidorien a4d9069a3d Fix image src 2025-01-06 00:21:45 +01:00
GaetanGrd be49f1e02b Merge pull request #3 from Zagrios/master
update local fork
2025-01-06 00:21:04 +01:00
Shidorien 0417f3d4fa Update Install or import a version for 1.5 2025-01-06 00:20:15 +01:00
Shidorien 3feca3769a Update BSManager installation instructions to include multiple download sources 2025-01-06 00:19:17 +01:00
124 changed files with 10328 additions and 1494 deletions
+11 -5
View File
@@ -24,14 +24,20 @@ jobs:
with:
node-version: 22.11.0
cache: "npm"
# Update package lists
- name: update apt-get
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get update
# Install and setup rpm/Flatpak for Ubuntu
- name: Install rpm/flatpak packages (Ubuntu only)
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get install -y flatpak flatpak-builder rpm libarchive-tools
- run: npm ci
- run: npm run package
# Install and setup Flatpak for Ubuntu
- name: Install flatpak packages (Ubuntu only)
if: matrix.os == 'ubuntu-latest'
run: sudo apt-get install -y flatpak flatpak-builder
- name: Setup flatpak repo (Ubuntu only)
if: matrix.os == 'ubuntu-latest'
run: |
+8 -2
View File
@@ -20,8 +20,11 @@ jobs:
- name: Check out Git repository
uses: actions/checkout@v4
- name: Install flatpak packages
run: sudo apt-get install -y flatpak flatpak-builder
- name: update apt-get
run: sudo apt-get update
- name: Install rpm/flatpak packages
run: sudo apt-get install -y flatpak flatpak-builder rpm libarchive-tools
- name: Setup flatpak repo
run: |
@@ -39,6 +42,9 @@ jobs:
- name: Build deb
run: npx electron-builder --config electron-builder.config.js --publish always --linux deb --x64
- name: Build rpm
run: npx electron-builder --config electron-builder.config.js --publish always --linux rpm --x64
- name: Build flatpak
run: env DEBUG="@malept/flatpak-bundler" npx electron-builder --config electron-builder.config.js --publish always --linux flatpak
+210
View File
@@ -1,4 +1,214 @@
## Not released yet ~
[PR Merged since the last release](https://github.com/Zagrios/bs-manager/pulls?q=is%3Apr+is%3Amerged+merged%3A%3E2025-02-03)\
[PR Merged since the last alpha](https://github.com/Zagrios/bs-manager/pulls?q=is%3Apr+is%3Amerged+merged%3A%3E2025-02-03)
## [1.5.2](https://github.com/Zagrios/bs-manager/releases/v1.5.2) (Feb 3, 2025)
### Fixes
- Fixed an issue where BSManager was unable to load mods #786
## [1.5.1](https://github.com/Zagrios/bs-manager/releases/v1.5.1) (Jan 31, 2025)
### Fixes
- Fixed an issue where playlist files were wiped when linking the playlist folder #769 (Thanks to @silentrald)
- Fixed missing translations when deleting duplicate maps #774
### Other changes
- Added more logs for file and folder manipulations #772 (Thanks to @silentrald)
## [1.5.0](https://github.com/Zagrios/bs-manager/releases/v1.5.0) (Jan 30, 2025)
### Features
- A notification for outdated mods now appears when navigating to a Beat Saber version #755
- Added Brazilian Portuguese translation #751 (Thanks to @arthurmluz)
- Advanced launch arguments now accept environment variables #749 (Thanks to @silentrald)
- Added the ability to create shortcuts in the Steam library to launch different versions of Beat Saber #731
- Added Italian translation #721 (Thanks to @Davitekk)
- Added the ability to log in directly with Meta within BSManager to download Beat Saber #716
- Added an advanced setting on Windows to use the system proxy #704 (Thanks to @GoldJohnKing)
- Added an advanced launch option to skip starting Steam before launching Beat Saber #664 (Thanks to @LiamillionSS)
- You can now filter maps by leaderboard (Beatleader/ScoreSaber/All) #623 (Thanks to @silentrald)
- BSManager now goes to the latest launched version page on startup #603 (Thanks to @silentrald)
- Drag and drop your mods to install them #575 (Thanks to @silentrald)
- Added Korean translation #641 (Thanks to @izunya)
- Drag and drop your maps to import them #575 (Thanks to @silentrald)
- Added playlist support #503
- Added Linux support (Thanks to @silentrald and @Insprill)
- Added reset button to reset values when editing a Beat Saber version #438 (Thanks to @Liborsaf)
- You can now choose to install only needed mods or reinstall all mods #540
- The installation location for contents downloaded by BSManager is now asked after the installation of BSManager or when the previous location can no longer be accessed #566 (Thanks to @silentrald)
- Added an advanced setting to disable hardware acceleration for BSManager #572
- Added an advanced setting to use symlinks instead of junctions #572
### Fixes
- Fixed an issue where, under certain conditions, BSManager could crash if an error occurred while downloading Beat Saber from Steam #722 (Thanks to @silentrald)
- Fixed an issue where switching between Beat Saber versions too quickly displayed mods for the wrong version #472
- Fixed an issue where the launch window from a shortcut would never close #485
- Advanced launch arguments are used even when the input field is closed #496
- Fixed an issue where having many maps caused some maps to not have their information loaded #503
- Fixed an issue where downloading maps sometimes resulted in a timeout error #524
- Fixed an issue where the duration of maps could be wrong in some cases #522
- Fixed missing styles on the "add" and "setting" buttons in the nav bar #530
- Fixed an issue where switching between two Beat Saber versions didn't clear the selected content of the previous version #557
- Fixed a potential crash when BeatMods cloud contained invalid mod dependencies #558
- Fixed an issue where the folder location for the contents downloaded by BSManager could not be changed if the previous location was unreachable #566 (Thanks to @silentrald)
### Other changes
- A new log file is now created each time BSManager is launched #750 (Thanks to @silentrald)
- The advanced launch panel has been completely reworked #709
- Link and Unlink modals have been reworked to provide more detailed information to users #699
- Some folders are now blacklisted to prevent issues caused by linking incorrect folders #691 (Thanks to @silentrald)
- .NET Framework is no longer required #489
- Clear English translations for the Steam credentials popup #476 (Thanks to @Aeywoo)
- NSFW models are now blurred when browsing for models #493
- If map stars are unavailable, NPS or NJS of the map is now used to indicate its difficulty #553
- When no mods are available for a Beat Saber version, the version number is now shown #507
- After a first full maps loading, maps loads almost instantly #503
- Improved performance in lists with lot of contents #503
- Improved German translation #559 (Thanks to @kgabriel-dev)
- Updated the icon for the button to un-maximize the BSManager window #562 (Thanks to @joriskleiber)
## [1.4.18](https://github.com/Zagrios/bs-manager/releases/v1.4.18) (Jan 18, 2025)
### Fixes
- Fixed an issue where enabling Oculus sideloading was not working if BSManager installation path contained spaces #735
- The IPA folder is now cleared each time BSIPA is installed to prevent potential crashes caused by conflicts with old BSIPA files #746
- Added a check to ensure the file system supports symlinks before moving contents to the shared folder #739
### Other changes
- Events occurring in `bs-admin-start.exe` and `oculus-allow-dev-sideloaded.exe` are now logged #735
## [1.4.17](https://github.com/Zagrios/bs-manager/releases/v1.4.17) (Jan 2, 2025)
### Fixes
- Fixed an issue where original Steam installations of Beat Saber did not have any mods available
- Mod descriptions should no longer contain HTML tags
## [1.4.16](https://github.com/Zagrios/bs-manager/releases/v1.4.16) (Jan 2, 2025)
### Fixes
- Fixed infinite mod loading when a mod file could not be found on BeatMods #725 (Thanks to @silentrald)
- Fixed an issue where no mods were shown as installed when a mod dependency could not be found #726 (Thanks to @silentrald)
- Fixed minor UI issues
## [1.4.15](https://github.com/Zagrios/bs-manager/releases/v1.4.15) (Jan 2, 2025)
### Fixes
- Use the sideloading feature of Oculus to prevent errors when launching Oculus versions of Beat Saber #720
- Oculus token starting with `OC` are now accepted
### Other changes
- Mods now load faster #508
- Switched to the new BeatMods API for loading mods
## [1.4.14](https://github.com/Zagrios/bs-manager/releases/v1.4.14) (Dec 2, 2024)
### Fixes
- Fixed an issue where downloading private playlists through OneClick was always resulting in an error #679 (Thanks to @Top-Cat)
- Beat Saber file verification was broken due to an error in DepotDownloader #677
## [1.4.13](https://github.com/Zagrios/bs-manager/releases/v1.4.13) (Nov 26, 2024)
### Features
- Added the possibility to unselect all mods from the mods panel #666
### Fixes
- Fixed an issue where downloading maps with subfolders was not possible #649 (Thanks to @silentrald)
- Corrected the mods search bar color in the light theme #660 (Thanks to @chk1)
### Other changes
- Added the BeatLeader icon to the OneClick playlist settings #653
- A warning now appears before downloading an outdated BeatSaber version #654
- Oculus BeatSaber versions no longer launch if the Oculus library is not found #657 (Thanks to @LiamillionSS)
- Maps from BeatSaver are now sorted by relevance by default #662 (Thanks to @Top-Cat)
- Removed the `--no-yeet` argument when launching original copies of BeatSaber #667
- Added warnings for outdated versions in the versions view #673
## [1.4.12](https://github.com/Zagrios/bs-manager/releases/v1.4.12) (Nov 6, 2024)
### Fixes
- Fixed OneClick playlist download that has been broken in the previous release #645
- Fixed an issue where downloading playlists could fail if the playlist's file contained special characters #645
## [1.4.11](https://github.com/Zagrios/bs-manager/releases/v1.4.11) (Nov 4, 2024)
### Fixes
- BSManager was not working if the path to BSM's installation folder contained URL special characters #636
- Linking folders was no longer working if the folders contained files
- Under certain conditions, loading maps could result in a black screen
### Other changes
- The "Broken Models" notification has been removed #640
## [1.4.10](https://github.com/Zagrios/bs-manager/releases/v1.4.10) (Oct 27, 2024)
### Fixes
- Mods using a manifest file were sometimes not detected as installed #564
- BSIPA installation was always considered as successfull even if an error occured #511
- Some map filter tags were no longer working #619 (Thanks to @silentrald)
- Fixed an issue where, under certain conditions, linking a folder could result in the loss of its contents #568
### Other changes
- Default mods are no longer reselected when mods are already installed #535
- The `DLC` folder has been removed from the default list of folders that can be linked #628
- The naming scheme for downloaded maps and playlists has been updated to prevent duplicates when downloading maps or playlists from Beat Saber or other tools
## [1.4.9](https://github.com/Zagrios/bs-manager/releases/v1.4.9) (Oct 15, 2024)
### Fixes
- Updated DepotDownloader dependency to fix connection errors with Steam
### Other changes
- Added support for `info.dat` v4
## [1.4.8](https://github.com/Zagrios/bs-manager/releases/v1.4.8) (Aug 1, 2024)
### Other changes
- Added checks to avoid logging sensitive data in log files
- The oldest log files are deleted to retain a maximum of 5 log files
## [1.4.7](https://github.com/Zagrios/bs-manager/releases/v1.4.7) (Mar 13, 2024)
### Fixes
- Fixed being unable to download models with BSManager #436
### Other changes
- Fixed a typo in german translation (Thanks to @fllppi)
## [1.4.6](https://github.com/Zagrios/bs-manager/releases/v1.4.6) (Mar 6, 2024)
### Features
- It's now possible to exclude already installed maps when downloading maps from BSManager #426 (Thanks to @Liborsaf)
### Fixes
- Fixed an issue where BSManager couldn't get path to user's documents under certain conditions, preventing any action from the user #431
- To prevent the last launched Beat Saber version getting wiped by Oculus auto-updates, the symlink created to launch the version is now deleted after the game stops #432
## [1.4.5](https://github.com/Zagrios/bs-manager/releases/v1.4.5) (Feb 12, 2024)
### Fixes
- Fixed an issue preventing launching downgraded Steam versions under certain conditions #415
- Fixed an issue preventing launching downgraded Oculus versions under certain conditions #421
- Fixed an issue where SteamVR was not properly restored after launching Beat Saber from a shortcut with FPFC mode #417 (Thanks to @slinkstr)
## [1.4.4](https://github.com/Zagrios/bs-manager/releases/v1.4.4) (Feb 1, 2024)
### Fixes
- When launching an Oculus Beat Saber version, the Oculus app is also started if needed to avoid crashes #405
- If Steam is running as admin, Beat Saber is now also started as admin to avoid crashes #404
- Fixed the issue where BSManager could fail to start due to a missing DLL #400
- Fixed the issue where BSManager could not locate the Oculus library #398
### Other changes
- Added some tooltips to the maps UIs #397
- Updated the maps panel UI for consistency with the rest of BSManager UIs #399
## [1.4.3](https://github.com/Zagrios/bs-manager/releases/tag/v1.4.3) (Dec 27, 2023)
+93 -18
View File
@@ -1,33 +1,108 @@
## Contributing
# Contributing to BSManager
[code-of-conduct]: CODE_OF_CONDUCT.md
[fork]: https://github.com/Zagrios/bs-manager/fork
[pr]: https://github.com/Zagrios/bs-manager/compare
[style]: https://github.com/basarat/typescript-book/blob/master/docs/styleguide/styleguide.md
Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great.
Hello! Were delighted that youre interested in contributing to **BSManager**. Your help is essential to maintaining and improving this project.
Please note that this project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating in this project you agree to abide by its terms.
> [!NOTE]
> This project is released with a [Contributor Code of Conduct][code-of-conduct]. By participating, you agree to abide by its terms.
## Issues and PRs
---
If you have suggestions for how this project could be improved, or want to report a bug, open an issue! We'd love all and any contributions. If you have questions, too, we'd love to hear them.
## Opening Issues
We'd also love PRs. If you're thinking of a large PR, we advise opening up an issue first to talk about it, though! Look at the links below if you're not sure how to open a PR.
- **Reporting Bugs**
If you find a bug or any unexpected behavior, feel free to open an issue. Be sure to include steps to reproduce it, relevant logs, versions, and any other pertinent details.
- **Suggesting Enhancements**
Have an idea for improving BSManager? We welcome all suggestions—open an issue to discuss your proposal.
## Submitting a pull request
> [!TIP]
> Before opening a new issue, please check if theres already an existing one for the same topic to avoid duplicates.
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. Push to your fork and [submit a pull request][pr].
1. Pat your self on the back and wait for your pull request to be reviewed and merged.
---
Here are a few things you can do that will increase the likelihood of your pull request being accepted:
## Submitting Pull Requests
- Follow the [style guide][style] which is using standard.
- Keep your changes as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
### Before You Begin
Work in Progress pull requests are also welcome to get feedback early on, or if there is something blocked you.
1. **[Fork the repository][fork]** and **clone** your fork locally.
2. **Install the required Node.js version** (we recommend using [Volta](https://volta.sh/) to manage Node versions).
3. **Install project dependencies**:
```bash
npm install
```
### Create a Dedicated Branch
Use a clear naming convention for your branch:
```bash
git checkout -b (feature|bugfix|hotfix|chore)/(short-description)(/issue-id)
```
> [!NOTE]
> Exemple : `git checkout -b feature/update-docs/42`
### Develop and Test
> [!TIP]
> - Follow the [style guide][style] which is using standard.
> - Keep your changes as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as separate pull requests.
- Make the necessary changes on your new branch.
- Test your changes manually to ensure everything is working correctly.
- Verify that your updates do not introduce regressions or break existing functionality.
### Open a Pull Request
1. **Push** your branch to your fork.
2. Visit the main **BSManager** repository and click “Compare & pull request” or use [this link][pr].
3. Provide a **clear description** of your changes (the context, what problem it solves, etc.).
4. **Submit** your pull request. A maintainer will review it and may ask for adjustments or clarifications.
---
## Useful Commands
### Development
```bash
npm start
```
> Runs the application in development mode.
### Packaging
```bash
npm run package
```
> Packages the application using Electron Builder.
```bash
npm run build && electron-builder --config electron-builder.config.js --publish never --x64 --linux <deb/rpm/pacman>
```
> Package for Linux distros (deb, rpm, pacman)
```bash
npm run publish
```
> Publishes the application (primarily for Windows).
```bash
npm run publish:flatpak
```
> Build a Flatpak package
## Need Help?
If you have any questions or need further assistance, feel free to **open an issue** or reach out via our [discord](https://discord.gg/uSqbHVpKdV). We appreciate any and all contributions you make to BSManager!
Thank you for your interest in making BSManager better!
+99 -182
View File
@@ -36,7 +36,6 @@
<hr>
</div>
<!--
----------------------------------------
BADGES
@@ -58,7 +57,7 @@
src="https://img.shields.io/badge/-DISCORD-5865f2?style=for-the-badge&logo=discord&logoColor=ffffff"
alt="discord" /></a>
<a href="https://twitter.com/BSManager_"><img
src="https://img.shields.io/badge/-Twitter-black?style=for-the-badge&logo=X" alt="Twitter" /></a>
src="https://img.shields.io/badge/-Twitter-black?style=for-the-badge&logo=X" alt="Twitter" /></a>
<a href="https://www.bsmanager.io">
<img src="https://img.shields.io/badge/-WebSite-00649c?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMDAwIDEwMDAiPgogIDxnIGZpbGw9IiMzYjgyZmYiPgogICAgPHBhdGggZD0ibTYyNi43NSA0ODUuOTEgMjAuODIgMzUxLjk0YTM1LjIxIDM1LjIxIDAgMCAxLTE1LjQ5IDMxLjM2bC0xNDQuNzEgOTYuMTRoLS4wNmEzNS41NiAzNS41NiAwIDAgMS0xMS4zMSA0Ljc3IDM0Ljg1IDM0Ljg1IDAgMCAxLTIzLjUxLTIuODdMMTM2LjU4IDgwOS45YTExNS40NSAxMTUuNDUgMCAwIDEtNjMuNzUtOTYuNDlMNTIgMzYxLjQ2YTM1LjI2IDM1LjI2IDAgMCAxIDE1LjIzLTMxLjIxbDE0NS4wNS05Ni4zNmEzNS43MiAzNS43MiAwIDAgMSAxMS4yNy00LjcgMzQuODcgMzQuODcgMCAwIDEgMjMuNTEgMi44N0w1NjMgMzg5LjQyYTExNS40MSAxMTUuNDEgMCAwIDEgNjMuNzUgOTYuNDlaIi8+CiAgICA8cGF0aCBmaWx0ZXI9ImJyaWdodG5lc3MoMzAlKSIgZD0ibTYxMy43OCA0ODYuNjcgMjAuODIgMzUyYTIyLjM0IDIyLjM0IDAgMCAxLTMyLjI2IDIxLjMxTDI4Ni40IDcwMi41N0ExMDIuNDMgMTAyLjQzIDAgMCAxIDIyOS44MyA2MTdMMjA5IDI2NWEyMi4zNSAyMi4zNSAwIDAgMSAzMi4yNi0yMS4zMkw1NTcuMiA0MDEuMDVhMTAyLjQyIDEwMi40MiAwIDAgMSA1Ni41OCA4NS42MloiLz4KICA8L2c+CiAgPHBhdGggZD0iTTcxOC4yOSA3NzYuNDggNzAwIDgwNC4xOGMtNDAuNjMgMzkuNS04Ny4wNiA2Ny4yNS0xMzMuODEgOTAuNzUtODcuMzIgNDMuOS0yMDMuNjcgNzcuMTktMjQ5IDkwLjM0YTQ4IDQ4IDAgMCAxLTI2LjE3LjE2Yy02NC0xNy43Mi0xODguNjItNzItMjM2LjI4LTEyNS40OWE0LjIzIDQuMjMgMCAwIDEgNS4zLTYuNDVjMzUuNjQgMjAuODcgMTExLjIyIDU3LjY0IDE1Ni4zNyA3NC4yMiA0OS4xOCAxOC4wNiA4MS4yOSA4Ljc0IDkyLjI4LTMuMjZhNC43NyA0Ljc3IDAgMCAxIDcuMjQuMjFjMTkuNTEgMjQgMTA3LjI2IDQuNiAxNjYuNjQtMTMgNjQuMzQtMTkgMTg1Ljg1LTc2LjU2IDIzNS43Mi0xMzUuMThabS0xOTYuMiAxNTcuMzMtMTQ3LjQ1IDU1LjY4YTMuOTIgMy45MiAwIDAgMCAxLjQ0IDcuNThsNTQuNjItLjc3YTM1LjYzIDM1LjYzIDAgMCAwIDE5LTUuNzlsNzYtNDkuNTVhNCA0IDAgMCAwLTMuNjEtNy4xNVpNMjE4LjkyIDE5NS41NmMtMjEuMTQuMDctMzMuNTUgMzUuODYtMzMuMTYgODkuMDguMiAyOCAyLjU4IDkyLjc0IDQuNzkgMTQ3LjczLjIzIDUuNzEtOC4xOCA2LjQ4LTkgLjgxbC0zOC40Mi0yNzAuNzFhNC4zOCA0LjM4IDAgMCAxIDcuMTItNCA2My42MyA2My42MyAwIDAgMCAyMS45MyAxMS44NSA0LjYxIDQuNjEgMCAwIDAgNS45LTMuNzJjNC4yLTI3LjcgMTYuNjMtNTguNzQgMzcuMzUtODguNzRhNC4zNiA0LjM2IDAgMCAxIDcuNTYuNjljMTAuODYgMjMuODcgNDkgMTAzLjggOTMuMzQgMTUyLjc5YTQuNjUgNC42NSAwIDAgMS01LjYyIDcuMjJjLTM2LjU3LTE5LjM4LTgzLjAxLTQzLjAyLTkxLjc5LTQzWm0tMTUuNDcgMzg0LjhhNC41MSA0LjUxIDAgMCAxLTQuNDYtMy45MmwtMTMuODItMTA3LjI5YTQuNSA0LjUgMCAwIDEgOC45My0xLjE1bDEzLjgyIDEwNy4yOWE0LjUgNC41IDAgMCAxLTMuODkgNSAzLjg2IDMuODYgMCAwIDEtLjU4LjA3Wk01NzcuMDggNTQuNThjLTQ2Ljc5IDE1Ljg1LTExNS40NSA0MC41Ni0xNTguNzMgNjIuNzlhMzUuNSAzNS41IDAgMCAwLTE4LjU3IDI0LjUzYy0zLjk1IDE5LjQ5LTExIDU2LjA5LTE5IDEwNy4yNi0uNyA0LjQ4LTcuMzYgMy43OC03LjExLS43NWwxMC42Ni0xOTQuNzNhMy43OCAzLjc4IDAgMCAxIDcuNDYtLjY2bDMuNDkgMTQuODVhMi41NyAyLjU3IDAgMCAwIDQuNjkuNzdjNi4xOC0xMCAyMi4zNy0zMi43OSA1Ny4zMi02NWE0LjEzIDQuMTMgMCAwIDEgNi41NiA0LjcybC0zMi41NCA3Mi44OXM3OC43OC0xOS4zNyAxNDQtMzMuMDljNC4xMS0uODcgNS42OSA1LjA4IDEuNzcgNi40MloiIGZpbGw9IiNmZmYiIHN0cm9rZT0iYmxhY2siLz4KICA8ZyBmaWxsPSIjZjQ0Ij4KICAgIDxwYXRoIGZpbHRlcj0iYnJpZ2h0bmVzcyg2MCUpIHNhdHVyYXRlKDEyMCUpIiBkPSJtOTQ3LjA3IDQxMC4zNS01Ny43NSAzNDcuNzlhMzUuMTggMzUuMTggMCAwIDEtMjIuMDYgMjcuMTVsLTE2Mi40MyA2MS42NWgtLjA2YTM1LjUyIDM1LjUyIDAgMCAxLTEyLjA2IDIuMTEgMzQuODMgMzQuODMgMCAwIDEtMjIuMjktOGwtMjczLjE3LTIyMy41YTExNS40MyAxMTUuNDMgMCAwIDEtNDAuNzYtMTA4LjIybDU3Ljc1LTM0Ny44QTM1LjI1IDM1LjI1IDAgMCAxIDQzNiAxMzQuNDdsMTYyLjgzLTYxLjc5YTM1LjUxIDM1LjUxIDAgMCAxIDEyLTIuMDggMzQuODMgMzQuODMgMCAwIDEgMjIuMjkgOGwyNzMuMTkgMjIzLjUyYTExNS4zOCAxMTUuMzggMCAwIDEgNDAuNzYgMTA4LjIzWiIvPgogICAgPHBhdGggZD0iTTkzNC4yNSA0MDguMjIgODc2LjUgNzU2YTIyLjM0IDIyLjM0IDAgMCAxLTM2LjE4IDEzLjYzbC0yNzMuMTctMjIzLjVhMTAyLjQyIDEwMi40MiAwIDAgMS0zNi4xNy05Nmw1Ny43NS0zNDcuODNhMjIuMzQgMjIuMzQgMCAwIDEgMzYuMTgtMTMuNjNsMjczLjE3IDIyMy41MWExMDIuNDIgMTAyLjQyIDAgMCAxIDM2LjE3IDk2LjA0WiIvPgogIDwvZz4KICA8ZWxsaXBzZSBmaWxsPSIjZmZmIiBzdHJva2U9ImJsYWNrIiBjeD0iNzMyLjYxIiBjeT0iNDI5LjE2IiByeD0iNjIuODMiIHJ5PSIxMTAuNzMiIHRyYW5zZm9ybT0icm90YXRlKC0xNS40NSA3MzIuNzAzIDQyOS4xOTkpIi8+Cjwvc3ZnPg==" alt="Website" /></a>
<a href="https://www.patreon.com/bsmanager"><img
@@ -72,76 +71,64 @@
-->
<div>
<hr>
<details open>
<details open>
<summary><b>Table of Contents</b></summary>
<ul>
<li>
<li>
<details>
<summary><a href="#bsmanager-your-beat-saber-assistant">BSManager: Your Beat Saber Assistant</a></summary>
<ul>
<summary><a href="#bsmanager-your-beat-saber-assistant">BSManager: Your Beat Saber Assistant</a></summary>
<ul>
<li><a href="#what-is-it">What is it?</a></li>
<li><a href="#why-use-it">Why use it?</a></li>
<li><a href="#built-with">Built with</a></li>
<li><a href="#how-to-install">How to install</a></li>
</ul>
<li><a href="#how-to-install">How to install?</a></li>
</ul>
</details>
</li>
<li>
</li>
<li>
<details>
<summary><a href="#version-management">Version Management</a></summary>
<ul>
<li>
<details>
<summary><a href="#downloading-multiple-versions">Downloading multiple versions</a></summary>
<ul>
<li>via Steam account</li>
<li>via Oculus (PCVR) account</li>
</ul>
</details>
</li>
<li><a href="#multi-version-sharing">Multi-version Sharing</a></li>
<li><a href="#importing-an-existing-version-of-beat-saber">Importing an existing version of Beat Saber</a>
</li>
<li><a href="#launch-arguments">Launch arguments</a></li>
<li><a href="#shortcut-creation">Shortcut creation</a></li>
</ul>
<summary><a href="#version-management">Version Management</a></summary>
<ul>
<li><a href="#downloading-multiple-versions">Downloading Multiple Versions</a></li>
<li><a href="#multi-version-sharing">Multi-Version Sharing</a></li>
<li><a href="#importing-an-existing-version-of-beat-saber">Importing an Existing Version of Beat Saber</a></li>
<li><a href="#launch-arguments">Launch Arguments</a></li>
<li><a href="#shortcut-creation">Shortcut Creation</a></li>
</ul>
</details>
</li>
<li>
</li>
<li>
<details>
<summary><a href="#download-and-installation">Download and Installation</a></summary>
<ul>
<li><a href="#maps">Maps</a></li>
<li><a href="#playlists">Playlists</a></li>
<li><a href="#mods">Mods</a></li>
<li><a href="#models">Models</a></li>
</ul>
<summary><a href="#download-and-installation">Download and Installation</a></summary>
<ul>
<li><a href="#features">Features</a></li>
</ul>
</details>
</li>
<li>
</li>
<li>
<details>
<summary><a href="#customization">Customization</a></summary>
<ul>
<summary><a href="#customization">Customization</a></summary>
<ul>
<li><a href="#tool">Tool</a></li>
<li><a href="#beat-saber-instance">Beat Saber Instance</a></li>
</ul>
</ul>
</details>
</li>
<li>
</li>
<li>
<details>
<summary><a href="#more-information">More Information</a></summary>
<ul>
<summary><a href="#more-information">More Information</a></summary>
<ul>
<li><a href="#support">Support</a></li>
<li><a href="#credits">Credits</a></li>
<li><a href="#Contributing">Contributing</a></li>
</ul>
<li><a href="#contributing">Contributing</a></li>
</ul>
</details>
</li>
</li>
</ul>
</details>
<div align="right">
</details>
<div align="right">
[<a href="#readme-bot">Go to bottom</a>]
</div>
</div>
</div>
@@ -155,7 +142,6 @@
<h1><b>BSManager: Your Beat Saber Assistant</b></h1>
</div>
<!--
WHAT IS IT?
----------------------------------------
@@ -166,7 +152,6 @@
Saber</a> experience by centralizing the management of mods, maps, and game versions in one place.</p>
</div>
<!--
WHY USE IT?
----------------------------------------
@@ -189,7 +174,6 @@
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/012b0196-74ef-4ee5-86a4-49e835612f96"/>
</div>
<!--
BUILT WITH
----------------------------------------
@@ -219,7 +203,6 @@
</ul>
</div>
<!--
HOW TO INSTALL
----------------------------------------
@@ -231,8 +214,12 @@
<div>
<h3><b>Windows Installation</b></h3>
<ul>
<li>Download the <a href="https://github.com/Zagrios/bs-manager/releases/latest">latest release</a> from <a
href="https://github.com/Zagrios/bs-manager/releases">Releases</a>.</li>
<li>Download and install <a href="https://www.bsmanager.io">BSManager</a> from one of the following sources:</li>
<ul>
<li><a href="https://github.com/Zagrios/bs-manager/releases/latest">GitHub</a></li>
<li><a href="https://www.bsmanager.io">Official Website</a></li>
<li><a href="https://www.nexusmods.com/beatsaber/mods/18?tab=files">Nexus Mods</a></li>
</ul>
<li>Execute the installer and <a href="https://github.com/Zagrios/bs-manager">BSManager</a> will start itself.</li>
<li>Once it's done, if you want to download a version, select a version and download it! </li>
<li>From the version page you can choose options, launch the version, and more!</li>
@@ -244,8 +231,8 @@
</div>
<div>
<h3><b>Linux installation</b></h3>
<p>Refer to <a href="https://github.com/Zagrios/bs-manager/wiki/Linux">Linux wiki</a></p>
<h3><b>Linux Installation</b></h3>
<p>BSManager is also available on Linux! Follow the step-by-step guide in our <a href="https://github.com/Zagrios/bs-manager/wiki/Linux">Linux installation wiki</a> to set it up properly.</p>
</div>
<!--
@@ -264,48 +251,27 @@
existing ones, provided that you own the game on a Steam or Oculus (PC) account.</p>
</div>
<!--
DOWNLOADING MULTIPLE VERSIONS
----------------------------------------
-->
<div>
<h2>Downloading Multiple Versions</h2>
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> provides two authentication methods to access your
Steam account, ensuring safe and flexible access to
different game versions:</p>
</div>
<div align="center">
<img src="https://github.com/Zagrios/bs-manager/assets/40648115/09d579de-8b8e-45d3-8829-21a25cc7daf8"/>
</div>
<div>
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> allows you to safely and easily access different versions of Beat Saber, ensuring flexibility for modding or version-specific preferences.</p>
<p>To download a version, you can authenticate using two methods:</p>
<ul>
<li><strong>via Steam</strong>: Use either your credentials, which will not be stored by <a
href="https://github.com/Zagrios/bs-manager">BSManager</a> or
<a href="https://github.com/SteamRE/DepotDownloader">DepotDownloader</a>, or opt for a quick and secure
authentication via QR with the Steam Guard app.</li>
<li><strong>via Steam</strong>: Authenticate securely using your Steam credentials or by scanning a QR code with the Steam Guard app.</li>
<li><strong>via Oculus</strong>: BSManager redirects you to the official Oculus website for authentication to retrieve your token securely. Alternatively, you can follow <a href="https://github.com/Zagrios/bs-manager/wiki/How-to-obtain-your-Oculus-Token">this guide</a> to obtain your token manually and paste it into BSManager.</li>
</ul>
<p>For detailed instructions on installing versions, follow this tutorial:
👉 <a href="https://github.com/Zagrios/bs-manager/wiki/install-or-import-a-version">How to install or import a version</a>
</p>
</div>
<div align="center">
<img src="https://github.com/Zagrios/bs-manager/assets/40648115/26da3a19-a936-4e6b-b79b-a139d7bd55de" />
<img src="https://github.com/Zagrios/bs-manager/assets/40648115/09d579de-8b8e-45d3-8829-21a25cc7daf8" alt=""/>
</div>
<div>
<ul>
<li><strong>via Oculus</strong>: For Oculus users, <a href="https://github.com/Zagrios/bs-manager">BSManager</a>
requires you to retrieve a connection token by following the instructions in this guide: <a href="https://github.com/Zagrios/bs-manager/wiki/How-to-obtain-your-Oculus-Token">How to obtain your Oculus Token</a>. Once obtained, please insert it into the form.</li>
</ul>
</div>
<div align="center">
<img height=450 src="https://github.com/Zagrios/bs-manager/assets/40648115/3674e629-542d-4de3-b8db-b248f25126d7" />
</div>
<!--
MULTI-VERSION SHARING
----------------------------------------
@@ -320,10 +286,9 @@
</div>
<div align="center">
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/ba39aceb-f55f-4e58-8386-51fcfaad4957"/>
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/ba39aceb-f55f-4e58-8386-51fcfaad4957"/>
</div>
<!--
IMPORTING AN EXISTING VERSION OF BEAT SABER
----------------------------------------
@@ -340,49 +305,53 @@
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/cdedb951-d97a-474d-a217-2422be3919f7"/>
</div>
<!--
LAUNCH ARGUMENTS
----------------------------------------
-->
<div>
<h2>Launch Arguments</h2>
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> provides efficient management of <a
href="https://beatsaber.com/">Beat Saber</a> launch arguments, including three predefined ones to
enhance your gaming experience:</p>
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> provides advanced management of launch arguments for <a href="https://beatsaber.com/">Beat Saber</a>, allowing you to customize your gaming experience.</p>
<h3>Predefined Arguments</h3>
<ul>
<li><strong>Oculus Mode</strong>: Launches the game directly in Oculus mode, avoiding the use of STEAM VR.</li>
<li><strong>FPFC Mode</strong>: Enables "FPFC" mode, allowing you to control the game without using a VR
headset.</li>
<li><strong>Debug Mode</strong>: Activates debugging mode, generating Windows log outputs for IPA.</li>
<li><strong>Oculus Mode</strong>: Allows you to use Oculus's VR composer while bypassing SteamVR, which may improve performance. (Only useful if you launch the game via Steam).</li>
<li><strong>FPFC Mode</strong>: Enables "First Person Flying Controller" mode, allowing you to navigate menus using the keyboard (WASD) and mouse without a VR headset.</li>
<li><strong>Debug Mode</strong>: Activates the IPA debug console, displaying logs used by mods.</li>
<li><strong>Skip Steam</strong>: Prevents Steam from opening automatically with Beat Saber, useful if you use an alternative VR runtime like WIVRn or Monado.</li>
<li><strong>Map Editor</strong>: Launches the official Beat Saber map editor instead of the game.</li>
</ul>
<p>You also have the freedom to add your own custom launch arguments by inserting them into "Advanced Launch" and
separating them with a semicolon (;).</p>
<h3>Custom Arguments</h3>
<p>You can also add your own custom launch arguments by inserting them into "Advanced Launch" and separating them with a semicolon (;).</p>
<h3>Pinning Arguments</h3>
<p>You can now pin your favorite launch arguments for quick access. By default, only <strong>Oculus Mode</strong> and <strong>Debug Mode</strong> are pinned.</p>
</div>
<div align="center">
<img src="https://github.com/Zagrios/bs-manager/assets/40648115/1244d49f-20e1-400c-9818-528f05cfadf5" />
<img src="https://github.com/user-attachments/assets/c247b237-9c49-4e7b-9366-aeb1dd3b7b9a" alt="" />
</div>
<!--
SHORTCUT CREATION
----------------------------------------
-->
<div>
<h2>Shortcut Creation</h2>
<p>Create shortcuts to easily access your different versions of <a href="https://beatsaber.com/">Beat Saber</a> with
<a href="https://github.com/Zagrios/bs-manager">BSManager</a>. After selecting your
version, go to the version settings, click on "Create a Shortcut", and then select your launch options. Thus,
you can quickly and easily launch your preferred version of <a href="https://beatsaber.com/">Beat Saber</a> with the
desired settings.</p>
<p>Create shortcuts to easily access your different versions of <a href="https://beatsaber.com/">Beat Saber</a> with <a href="https://github.com/Zagrios/bs-manager">BSManager</a>.
</p>
<p>To learn how to create a shortcut, follow the tutorial available here:
👉 <a href="https://github.com/Zagrios/bs-manager/wiki/create-a-shortcut">Shortcut Creation Guide</a>
</p>
<p>This allows you to quickly and easily launch your preferred version of <a href="https://beatsaber.com/">Beat Saber</a> with the desired settings.
</p>
</div>
<div align="center">
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/bfa1e97d-6392-4dec-a1fc-bb05048cf6d2" />
</div>
<!--
----------------------------------------
DOWNLOAD AND INSTALLATION
@@ -392,79 +361,30 @@
<hr>
<h1><b>Download and Installation</b></h1>
</div>
<div>
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> provides centralized and organized management of
beatmaps, mods, and models, integrating download features and sharing between <a href="https://beatsaber.com/">Beat
Saber</a> versions.</p>
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> centralizes and simplifies the management of beatmaps, playlists, mods, and models, offering seamless downloads and sharing between different <a href="https://beatsaber.com/">Beat Saber</a> versions.</p>
</div>
<!--
MAPS
----------------------------------------
-->
<div>
<h2>Maps</h2>
<p><a href="https://github.com/Zagrios/bs-manager">BSManager</a> integrates the <a href="https://beatsaver.com">Beat
Saver</a> API to provide you with a comprehensive list of maps downloadable directly from its interface. Moreover,
the OneClick feature allows you to download maps with one click from various sources such as <a
href="https://beatsaver.com">Beat Saver</a>, <a href="https://bsaber.com">BeastSaber</a>, <a
href="https://scoresaber.com">ScoreSaber</a>, and <a href="https://www.beatleader.xyz">Beat Leader</a>. To add
maps, simply select the desired version or the "shared" tab, then click "ADD".</p>
<h2>Features</h2>
<p>With <a href="https://github.com/Zagrios/bs-manager">BSManager</a>, you can manage your:
</p>
<ul>
<li><b>Maps:</b> Browse and download maps directly from sources like <a href="https://beatsaver.com">Beat Saver</a>, <a href="https://bsaber.com">BeastSaber</a>, <a href="https://scoresaber.com">ScoreSaber</a>, and <a href="https://www.beatleader.xyz">Beat Leader</a>.</li>
<li><b>Playlists:</b> Search for and download playlists directly from BSManager, just like maps. 😉
<li><b>Mods:</b> Manage and install mods via the "Mods" tab, offering the same features as Mod Assistant.</li>
<li><b>Models:</b> Add and manage models easily with the integration of the <a href="https://modelsaber.com">Model Saber</a> API.</li>
</ul>
<p>For detailed instructions on each feature, check the following guides:</p>
<ul>
<li>👉 <a href="https://github.com/Zagrios/bs-manager/wiki/install-any-map">Install Any Map</a></li>
<li>👉 <a href="https://github.com/Zagrios/bs-manager/wiki/install-all-playlists-you-want">Install Playlists</a></li>
<li>👉 <a href="https://github.com/Zagrios/bs-manager/wiki/install-mods-on-your-version">Install Mods</a></li>
<li>👉 <a href="https://github.com/Zagrios/bs-manager/wiki/modify-models-in-your-beat-saber">Manage Models</a></li>
</ul>
</div>
<div align="center">
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/3f9f9968-ac55-465e-bc28-17aebfc787ea"/>
</div>
<!--
PLAYLISTS
----------------------------------------
-->
<div>
<h2>Playlists</h2>
<p>Although the playlist management interface is not yet developed, <a
href="https://github.com/Zagrios/bs-manager">BSManager</a> offers a OneClick feature for playlists from <a
href="https://beatsaver.com">Beat Saver</a>.</p>
</div>
<div align="center">
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/733fe6bb-c157-4c47-8a79-f8062380bcc8" />
</div>
<!--
MODS
----------------------------------------
-->
<div>
<h2>Mods</h2>
<p>Manage your mods easily with <a href="https://github.com/Zagrios/bs-manager">BSManager</a>. After downloading a
version, navigate to the "Mods" tab of the selected version to manage and install mods. <a
href="https://github.com/Zagrios/bs-manager">BSManager</a> operates similarly to Mod Assistant and offers the same
mods as this tool.</p>
</div>
<div align="center">
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/ca8f0727-b2dd-41f3-bbea-9bcaf61a89f1"/>
</div>
<!--
MODELS
----------------------------------------
-->
<div>
<h2>Models</h2>
<p>Add and manage models simply via the corresponding tab on the desired version or by going to the "shared" tab. The
<a href="https://modelsaber.com">Model Saber</a> API is integrated to provide easy and direct access to models
available for <a href="https://beatsaber.com/">Beat Saber</a>.</p>
</div>
<div align="center">
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/d983b34b-9f4c-4274-aee4-4c1c03dd90d0"/>
</div>
<!--
----------------------------------------
CUSTOMIZATION
@@ -495,7 +415,7 @@
</ul>
</div>
<div align="center">
<div align="center">
<video src = "https://github.com/Zagrios/bs-manager/assets/40648115/d2c1b0ec-a2f5-41b5-99d5-c4c2cd34cbd4" />
</div>
@@ -520,7 +440,6 @@
<video src="https://github.com/Zagrios/bs-manager/assets/40648115/f66a2c99-770e-42ca-9a71-b1806946ad0c" />
</div>
<!--
----------------------------------------
MORE INFORMATION
@@ -546,7 +465,6 @@
GitHub</a>.</p>
</div>
<!--
CREDITS
----------------------------------------
@@ -554,12 +472,12 @@
<div>
<h2>Credits</h2>
<ul>
<li><a href="https://github.com/Zagrios">Zagrios - Mathieu Gries</a> - Lead Developer & Founder.</li>
<li><a href="https://github.com/Zagrios">Zagrios</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>
<li><a href="https://github.com/Insprill">Insprill</a> - Co-Developer, Linux Developer, AUR Maintainer.</li>
<li><a href="https://github.com/silentrald">silentrald</a> - Co-Developer, Linux Developer, deb and flatpak Maintainer.</li>
<li><a href="https://github.com/silentrald">silentrald</a> - Co-Developer, Linux Developer, deb/rpm/flatpak Maintainer.</li>
</ul>
</div>
@@ -575,12 +493,11 @@
href="https://github.com/Zagrios/bs-manager">BSManager</a>!</p>
<div align="center">
<a href="https://github.com/Zagrios/bs-manager/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Zagrios/bs-manager" />
<img src="https://contrib.rocks/image?repo=Zagrios/bs-manager" alt="Contributor list" />
</a>
</div>
</div>
<!--
----------------------------------------
Others
+38 -2
View File
@@ -802,8 +802,7 @@
"OculusBinaryId": "8189133517853022",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/4451340304252928024",
"ReleaseDate": "1732107748",
"year": "2024",
"recommended": true
"year": "2024"
},
{
"BSVersion": "1.40.0",
@@ -813,5 +812,42 @@
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/65df6b24c3b8a7f415080ac01394917cedabf9fc.png",
"ReleaseDate": "1733846128",
"year": "2024"
},
{
"BSVersion": "1.40.1",
"BSManifest": "8655702238937650396",
"OculusBinaryId": "8526525354113835",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510696972023234581",
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/5cc1ca353151ed31da2764716cdcbe89442aeeeb.png",
"ReleaseDate": "1738144954",
"year": "2025"
},
{
"BSVersion": "1.40.2",
"BSManifest": "5496832903170665287",
"OculusBinaryId": "8626990240734012",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510697606000673692",
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/04ad25256ffc2686f4ea19830e70df6b2752f728.png",
"ReleaseDate": "1738951837",
"year": "2025"
},
{
"BSVersion": "1.40.3",
"BSManifest": "2468336483467342711",
"OculusBinaryId": "8747607938672241",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510699509433500685",
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/648716b53c17412cd269567ce67d3867514b6acd.png",
"ReleaseDate": "1740672033",
"year": "2025"
},
{
"BSVersion": "1.40.4",
"BSManifest": "3323243802033113443",
"OculusBinaryId": "8889412401158460",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/510702041542166484",
"ReleaseImg": "https://clan.fastly.steamstatic.com/images/32055887/c27dac7f96e53672d020d97838cec1d5d28e3180.png",
"ReleaseDate": "1743515622",
"year": "2025",
"recommended": true
}
]
+25 -1
View File
@@ -138,5 +138,29 @@
{
"username": "Alexander Herman",
"type": "gold"
}
},
{
"username": "Arts Rimuro Suraimu",
"type": "gold"
},
{
"username": "Ricardo Boss"
},
{
"username": "Jesper Norsted"
},
{
"username": "Marcus Hamm"
},
{
"username": "jxkelol"
},
{
"username": "paperwasp",
"type": "gold"
},
{
"username": "KingCrocman",
"type": "diamond"
}
]
+71 -14
View File
@@ -11,7 +11,7 @@
"avatars": "Avatare",
"sabers": "Säbel",
"platforms": "Plattformen",
"blocks": "Blöcke",
"bloqs": "Blöcke",
"cancel": "Abbrechen",
"delete": "Löschen",
"accept": "Akzeptieren",
@@ -26,7 +26,15 @@
"continue": "Weiter",
"shared": "Geteilt",
"pin": "Anheften",
"unpin": "Lösen"
"unpin": "Lösen",
"this-guide": "dieser Leitfaden",
"create": "Erstellen",
"edit": "Bearbeiten"
},
"generic": {
"env": {
"parse": "Konnte den Umgebungsvariablen-String nicht richtig parsen."
}
},
"title-bar": {
"outdated": "veraltet"
@@ -51,7 +59,8 @@
"outdated-tippy": "Diese Version ist veraltet, und einige Mods oder Funktionen funktionieren möglicherweise nicht wie erwartet. Es wird empfohlen, die empfohlene Version ({recommendedVersion}) von Beat Saber zu verwenden, um die neuesten Funktionen und Fehlerbehebungen zu genießen.",
"advanced-launch": {
"button": "Startoptionen",
"placeholder": "Weitere Argumente, bspw: --revert; --nowait"
"placeholder": "Startoptionen, bspw: KEY=VALUE %command% fpfc",
"create-launch-option": "Eine Startoption erstellen"
},
"skipsteam": "Steam überspringen",
"skipsteam-description": "Verhindert, dass Steam automatisch mit Beat Saber geöffnet wird. Aktivieren Sie dies, wenn Sie eine andere VR-Laufzeit wie WiVRn oder Monado verwenden, mit der SteamVR interferieren könnte.",
@@ -126,6 +135,11 @@
"loading-mods": "Lade Mods...",
"no-internet": "Kein Internet",
"mods-not-available": "Für diese Version von Beat Saber sind noch keine Mods verfügbar.",
"status": {
"no-wineprefix": "Konnte den BSManager WINEPREFIX-Pfad nicht finden. Bitte starte Beat Saber zuerst in BSManager.",
"beatmods-down": "Beatmods ist derzeit nicht erreichbar. Bitte versuchen Sie es später noch einmal. Wenn das Problem weiterhin besteht, informieren Sie uns auf {links}.",
"unknown": "Ein unbekannter Fehler ist aufgetreten ¯\\_(ツ)_/¯"
},
"buttons": {
"more-infos": "Mehr Infos",
"install-or-update": "Installieren oder aktualisieren",
@@ -134,6 +148,7 @@
"mods-grid": {
"header-bar": {
"name": "Name",
"size": "Größe",
"installed": "Installiert",
"latest": "Neueste",
"description": "Beschreibung",
@@ -142,12 +157,24 @@
"uninstall-all": "Alle deinstallieren",
"unselect-all": "Alle abwählen"
}
},
"mod-item": {
"this-is-a-large-mod": "Das ist ein großer Mod!",
"this-is-a-very-large-mod": "Das ist ein sehr großer Mod!"
}
},
"notifications": {
"all-mods-already-installed": {
"title": "Mods bereits installiert",
"description": "Alle ausgewählten Mods sind bereits installiert"
},
"outdated-mods": {
"title": "Veraltetes Mod",
"title-plural": "Veraltete Mods",
"description": "Das Mod {name} ist veraltet. Möchten Sie es aktualisieren?",
"description-plural": "Diese Version enthält {nb} veraltete Mods. Möchten Sie sie aktualisieren?",
"dont-remind-me": "Nicht mehr daran erinnern",
"update": "Aktualisieren"
}
},
"drop-zone": {
@@ -227,23 +254,29 @@
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"pt-BR": "Português (Brasil)",
"ru-RU": "Русский",
"zh-CN": "简体中文",
"zh-TW": "正體中文",
"ja-JP": "日本語",
"ko-KR": "한국어",
"uk-UA": "Українська",
"tl-PH": "Filipino",
"translated": {
"en-EN": "Englisch, Vereinigtes Königreich",
"en-US": "Englisch, USA",
"fr-FR": "Französisch",
"es-ES": "Spanisch",
"pt-BR": "Portugiesisch",
"it-IT": "Italienisch",
"de-DE": "Deutsch",
"ru-RU": "Russisch",
"zh-CN": "Vereinfachtes Chinesisch",
"zh-TW": "Traditionelles Chinesisch",
"ja-JP": "Japanisch",
"ko-KR": "Koreanisch"
"ko-KR": "Koreanisch",
"uk-UA": "Ukrainisch",
"tl-PH": "Filipino"
}
}
},
@@ -382,17 +415,20 @@
"oculus-download": {
"errors": {
"msg": {
"DOWNLOAD_MANIFEST_FAILED": "Es ist nicht möglich, das Manifest für diese Version herunterzuladen. Dein 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 oder du besitzt die Desktop-Version von Beat Saber nicht.",
"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.",
"UNABLE_TO_GET_MANIFEST": "Unmöglich, das für den Download erforderliche Manifest zu erhalten. Dein Anmelde-Token ist möglicherweise ungültig oder du besitzt die Desktop-Version von Beat Saber nicht.",
"VERIFY_INTEGRITY_FAILED": "Beim Überprüfen der Dateien ist ein Fehler aufgetreten.",
"SOME_FILES_FAILED_TO_DOWNLOAD": "Einige Dateien konnten nicht heruntergeladen werden.",
"META_LOGIN_TIMED_OUT": "Es hat zu lange gedauert, das Anmelde-Token abzurufen.",
"META_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."
},
"actions": {
"claim-your-desktop-version": "Fordere deine Desktop-Version an"
}
}
}
@@ -573,7 +609,8 @@
"msg": {
"success": "Alle Mods wurden entfernt.",
"errors": {
"no-mods": "Keine Mods sind in dieser Version installiert 😑"
"no-mods": "Keine Mods sind in dieser Version installiert 😑",
"no-wineprefix": "Konnte den BSManager WINEPREFIX-Pfad nicht finden. Bitte starte Beat Saber zuerst in BSManager."
}
}
},
@@ -641,6 +678,7 @@
"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.",
"LinkingNotSupported": "Ordnerverknüpfung wird auf diesem Dateisystem nicht unterstützt.",
"UNKNOWN_ERROR": "Ein unbekannter Fehler ist beim Verknüpfen des Ordners aufgetreten."
}
},
@@ -666,6 +704,16 @@
"do-not-remind": "Nicht mehr daran erinnern",
"ok": "Ok"
}
},
"bs-uninstall": {
"errors": {
"title": "Version kann nicht deinstalliert werden",
"desc": {
"CantDeleteOfficialVersion": "BSManager kann die originale Beat Saber Version nicht deinstallieren",
"VersionNotFound": "Version nicht gefunden.",
"delete-folder": "Version kann nicht gelöscht werden. Stelle sicher, dass Beat Saber nicht läuft."
}
}
}
},
"modals": {
@@ -870,7 +918,8 @@
"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ö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 dir, deinen Anmelde-Token einzugeben, ohne dich über Meta anzumelden."
"enter-token-manually-tooltip": "Dies ermöglicht es dir, deinen Anmelde-Token einzugeben, ohne dich über Meta anzumelden.",
"must-own-desktop-version": "Du musst die Desktop-Version von Beat Saber besitzen. Wenn du die eigenständige Version gekauft hast, folge {link}, um sie zu beanspruchen."
},
"stay": "Angemeldet bleiben",
"connect-to-meta": "Mit Meta verbinden"
@@ -925,12 +974,20 @@
},
"bs-version-outdated": {
"body": "Die Version {outdatedVersion} ist veraltet, und einige Mods oder Funktionen funktionieren möglicherweise nicht mehr wie erwartet. Bitte lade die neueste empfohlene Version ({recommendedVersion}) von Beat Saber herunter, um die neuesten Funktionen und Fehlerbehebungen zu nutzen."
},
"custom-launch-option": {
"title": "Benutzerdefinierte Startoption",
"title-placeholder": "Meine Option",
"desc": "Erstellen Sie eine wiederverwendbare Startoption, um Beat Saber mit einem bestimmten Befehl zu starten.",
"name": "Name",
"launch-option": "Startoption",
"create": "Erstellen"
}
},
"maps": {
"map-filter-panel": {
"duration": "Länge",
"nps" : "Noten pro Sekunde",
"nps": "Noten pro Sekunde",
"njs": "Notensprunggeschwindigkeit",
"tags": "Stichwörter",
"specificities": "Allgemein",
@@ -1011,11 +1068,11 @@
"by": "Von {songAutor}",
"mapped-by": "Gemapped von",
"delete": "Löschen",
"preview" : "Kartenvorschau",
"bsr-code" : "BSR Code",
"download" : "Karte herunterladen",
"downloading" :"Karte wird heruntergeladen",
"cancel-download" : "Download abbrechen",
"preview": "Kartenvorschau",
"bsr-code": "BSR Code",
"download": "Karte herunterladen",
"downloading": "Karte wird heruntergeladen",
"cancel-download": "Download abbrechen",
"hightlight-difficulty": "Schwierigkeit hervorheben"
}
},
+75 -62
View File
@@ -11,7 +11,7 @@
"avatars": "Avatars",
"sabers": "Sabers",
"platforms": "Platforms",
"blocks": "Blocks",
"bloqs": "Blocks",
"cancel": "Cancel",
"delete": "Delete",
"accept": "Accept",
@@ -26,7 +26,15 @@
"continue": "Continue",
"shared": "Shared",
"pin": "Pin",
"unpin": "Unpin"
"unpin": "Unpin",
"this-guide": "this guide",
"create": "Create",
"edit": "Edit"
},
"generic": {
"env": {
"parse": "Could not properly parse the env var string."
}
},
"title-bar": {
"outdated": "outdated"
@@ -51,7 +59,8 @@
"outdated-tippy": "This version is outdated, and some mods or features may no longer work as expected. Prefer using the recommended version ({recommendedVersion}) of Beat Saber to enjoy the latest features and bugfixes.",
"advanced-launch": {
"button": "Launch options",
"placeholder": "Additional arguments ex: --revert; --nowait"
"placeholder": "Launch options ex: KEY=VALUE %command% fpfc",
"create-launch-option": "Create a launch option"
},
"skipsteam": "Skip Steam",
"skipsteam-description": "Stops Steam from opening automatically with Beat Saber, enable if you are using a different VR runtime like WiVRn or Monado that SteamVR may interfere with.",
@@ -126,6 +135,11 @@
"loading-mods": "Loading mods...",
"no-internet": "No internet",
"mods-not-available": "No mods are available yet for this version of Beat Saber",
"status": {
"no-wineprefix": "Could not find BSManager WINEPREFIX path. Please launch Beat Saber in BSManager first.",
"beatmods-down": "Beatmods is currently unreachable. Please retry later. If the issue persists, inform us on {links}.",
"unknown": "An unknown error occurred ¯\\_(ツ)_/¯"
},
"buttons": {
"more-infos": "More info",
"install-or-update": "Install or update",
@@ -134,6 +148,7 @@
"mods-grid": {
"header-bar": {
"name": "Name",
"size": "Size",
"installed": "Installed",
"latest": "Latest",
"description": "Description",
@@ -142,12 +157,24 @@
"uninstall-all": "Uninstall all",
"unselect-all": "Unselect all"
}
},
"mod-item":{
"this-is-a-large-mod": "This is a large mod!",
"this-is-a-very-large-mod": "This is a very large mod!"
}
},
"notifications": {
"all-mods-already-installed": {
"title": "Mods already installed",
"description": "All selected mods are already installed"
},
"outdated-mods": {
"title": "Outdated Mod",
"title-plural": "Outdated Mods",
"description": "{name} mod is outdated. Do you want to update it?",
"description-plural": "This version has {nb} outdated mods. Do you want to update them?",
"dont-remind-me": "Don't remind me",
"update": "Update"
}
},
"drop-zone": {
@@ -227,11 +254,14 @@
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"pt-BR": "Português (Brasil)",
"ru-RU": "Русский",
"zh-CN": "简体中文",
"zh-TW": "正體中文",
"ja-JP": "日本語",
"ko-KR": "한국어",
"uk-UA": "Українська",
"tl-PH": "Filipino",
"translated": {
"en-EN": "English, United Kingdom",
"en-US": "English, USA",
@@ -239,11 +269,14 @@
"es-ES": "Spanish",
"it-IT": "Italian",
"de-DE": "German",
"pt-BR": "Portuguese (Brazil)",
"ru-RU": "Russian",
"zh-CN": "Chinese (Simplified)",
"zh-TW": "Chinese (Traditional)",
"ja-JP": "Japanese",
"ko-KR": "Korean"
"ko-KR": "Korean",
"uk-UA": "Ukrainian",
"tl-PH": "Filipino"
}
}
},
@@ -382,17 +415,20 @@
"oculus-download": {
"errors": {
"msg": {
"DOWNLOAD_MANIFEST_FAILED": "Unable to download the manifest for this version; your login token may be invalid.",
"DOWNLOAD_MANIFEST_FAILED": "Unable to download the manifest. Your login token may be invalid, or you don't own the desktop version of Beat Saber.",
"MANIFEST_FILE_NOT_FOUND": "Unable to find the manifest for this version.",
"PARSE_MANIFEST_FILE_FAILED": "An error occurred while reading the manifest.",
"ALREADY_DOWNLOADING": "A version is already being downloaded.",
"UNABLE_TO_GET_MANIFEST": "Unable to obtain the manifest required for download.",
"UNABLE_TO_GET_MANIFEST": "Unable to obtain the manifest required for download. Your login token may be invalid, or you don't own the desktop version of Beat Saber.",
"VERIFY_INTEGRITY_FAILED": "An error occurred during file verification.",
"SOME_FILES_FAILED_TO_DOWNLOAD": "Some files failed to download.",
"META_LOGIN_TIMED_OUT": "The login token took too long to retrieve.",
"META_LOGIN_WINDOW_CLOSED_BY_USER": "The Meta login window was closed.",
"NO_META_AUTH_TOKEN": "Unable to retrieve the Meta login token required for download.",
"UNKNOWN_ERROR": "An unknown error occurred."
},
"actions":{
"claim-your-desktop-version": "Claim your desktop version"
}
}
}
@@ -573,7 +609,8 @@
"msg": {
"success": "All mods have been uninstalled.",
"errors": {
"no-mods": "No mods are installed in this version 😑"
"no-mods": "No mods are installed in this version 😑",
"no-wineprefix": "Could not find BSManager WINEPREFIX path. Please launch Beat Saber in BSManager first."
}
}
},
@@ -641,6 +678,7 @@
"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.",
"LinkingNotSupported": "Folder linking is not supported on this file system.",
"UNKNOWN_ERROR": "An unknown error has occurred while linking the folder."
}
},
@@ -666,6 +704,16 @@
"do-not-remind": "Do not remind me",
"ok": "Ok"
}
},
"bs-uninstall": {
"errors": {
"title": "Unable to uninstall version",
"desc": {
"CantDeleteOfficialVersion": "BSManager cannot uninstall the original Beat Saber version",
"VersionNotFound": "Version not found.",
"delete-folder": "Unable to delete the version. Ensure Beat Saber is not running."
}
}
}
},
"modals": {
@@ -802,9 +850,14 @@
"multiple": "These maps will also be removed from versions using shared 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?"
}
},
"link-contents":{
"link-contents": {
"title": "Link {contentType}",
"p-1": "Linking {contentType} allows sharing {contentType} between all versions with this feature enabled. Once linked, this version will benefit from shared {contentType}.",
"p-2": "In order to enjoy the linking feature, all your {itemsHtml} will be moved to the {sharedHtml} folder. After that, a symbolic link pointing to the shared {contentType} folder will replace your original {contentType} folder.",
@@ -813,7 +866,7 @@
"i-need-help": "I need help",
"valid-btn": "Link {contentType}"
},
"unlink-contents":{
"unlink-contents": {
"title": "Unlink {contentType}",
"p-1": "Be careful, unlinking {contentType} will disable the use of shared {contentType} for this version, until it is reenabled.",
"p-2": "In order to unlink your maps, the symbolic link pointing to the {sharedHtml} will be removed. After that, all maps in the shared folder will be copied to the {itemsHtml} folder of your Beat Saber version.",
@@ -865,7 +918,8 @@
"token-needed": "Your Meta connection token is required to download Beat Saber.",
"need-cookie-enabled": "By logging into Meta, a login window will open and you can then start the login process. Please ensure to accept cookies, otherwise, we might not be able to retrieve your token to start the download.",
"enter-token-manually": "Enter my login token manually",
"enter-token-manually-tooltip": "This will allow you to enter your login token without going through Meta login."
"enter-token-manually-tooltip": "This will allow you to enter your login token without going through Meta login.",
"must-own-desktop-version": "You must own the desktop version of Beat Saber. If you bought the standalone version, follow {link} to claim it."
},
"stay": "Remember me",
"connect-to-meta": "Connect to Meta"
@@ -920,6 +974,14 @@
},
"bs-version-outdated": {
"body": "The version {outdatedVersion} is outdated, and some mods or features may no longer work as expected. Please download the latest recommended version ({recommendedVersion}) of Beat Saber to enjoy the latest features and bugfixes."
},
"custom-launch-option": {
"title": "Custom launch option",
"title-placeholder": "My option",
"desc": "Create a re-usable launch option to launch Beat Saber with a specific command.",
"name": "Name",
"launch-option": "Launch option",
"create": "Create"
}
},
"maps": {
@@ -1249,57 +1311,8 @@
}
},
"dateformat": {
"dayNames": [
"Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sunday",
"Monday",
"Tuesday",
"Wednesday",
"Thursday",
"Friday",
"Saturday"
],
"monthNames": [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"June",
"Jul",
"Aug",
"Sept",
"Oct",
"Nov",
"Dec",
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
],
"timeNames": [
"a",
"p",
"am",
"pm",
"A",
"P",
"AM",
"PM"
]
"dayNames": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"],
"monthNames": ["Jan", "Feb", "Mar", "Apr", "May", "June", "Jul", "Aug", "Sept", "Oct", "Nov", "Dec", "January", "February", "March", "April", "May", "June", "July", "August", "September", "October", "November", "December"],
"timeNames": ["a", "p", "am", "pm", "A", "P", "AM", "PM"]
}
}
+68 -60
View File
@@ -11,7 +11,7 @@
"avatars": "Avatares",
"sabers": "Sables",
"platforms": "Plataformas",
"blocks": "Bloques",
"bloqs": "Bloques",
"cancel": "Cancelar",
"delete": "Borrar",
"accept": "Aceptar",
@@ -26,7 +26,15 @@
"continue": "Continuar",
"shared": "Compartido",
"pin": "Fijar",
"unpin": "Desfijar"
"unpin": "Desfijar",
"this-guide": "esta guía",
"create": "Crear",
"edit": "Editar"
},
"generic": {
"env": {
"parse": "No se pudo analizar correctamente la cadena de la variable de entorno."
}
},
"title-bar": {
"outdated": "obsoleto"
@@ -51,7 +59,8 @@
"outdated-tippy": "Esta versión está desactualizada, y algunos mods o funciones pueden no funcionar como se espera. Es mejor usar la versión recomendada ({recommendedVersion}) de Beat Saber para disfrutar de las últimas características y correcciones.",
"advanced-launch": {
"button": "Opciones de lanzamiento",
"placeholder": "Argumentos adicionales ej: --revert; --no-wait"
"placeholder": "Opciones de lanzamiento ej: KEY=VALUE %command% fpfc",
"create-launch-option": "Crear una opción de lanzamiento"
},
"skipsteam": "Saltar Steam",
"skipsteam-description": "Evita que Steam se abra automáticamente con Beat Saber, habilita esto si estás usando un runtime de VR diferente como WiVRn o Monado que SteamVR podría interferir.",
@@ -126,6 +135,11 @@
"loading-mods": "Cargando mods...",
"no-internet": "Sin internet",
"mods-not-available": "Aún no hay mods disponibles para esta versión de Beat Saber",
"status": {
"no-wineprefix": "No se pudo encontrar la ruta del WINEPREFIX de BSManager. Por favor, inicia Beat Saber en BSManager primero.",
"beatmods-down": "Beatmods no está disponible en este momento. Por favor, inténtalo más tarde. Si el problema persiste, infórmanos en {links}.",
"unknown": "Se ha producido un error desconocido ¯\\_(ツ)_/¯"
},
"buttons": {
"more-infos": "Más información",
"install-or-update": "Instalar o actualizar",
@@ -134,6 +148,7 @@
"mods-grid": {
"header-bar": {
"name": "Nombre",
"size": "Tamaño",
"installed": "Instalado",
"latest": "Último",
"description": "Descripción",
@@ -142,12 +157,24 @@
"uninstall-all": "Desinstalar todos",
"unselect-all": "Deseleccionar todo"
}
},
"mod-item": {
"this-is-a-large-mod": "¡Este es un mod grande!",
"this-is-a-very-large-mod": "¡Este es un mod muy grande!"
}
},
"notifications": {
"all-mods-already-installed": {
"title": "Mods ya instalados",
"description": "Todos los mods seleccionados ya están instalados"
},
"outdated-mods": {
"title": "Mod desactualizado",
"title-plural": "Mods desactualizados",
"description": "El mod {name} está desactualizado. ¿Quieres actualizarlo?",
"description-plural": "Esta versión tiene {nb} mods desactualizados. ¿Quieres actualizarlos?",
"dont-remind-me": "No me lo recuerdes",
"update": "Actualizar"
}
},
"drop-zone": {
@@ -227,11 +254,14 @@
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"pt-BR": "Português (Brasil)",
"ru-RU": "Русский",
"zh-CN": "简体中文",
"zh-TW": "正體中文",
"ja-JP": "日本語",
"ko-KR": "한국어",
"uk-UA": "Українська",
"tl-PH": "Filipino",
"translated": {
"en-EN": "Inglés, Reino Unido",
"en-US": "Inglés, Estados Unidos",
@@ -239,11 +269,14 @@
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Alemán",
"pt-BR": "Portugués (Brasil)",
"ru-RU": "Ruso",
"zh-CN": "Chino simplificado",
"zh-TW": "Chino tradicional",
"ja-JP": "Japonés",
"ko-KR": "Coreano"
"ko-KR": "Coreano",
"uk-UA": "Ucraniano",
"tl-PH": "Filipino"
}
}
},
@@ -382,17 +415,20 @@
"oculus-download": {
"errors": {
"msg": {
"DOWNLOAD_MANIFEST_FAILED": "No se puede descargar el manifiesto de esta versión; su token de inicio de sesión podría ser inválido.",
"DOWNLOAD_MANIFEST_FAILED": "No se puede descargar el manifiesto de esta versión. Su token de inicio de sesión podría ser inválido o que no poseas la versión de escritorio de Beat Saber.",
"MANIFEST_FILE_NOT_FOUND": "Imposible encontrar el manifiesto de esta versión.",
"PARSE_MANIFEST_FILE_FAILED": "Se produjo un error al leer el manifiesto.",
"ALREADY_DOWNLOADING": "Ya se está descargando una versión.",
"UNABLE_TO_GET_MANIFEST": "Imposible obtener el manifiesto necesario para la descarga.",
"UNABLE_TO_GET_MANIFEST": "Imposible obtener el manifiesto necesario para la descarga. Su token de inicio de sesión podría ser inválido o que no poseas la versión de escritorio de Beat Saber.",
"VERIFY_INTEGRITY_FAILED": "Se produjo un error durante la verificación de archivos.",
"SOME_FILES_FAILED_TO_DOWNLOAD": "Algunos archivos no pudieron ser descargados.",
"META_LOGIN_TIMED_OUT": "El token de inicio de sesión tardó demasiado en recuperarse.",
"META_LOGIN_WINDOW_CLOSED_BY_USER": "La ventana de inicio de sesión de Meta fue cerrada.",
"NO_META_AUTH_TOKEN": "Imposible recuperar el token de inicio de sesión de Meta necesario para la descarga.",
"UNKNOWN_ERROR": "Se produjo un error desconocido."
},
"actions": {
"claim-your-desktop-version": "Reclama tu versión de escritorio"
}
}
}
@@ -573,7 +609,8 @@
"msg": {
"success": "Todos los mods han sido desinstalados.",
"errors": {
"no-mods": "No hay mods instalados en esta versión 😑"
"no-mods": "No hay mods instalados en esta versión 😑",
"no-wineprefix": "No se pudo encontrar la ruta del WINEPREFIX de BSManager. Por favor, inicia Beat Saber en BSManager primero."
}
}
},
@@ -641,6 +678,7 @@
"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.",
"LinkingNotSupported": "El enlace de carpetas no es compatible con este sistema de archivos.",
"UNKNOWN_ERROR": "Se ha producido un error desconocido al enlazar la carpeta."
}
},
@@ -666,6 +704,16 @@
"do-not-remind": "No volver a recordármelo",
"ok": "Ok"
}
},
"bs-uninstall": {
"errors": {
"title": "No se puede desinstalar la versión",
"desc": {
"CantDeleteOfficialVersion": "BSManager no puede desinstalar la versión original de Beat Saber",
"VersionNotFound": "Versión no encontrada.",
"delete-folder": "No se puede eliminar la versión. Asegúrate de que Beat Saber no esté en marcha."
}
}
}
},
"modals": {
@@ -870,7 +918,8 @@
"token-needed": "Tu token de conexión a Meta es necesario para descargar Beat Saber.",
"need-cookie-enabled": "Al conectarte a Meta, se abrirá una ventana de inicio de sesión y entonces podrás empezar el proceso de conexión. Asegúrate de aceptar las cookies, de lo contrario, podríamos no poder recuperar tu token para iniciar la descarga.",
"enter-token-manually": "Ingresar mi token de inicio de sesión manualmente",
"enter-token-manually-tooltip": "Esto te permitirá ingresar tu token de inicio de sesión sin pasar por el inicio de sesión de Meta."
"enter-token-manually-tooltip": "Esto te permitirá ingresar tu token de inicio de sesión sin pasar por el inicio de sesión de Meta.",
"must-own-desktop-version": "Debes poseer la versión de escritorio de Beat Saber. Si compraste la versión autónoma, sigue {link} para reclamarla."
},
"stay": "Recuérdame",
"connect-to-meta": "Conectarse a Meta"
@@ -925,6 +974,14 @@
},
"bs-version-outdated": {
"body": "La versión {outdatedVersion} está obsoleta, y algunos mods o funciones pueden no funcionar como se espera. Por favor, descarga la última versión recomendada ({recommendedVersion}) de Beat Saber para disfrutar de las últimas funciones y correcciones de errores."
},
"custom-launch-option": {
"title": "Opción de lanzamiento personalizada",
"title-placeholder": "Mi opción",
"desc": "Crea una opción de lanzamiento reutilizable para lanzar Beat Saber con un comando específico.",
"name": "Nombre",
"launch-option": "Opción de lanzamiento",
"create": "Crear"
}
},
"maps": {
@@ -1254,57 +1311,8 @@
}
},
"dateformat": {
"dayNames": [
"Dom",
"Lun",
"Mar",
"Mié",
"Jue",
"Vie",
"Sáb",
"Domingo",
"Lunes",
"Martes",
"Miércoles",
"Jueves",
"Viernes",
"Sábado"
],
"monthNames": [
"Ene",
"Feb",
"Mar",
"Abr",
"May",
"Jun",
"Jul",
"Ago",
"Sep",
"Oct",
"Nov",
"Dic",
"Enero",
"Febrero",
"Marzo",
"Abril",
"Mayo",
"Junio",
"Julio",
"Agosto",
"Septiembre",
"Octubre",
"Noviembre",
"Diciembre"
],
"timeNames": [
"a",
"p",
"am",
"pm",
"A",
"P",
"AM",
"PM"
]
"dayNames": ["Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb", "Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"],
"monthNames": ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic", "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", "Noviembre", "Diciembre"],
"timeNames": ["a", "p", "am", "pm", "A", "P", "AM", "PM"]
}
}
+79 -24
View File
@@ -11,7 +11,7 @@
"avatars": "Avatars",
"sabers": "Sabres",
"platforms": "Plateformes",
"blocks": "Blocs",
"bloqs": "Blocs",
"cancel": "Annuler",
"delete": "Supprimer",
"accept": "Accepter",
@@ -26,7 +26,15 @@
"continue": "Continuer",
"shared": "Partagé",
"pin": "Épingler",
"unpin": "Désépingler"
"unpin": "Désépingler",
"this-guide": "ce guide",
"create": "Créer",
"edit": "Modifier"
},
"generic": {
"env": {
"parse": "Impossible d'analyser correctement la chaîne de variable d'environnement."
}
},
"title-bar": {
"outdated": "obsolète"
@@ -51,7 +59,8 @@
"outdated-tippy": "Cette version est obsolète et certains mods ou fonctionnalités peuvent ne plus fonctionner comme prévu. Préférez utiliser la version recommandée ({recommendedVersion}) de Beat Saber pour profiter des dernières fonctionnalités et correctifs.",
"advanced-launch": {
"button": "Options de lancement",
"placeholder": "Arguments supplémentaires ex: --revert; --nowait"
"placeholder": "Options de lancement ex: KEY=VALUE %command% fpfc",
"create-launch-option": "Créer une option de lancement"
},
"skipsteam": "Ignorer Steam",
"skipsteam-description": "Empêche Steam de s'ouvrir automatiquement avec Beat Saber, activez-le si vous utilisez un autre runtime VR comme WiVRn ou Monado avec lequel SteamVR pourrait interférer.",
@@ -126,6 +135,11 @@
"loading-mods": "Chargement des mods...",
"no-internet": "Pas d'accès Internet",
"mods-not-available": "Aucun mod n'est encore disponible pour cette version de Beat Saber",
"status": {
"no-wineprefix": "Impossible de trouver le chemin WINEPREFIX de BSManager. Veuillez d'abord lancer Beat Saber dans BSManager.",
"beatmods-down": "Beatmods est actuellement inaccessible. Veuillez réessayer plus tard. Si le problème persiste, informez-nous sur {links}.",
"unknown": "Une erreur inconnue s'est produite ¯\\_(ツ)_/¯"
},
"buttons": {
"more-infos": "Plus d'infos",
"install-or-update": "Installer ou mettre à jour",
@@ -134,6 +148,7 @@
"mods-grid": {
"header-bar": {
"name": "Nom",
"size": "Taille",
"installed": "Installé",
"latest": "Récent",
"description": "Description",
@@ -142,12 +157,24 @@
"uninstall-all": "Tout désinstaller",
"unselect-all": "Tout désélectionner"
}
},
"mod-item": {
"this-is-a-large-mod": "C'est un mod volumineux !",
"this-is-a-very-large-mod": "C'est un mod très volumineux !"
}
},
"notifications": {
"all-mods-already-installed": {
"title": "Mods déjà installées",
"description": "Tous les mods séléctionnées sont déjà installées"
},
"outdated-mods": {
"title": "Mod obsolète",
"title-plural": "Mods obsolètes",
"description": "Le mod {name} est obsolète. Voulez-vous le mettre à jour ?",
"description-plural": "Cette version contient {nb} mods obsolètes. Voulez-vous les mettre à jour ?",
"dont-remind-me": "Ne plus me rappeler",
"update": "Mettre à jour"
}
},
"drop-zone": {
@@ -227,23 +254,29 @@
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"pt-BR": "Português (Brasil)",
"ru-RU": "Русский",
"zh-CN": "简体中文",
"zh-TW": "正體中文",
"ja-JP": "日本語",
"ko-KR": "한국어",
"uk-UA": "Українська",
"tl-PH": "Filipino",
"translated": {
"en-EN": "Anglais, Royaume-Uni",
"en-US": "Anglais, États-Unis",
"fr-FR": "Français",
"es-ES": "Espagnol",
"pt-BR": "Portugais (Brésil)",
"it-IT": "Italien",
"de-DE": "Allemand",
"ru-RU": "Russe",
"zh-CN": "Chinois simplifié",
"zh-TW": "Chinois traditionnel",
"ja-JP": "Japonais",
"ko-KR": "Coréen"
"ko-KR": "Coréen",
"uk-UA": "Ukrainien",
"tl-PH": "Philippin"
}
}
},
@@ -382,17 +415,20 @@
"oculus-download": {
"errors": {
"msg": {
"DOWNLOAD_MANIFEST_FAILED": "Impossible de télécharger le manifest de cette version; vote token de connexion est peut-être invalide.",
"DOWNLOAD_MANIFEST_FAILED": "Impossible de télécharger le manifest de cette version. Votre token de connexion est peut-être invalide, ou vous ne possédez pas la version PC de Beat Saber.",
"MANIFEST_FILE_NOT_FOUND": "Impossible de trouver le manifest de cette version.",
"PARSE_MANIFEST_FILE_FAILED": "Une erreur c'est produite lors de la lecture du manifest.",
"ALREADY_DOWNLOADING": "Une version est déjà en cours de téléchargement.",
"UNABLE_TO_GET_MANIFEST": "Impossible d'obtenir le manifest nécessaire au téléchargement.",
"UNABLE_TO_GET_MANIFEST": "Impossible d'obtenir le manifest nécessaire au téléchargement. Votre token de connexion est peut-être invalide, ou vous ne possédez pas la version PC de Beat Saber.",
"VERIFY_INTEGRITY_FAILED": "Une erreur c'est produite lors de la vérification des fichiers.",
"SOME_FILES_FAILED_TO_DOWNLOAD": "Certains fichiers n'ont pas pu être téléchargés.",
"META_LOGIN_TIMED_OUT": "Le Token de connexion à mis trop de temps à être récupéré.",
"META_LOGIN_WINDOW_CLOSED_BY_USER": "La fenêtre de connexion à Meta a été fermée.",
"NO_META_AUTH_TOKEN": "Impossible de récupérer le token de connexion à Meta nécessaire au téléchargement.",
"UNKNOWN_ERROR": "Une erreur inconnue s'est produite."
},
"actions": {
"claim-your-desktop-version": "Réclamez votre version PC"
}
}
}
@@ -573,7 +609,8 @@
"msg": {
"success": "Tous les mods ont été désinstallés.",
"errors": {
"no-mods": "Aucun mod n'est installé dans dans cette version 😑"
"no-mods": "Aucun mod n'est installé dans dans cette version 😑",
"no-wineprefix": "Impossible de trouver le chemin WINEPREFIX de BSManager. Veuillez d'abord lancer Beat Saber dans BSManager."
}
}
},
@@ -641,6 +678,7 @@
"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.",
"LinkingNotSupported": "La liaison de dossier n'est pas supportée sur ce système de fichiers.",
"UNKNOWN_ERROR": "Une erreur inconnue est survenue lors de la liaison du dossier."
}
},
@@ -648,7 +686,6 @@
"title": "L'ajout du dossier partagé a échoué",
"msg": "Vous ne pouvez pas ajouter \"{folder}\" aux dossiers partagés."
}
},
"create-launch-shortcut": {
"success": {
@@ -667,6 +704,16 @@
"do-not-remind": "Ne plus me rappeler",
"ok": "Ok"
}
},
"bs-uninstall": {
"errors": {
"title": "Impossible de désinstaller la version",
"desc": {
"CantDeleteOfficialVersion": "BSManager ne peut pas désinstaller la version originale de Beat Saber",
"VersionNotFound": "La version n'a pas été trouvée.",
"delete-folder": "Impossible de supprimer la version. Vérifiez que Beat Saber n'est pas en cours d'exécution."
}
}
}
},
"modals": {
@@ -871,7 +918,8 @@
"token-needed": "Votre token de connexion à Meta est nécessaire pour télécharger Beat Saber.",
"need-cookie-enabled": "En vous connectant à Meta, une fenêtre de connexion s'ouvrira et vous pourrez alors débuter le processus de connexion. Veillez à bien accepter les cookies sinon quoi, il se pourrait que l'on n'arrive pas à récupérer votre token pour démarrer le téléchargement.",
"enter-token-manually": "Entrer mon token de connexion manuellement",
"enter-token-manually-tooltip": "Cela vous permettra d'entrer votre token de connexion sans passer par la connexion Meta."
"enter-token-manually-tooltip": "Cela vous permettra d'entrer votre token de connexion sans passer par la connexion Meta.",
"must-own-desktop-version": "Vous devez posséder la version PC de Beat Saber. Si vous avez acheté la version autonome, suivez {link} pour la réclamer."
},
"stay": "Se souvenir de moi",
"connect-to-meta": "Se connecter à Meta"
@@ -926,12 +974,20 @@
},
"bs-version-outdated": {
"body": "La version {outdatedVersion} est obsolète, et certains mods ou fonctionnalités peuvent ne plus fonctionner comme prévu. Veuillez télécharger la dernière version recommandée ({recommendedVersion}) de Beat Saber pour profiter des dernières fonctionnalités et corrections de bugs."
},
"custom-launch-option": {
"title": "Option de lancement personnalisée",
"title-placeholder": "Mon option",
"desc": "Créer une option de lancement réutilisable pour lancer Beat Saber avec une commande spécifique.",
"name": "Nom",
"launch-option": "Option de lancement",
"create": "Créer"
}
},
"maps": {
"map-filter-panel": {
"duration": "Durée",
"nps" : "Notes Par Seconde",
"nps": "Notes Par Seconde",
"njs": "Vitesse de saut des notes",
"tags": "tags",
"specificities": "général",
@@ -1012,12 +1068,12 @@
"by": "Par {songAutor}",
"mapped-by": "mappée par",
"delete": "Supprimer",
"preview" : "Aperçu de la carte",
"bsr-code" : "Code BSR",
"download" : "Télécharger la carte",
"downloading" :"Téléchargement de la carte",
"cancel-download" : "Annuler le téléchargement",
"hightlight-difficulty" : "Surligner la difficulté"
"preview": "Aperçu de la carte",
"bsr-code": "Code BSR",
"download": "Télécharger la carte",
"downloading": "Téléchargement de la carte",
"cancel-download": "Annuler le téléchargement",
"hightlight-difficulty": "Surligner la difficulté"
}
},
"models": {
@@ -1194,15 +1250,15 @@
"no-playlists-found": "Aucune playlists trouvées",
"error-occur-while-loading-playlists": "Une erreur est survenue lors du chargement des playlists",
"error-occur-while-loading-playlist": "Une erreur est survenue lors du chargement de la playlist",
"loading-maps":"Chargement des maps...",
"no-maps-found-for-playlist":"Aucune map trouvée pour cette playlist",
"playlist-contain-no-maps":"La playlist ne contient aucune map",
"no-map-installed-for-playlist":"Aucune map installée pour cette playlist",
"playlist-is-waiting-to-download":"La playlist est en attente de téléchargment",
"loading-maps": "Chargement des maps...",
"no-maps-found-for-playlist": "Aucune map trouvée pour cette playlist",
"playlist-contain-no-maps": "La playlist ne contient aucune map",
"no-map-installed-for-playlist": "Aucune map installée pour cette playlist",
"playlist-is-waiting-to-download": "La playlist est en attente de téléchargment",
"download-maps": "Télécharger les maps",
"download-missing-maps": "Télécharger les maps manquantes",
"playlist-is-downloading":"La playlist est en cours de téléchargement",
"some-playlist-maps-are-missing":"Certaines maps de cette playlist sont manquantes",
"playlist-is-downloading": "La playlist est en cours de téléchargement",
"some-playlist-maps-are-missing": "Certaines maps de cette playlist sont manquantes",
"create-a-playlist": "Créer une playlist",
"synchronize-playlists": "Synchroniser les playlists",
"export-playlists": "Exporter les playlists",
@@ -1253,7 +1309,6 @@
"CANNOT_PARSE_PLAYLIST": "Les playlists ne sont pas lisibles",
"unknown": "Aucune playlist n'a pu être importée"
}
},
"dateformat": {
"dayNames": ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"],
+129 -116
View File
@@ -11,7 +11,7 @@
"avatars": "Avatar",
"sabers": "Spade",
"platforms": "Piattaforme",
"blocks": "Blocchi",
"bloqs": "Blocchi",
"cancel": "Cancella",
"delete": "Elimina",
"accept": "Accetta",
@@ -26,7 +26,15 @@
"continue": "Continua",
"shared": "Condivisi",
"pin": "Fissa",
"unpin": "Rimuovi Fissaggio"
"unpin": "Rimuovi Fissaggio",
"this-guide": "questa guida",
"create": "Crea",
"edit": "Modifica"
},
"generic": {
"env": {
"parse": "Impossibile analizzare correttamente la stringa della variabile d'ambiente."
}
},
"title-bar": {
"outdated": "obsoleta"
@@ -51,7 +59,8 @@
"outdated-tippy": "Questa versione è obsoleta, e alcune mod o funzioni non potrebbero più funzionare correttamente. Consigliamo di usare la versione raccomandata ({recommendedVersion}) di Beat Saber per godere delle ultime funzioni e bugfix.",
"advanced-launch": {
"button": "Opzioni di Lancio",
"placeholder": "Argomenti aggiuntivi es: --revert; --nowait"
"placeholder": "Opzioni di Lancio es: KEY=VALUE %command% fpfc",
"create-launch-option": "Crea un'opzione di lancio"
},
"skipsteam": "Salta Steam",
"skipsteam-description": "Ferma Steam da aprirsi automaticamente con Beat Saber, abilitalo se stai usando un VR runtime differente come WiVRn o Monado con cui SteamVR potrebbe interferire.",
@@ -126,6 +135,11 @@
"loading-mods": "Caricamento delle mod in corso...",
"no-internet": "Internet non disponibile",
"mods-not-available": "Nessuna mod è ancora disponibile per questa versione di Beat Saber",
"status": {
"no-wineprefix": "Impossibile trovare il percorso del WINEPREFIX di BSManager. Per favore avvia prima Beat Saber in BSManager.",
"beatmods-down": "Beatmods non è attualmente raggiungibile. Riprova più tardi. Se il problema persiste, informaci su {links}.",
"unknown": "È accaduto un errore sconosciuto ¯\\_(ツ)_/¯"
},
"buttons": {
"more-infos": "Più info",
"install-or-update": "Installa o aggiorna",
@@ -134,6 +148,7 @@
"mods-grid": {
"header-bar": {
"name": "Nome",
"size": "Dimensione",
"installed": "Installata",
"latest": "Ultima Versione",
"description": "Descrizione",
@@ -142,12 +157,24 @@
"uninstall-all": "Disinstalla tutto",
"unselect-all": "Deseleziona tutto"
}
},
"mod-item": {
"this-is-a-large-mod": "Questo è un mod grande!",
"this-is-a-very-large-mod": "Questo è un mod molto grande!"
}
},
"notifications": {
"all-mods-already-installed": {
"title": "Mod già installata",
"description": "Tutte le mod selezionate sono già installate"
},
"outdated-mods": {
"title": "Mod obsoleto",
"title-plural": "Mod obsoleti",
"description": "Il mod {name} è obsoleto. Vuoi aggiornarlo?",
"description-plural": "Questa versione ha {nb} mod obsoleti. Vuoi aggiornarli?",
"dont-remind-me": "Non ricordarmelo",
"update": "Aggiorna"
}
},
"drop-zone": {
@@ -227,11 +254,14 @@
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"pt-BR": "Português (Brasil)",
"ru-RU": "Русский",
"zh-CN": "简体中文",
"zh-TW": "正體中文",
"ja-JP": "日本語",
"ko-KR": "한국어",
"uk-UA": "Українська",
"tl-PH": "Filipino",
"translated": {
"en-EN": "Inglese, Regno Unito",
"en-US": "Inglese, Stati Uniti d'America",
@@ -239,11 +269,14 @@
"es-ES": "Spagnolo",
"it-IT": "Italiano",
"de-DE": "Tedesco",
"pt-BR": "Portoghese (Brasile)",
"ru-RU": "Russo",
"zh-CN": "Cinese (Semplificato)",
"zh-TW": "Cinese (Tradizionale)",
"ja-JP": "Giapponese",
"ko-KR": "Coreano"
"ko-KR": "Coreano",
"uk-UA": "Ucraino",
"tl-PH": "Filippino"
}
}
},
@@ -382,17 +415,20 @@
"oculus-download": {
"errors": {
"msg": {
"DOWNLOAD_MANIFEST_FAILED": "Impossibile scaricare il manifest per questa versione; il tuo token di accesso potrebbe essere invalido.",
"DOWNLOAD_MANIFEST_FAILED": "Impossibile scaricare il manifesto. Il tuo token di accesso potrebbe non essere valido o potresti non possedere la versione desktop di Beat Saber.",
"MANIFEST_FILE_NOT_FOUND": "Impossibile trovare il manifest per questa versione.",
"PARSE_MANIFEST_FILE_FAILED": "C'è stato un errore durante la lettura del manifest.",
"ALREADY_DOWNLOADING": "Una versione sta già venendo scaricata.",
"UNABLE_TO_GET_MANIFEST": "Impossibile ottenere il manifest richiesto per il download.",
"UNABLE_TO_GET_MANIFEST": "Impossibile ottenere il manifesto necessario per il download. Il tuo token di accesso potrebbe non essere valido o potresti non possedere la versione desktop di Beat Saber.",
"VERIFY_INTEGRITY_FAILED": "C'è stato un errore durante la verifica dei file.",
"SOME_FILES_FAILED_TO_DOWNLOAD": "Alcuni file non sono stati scaricati con successo.",
"META_LOGIN_TIMED_OUT": "Il token di accesso ha impiegato troppo tempo per essere recuperato.",
"META_LOGIN_WINDOW_CLOSED_BY_USER": "La finestra di login di Meta è stata chiusa.",
"NO_META_AUTH_TOKEN": "Impossibile recuperare il token di accesso Meta necessario per il download.",
"UNKNOWN_ERROR": "C'è stato un errore sconosciuto."
},
"actions": {
"claim-your-desktop-version": "Richiedi la tua versione desktop"
}
}
}
@@ -573,7 +609,8 @@
"msg": {
"success": "Tutte le mod sono state installate.",
"errors": {
"no-mods": "Non ci sono mod installate su questa versione 😑"
"no-mods": "Non ci sono mod installate su questa versione 😑",
"no-wineprefix": "Impossibile trovare il percorso del WINEPREFIX di BSManager. Per favore avvia prima Beat Saber in BSManager."
}
}
},
@@ -641,6 +678,7 @@
"EPERM": "BSManager non ha i permessi necessari per condividere la cartella.",
"EACCES": "BSManager non ha i permessi necessari per condividere la cartella.",
"ENOSPC": "Il disco è pieno, libera un pò di spazio e riprova.",
"LinkingNotSupported": "Il collegamento delle cartelle non è supportato su questo file system.",
"UNKNOWN_ERROR": "C'è stato un errore sconosciuto durante la condivisione della cartella."
}
},
@@ -666,6 +704,16 @@
"do-not-remind": "Non ricordarmelo più",
"ok": "Ok"
}
},
"bs-uninstall": {
"errors": {
"title": "Impossibile disinstallare la versione",
"desc": {
"CantDeleteOfficialVersion": "BSManager non può disinstallare la versione originale di Beat Saber",
"VersionNotFound": "Versione non trovata.",
"delete-folder": "Impossibile eliminare la versione. Assicurati che Beat Saber non sia in esecuzione."
}
}
}
},
"modals": {
@@ -802,9 +850,14 @@
"multiple": "Queste mappe saranno rimosse anche dalle versioni che usano mappe condivise"
}
}
},
"delete-duplicate-maps": {
"title": "Eliminare le mappe?",
"desc": "Solo la mappa \"{map}\" è un duplicato. Sei sicuro di volerla eliminare?",
"desc-plural": "Sono state trovate {nb} mappe duplicate. Sei sicuro di volerle eliminare?"
}
},
"link-contents":{
"link-contents": {
"title": "Collega {contentType}",
"p-1": "Collegare {contentType} consente di condividere {contentType} tra tutte le versioni con questa funzione attiva. Quando collegate, questa versione avrà il beneficio della condivisione di {contentType}.",
"p-2": "Per usufruire della funzione di collegamento, tutti i tuoi {itemsHtml} saranno spostati nella cartella {sharedHtml}. Successivamente, un collegamento simbolico che punta alla cartella condivisa {contentType} sostituirà la tua cartella originale {contentType}.",
@@ -813,7 +866,7 @@
"i-need-help": "Mi serve aiuto",
"valid-btn": "Collega {contentType}"
},
"unlink-contents":{
"unlink-contents": {
"title": "Scollega {contentType}",
"p-1": "Attenzione, scollegare {contentType} disabiliterà l'uso di {contentType} condivisi per questa versione, fino a quando non verrà riabilitato.",
"p-2": "Per scollegare le tue mappe, il collegamento simbolico che punta alla {sharedHtml} verrà rimosso. Dopodiché, tutte le mappe nella cartella condivisa verranno copiate nella cartella {itemsHtml} della tua versione di Beat Saber.",
@@ -862,65 +915,74 @@
"connect-to-meta": {
"title": "Connettiti a Meta",
"body": {
"token-needed": "Il tuo token di connessione Meta è necessario per scaricare Beat Saber.",
"need-cookie-enabled": "Accedendo a Meta, si aprirà una finestra di login e potrai quindi avviare il processo di login. Assicurati di accettare i cookie, altrimenti potremmo non essere in grado di recuperare il tuo token per avviare il download.",
"enter-token-manually": "Inserisci manualmente il mio token di login",
"enter-token-manually-tooltip": "Questo ti permetterà di inserire il tuo token di accesso senza passare per il login di Meta."
"token-needed": "Il tuo token di connessione Meta è necessario per scaricare Beat Saber.",
"need-cookie-enabled": "Accedendo a Meta, si aprirà una finestra di login e potrai quindi avviare il processo di login. Assicurati di accettare i cookie, altrimenti potremmo non essere in grado di recuperare il tuo token per avviare il download.",
"enter-token-manually": "Inserisci manualmente il mio token di login",
"enter-token-manually-tooltip": "Questo ti permetterà di inserire il tuo token di accesso senza passare per il login di Meta.",
"must-own-desktop-version": "Devi possedere la versione desktop di Beat Saber. Se hai acquistato la versione autonoma, segui {link} per richiederla."
},
"stay": "Ricordami",
"connect-to-meta": "Connettiti a Meta"
},
"stay": "Ricordami",
"connect-to-meta": "Connettiti a Meta"
},
"enable-oculus-sideloaded-apps": {
"title": "Abilita Sideloading",
"info-1": "Per avviare Beat Saber, è necessario abilitare la possibilità di eseguire app sideloaded. BSManager richiederà i diritti di amministratore per abilitare automaticamente questa funzione.",
"info-2": "La funzione di sideloading consente di avviare giochi situati al di fuori della cartella della libreria Oculus.",
"info-3": "Dopo che il sideloading è stato attivato, la funzione rimarrà attiva e non ti verrà più chiesto di abilitarla.",
"i-want-to-do-it-myself": "Voglio farlo da solo",
"understood": "Ho capito"
},
"enter-meta-token": {
"title": "Token Oculus",
"body": {
"info-enter-token": "Per scaricare Beat Saber, è richiesto il tuo token di accesso Oculus.",
"how-obtain-token": "Come posso ottenere il mio token Oculus?",
"oculus-token": "Token Oculus",
"token-is-invalid": "Il token non è valido.",
"save-my-token": "Salva il mio token",
"have-token-saved": "Ho già un token salvato",
"save-token-info": "Questo salverà il tuo token per un uso più facile. Dovrai creare una password per criptare il tuo token a scopo di archiviazione. Se dimentichi la tua password, dovrai semplicemente fornire di nuovo il tuo token.",
"password": "Password",
"password-too-short": "Password troppo corta",
"info-enter-password": "Per scaricare Beat Saber, è richiesto il tuo token di accesso Oculus. Inserisci la password usata per salvare il tuo token Oculus.",
"info-disabled-btn-password": "Il token decriptato con la password fornita non è valido. Assicurati che la password sia la stessa usata per salvare il token.",
"enter-oculus-token": "Inserisci un token Oculus"
"title": "Abilita Sideloading",
"info-1": "Per avviare Beat Saber, è necessario abilitare la possibilità di eseguire app sideloaded. BSManager richiederà i diritti di amministratore per abilitare automaticamente questa funzione.",
"info-2": "La funzione di sideloading consente di avviare giochi situati al di fuori della cartella della libreria Oculus.",
"info-3": "Dopo che il sideloading è stato attivato, la funzione rimarrà attiva e non ti verrà più chiesto di abilitarla.",
"i-want-to-do-it-myself": "Voglio farlo da solo",
"understood": "Ho capito"
},
"valid-btn": "Valida"
},
"launch-as-admin": {
"title": "Permessi di Amministratore",
"body": {
"info": "Steam è stato rilevato come in esecuzione con permessi di amministratore. Per comunicare con Steam, Beat Saber deve essere avviato anche come amministratore. Altrimenti, Beat Saber potrebbe riscontrare problemi e chiudersi dopo l'avvio.",
"info-2": "Avviare Beat Saber in modalità amministratore darà anche diritti di amministratore alle mod installate. Pertanto, è consigliato riavviare Steam senza permessi di amministratore.",
"info-3": "Si sconsiglia di dare diritti di amministratore a Steam, poiché questo influisce sui giochi e le mod installate, presentando un rischio per la sicurezza."
"enter-meta-token": {
"title": "Token Oculus",
"body": {
"info-enter-token": "Per scaricare Beat Saber, è richiesto il tuo token di accesso Oculus.",
"how-obtain-token": "Come posso ottenere il mio token Oculus?",
"oculus-token": "Token Oculus",
"token-is-invalid": "Il token non è valido.",
"save-my-token": "Salva il mio token",
"have-token-saved": "Ho già un token salvato",
"save-token-info": "Questo salverà il tuo token per un uso più facile. Dovrai creare una password per criptare il tuo token a scopo di archiviazione. Se dimentichi la tua password, dovrai semplicemente fornire di nuovo il tuo token.",
"password": "Password",
"password-too-short": "Password troppo corta",
"info-enter-password": "Per scaricare Beat Saber, è richiesto il tuo token di accesso Oculus. Inserisci la password usata per salvare il tuo token Oculus.",
"info-disabled-btn-password": "Il token decriptato con la password fornita non è valido. Assicurati che la password sia la stessa usata per salvare il token.",
"enter-oculus-token": "Inserisci un token Oculus"
},
"valid-btn": "Valida"
},
"launch-as-admin": "Avvia come Amministratore",
"not-remind-me": "Non ricordarmelo"
},
"ask-install-path": {
"title": "Cartella di installazione",
"choose-folder-description": "Scegli la cartella che conterrà tutti i contenuti scaricati da BSManager. (versioni, mod, mappe, playlist, ecc.)",
"default": "Predefinito",
"default-tooltip": "Predefinito nella tua cartella home"
},
"choose-proton-folder": {
"title": "Cartella di Proton",
"proton-folder-description": "Su Linux, BSManager richiede Proton per funzionare. Scegli la cartella di installazione di Proton per continuare.",
"proton-folder-placeholder": "Cartella di installazione di Proton",
"where-is-proton-installed": "Dove è installato Proton?"
},
"bs-version-outdated": {
"body": "La versione {outdatedVersion} è obsoleta, e alcune mod o funzioni potrebbero non funzionare più come previsto. Per favore scarica l'ultima versione raccomandata ({recommendedVersion}) di Beat Saber per godere delle ultime funzioni e bugfix."
}
"launch-as-admin": {
"title": "Permessi di Amministratore",
"body": {
"info": "Steam è stato rilevato come in esecuzione con permessi di amministratore. Per comunicare con Steam, Beat Saber deve essere avviato anche come amministratore. Altrimenti, Beat Saber potrebbe riscontrare problemi e chiudersi dopo l'avvio.",
"info-2": "Avviare Beat Saber in modalità amministratore darà anche diritti di amministratore alle mod installate. Pertanto, è consigliato riavviare Steam senza permessi di amministratore.",
"info-3": "Si sconsiglia di dare diritti di amministratore a Steam, poiché questo influisce sui giochi e le mod installate, presentando un rischio per la sicurezza."
},
"launch-as-admin": "Avvia come Amministratore",
"not-remind-me": "Non ricordarmelo"
},
"ask-install-path": {
"title": "Cartella di installazione",
"choose-folder-description": "Scegli la cartella che conterrà tutti i contenuti scaricati da BSManager. (versioni, mod, mappe, playlist, ecc.)",
"default": "Predefinito",
"default-tooltip": "Predefinito nella tua cartella home"
},
"choose-proton-folder": {
"title": "Cartella di Proton",
"proton-folder-description": "Su Linux, BSManager richiede Proton per funzionare. Scegli la cartella di installazione di Proton per continuare.",
"proton-folder-placeholder": "Cartella di installazione di Proton",
"where-is-proton-installed": "Dove è installato Proton?"
},
"bs-version-outdated": {
"body": "La versione {outdatedVersion} è obsoleta, e alcune mod o funzioni potrebbero non funzionare più come previsto. Per favore scarica l'ultima versione raccomandata ({recommendedVersion}) di Beat Saber per godere delle ultime funzioni e bugfix."
},
"custom-launch-option": {
"title": "Opzione di lancio personalizzata",
"title-placeholder": "La mia opzione",
"desc": "Crea un'opzione di lancio rilocabile per lanciare Beat Saber con un comando specifico.",
"name": "Nome",
"launch-option": "Opzione di lancio",
"create": "Crea"
}
},
"maps": {
"map-filter-panel": {
@@ -1250,57 +1312,8 @@
}
},
"dateformat": {
"dayNames": [
"Dom",
"Lun",
"Mar",
"Mer",
"Gio",
"Ven",
"Sab",
"Domenica",
"Lunedì",
"Martedì",
"Mercoledì",
"Giovedì",
"Venerdì",
"Sabato"
],
"monthNames": [
"Gen",
"Feb",
"Mar",
"Apr",
"Mag",
"Giu",
"Lug",
"Ago",
"Set",
"Ott",
"Nov",
"Dic",
"Gennaio",
"Febbraio",
"Marzo",
"Aprile",
"Maggio",
"Giugno",
"Luglio",
"Augosto",
"Settembre",
"Ottobre",
"Novembre",
"Dicembre"
],
"timeNames": [
"a",
"p",
"am",
"pm",
"A",
"P",
"AM",
"PM"
]
"dayNames": ["Dom", "Lun", "Mar", "Mer", "Gio", "Ven", "Sab", "Domenica", "Lunedì", "Martedì", "Mercoledì", "Giovedì", "Venerdì", "Sabato"],
"monthNames": ["Gen", "Feb", "Mar", "Apr", "Mag", "Giu", "Lug", "Ago", "Set", "Ott", "Nov", "Dic", "Gennaio", "Febbraio", "Marzo", "Aprile", "Maggio", "Giugno", "Luglio", "Augosto", "Settembre", "Ottobre", "Novembre", "Dicembre"],
"timeNames": ["a", "p", "am", "pm", "A", "P", "AM", "PM"]
}
}
+71 -14
View File
@@ -11,7 +11,7 @@
"avatars": "アバター",
"sabers": "セイバー",
"platforms": "プラットフォーム",
"blocks": "ブロック",
"bloqs": "ブロック",
"cancel": "キャンセル",
"delete": "削除",
"accept": "同意する",
@@ -26,7 +26,15 @@
"continue": "続ける",
"shared": "共有",
"pin": "固定",
"unpin": "固定解除"
"unpin": "固定解除",
"this-guide": "このガイド",
"create": "作成",
"edit": "編集"
},
"generic": {
"env": {
"parse": "環境変数の文字列を正しく解析できませんでした。"
}
},
"title-bar": {
"outdated": "時代遅れ"
@@ -51,7 +59,8 @@
"outdated-tippy": "このバージョンは古いため、一部のMODや機能が期待通りに動作しない可能性があります。最新の機能やバグ修正を楽しむには、推奨バージョン ({recommendedVersion}) のBeat Saberを使用することをお勧めします。",
"advanced-launch": {
"button": "起動オプション",
"placeholder": "追加引数 例:--revert; --nowait"
"placeholder": "起動オプション 例:KEY=VALUE %command% fpfc",
"create-launch-option": "起動オプションを作成"
},
"skipsteam": "Steamをスキップ",
"skipsteam-description": "Beat Saberと一緒にSteamが自動的に開くのを防ぎます。SteamVRが干渉する可能性のあるWiVRnやMonadoなど、別のVRランタイムを使用している場合は有効にしてください。",
@@ -126,6 +135,11 @@
"loading-mods": "Modを読み込み中...",
"no-internet": "インターネット接続がありません!",
"mods-not-available": "このバージョンで使用できるMODはまだありません。",
"status": {
"no-wineprefix": "BSManager WINEPREFIXのパスが見つかりませんでした。まずBSManagerでBeat Saberを起動してください。",
"beatmods-down": "現在、Beatmodsにアクセスできません。後でもう一度お試しください。問題が解決しない場合は、{links}でお知らせください。",
"unknown": "不明なエラーが発生しました (´・ω・`)"
},
"buttons": {
"more-infos": "詳細情報",
"install-or-update": "インストールとアップデート",
@@ -134,6 +148,7 @@
"mods-grid": {
"header-bar": {
"name": "名前",
"size": "サイズ",
"installed": "インストール済み",
"latest": "最新",
"description": "說明",
@@ -142,12 +157,24 @@
"uninstall-all": "全てアンインストールする",
"unselect-all": "すべて選択解除"
}
},
"mod-item": {
"this-is-a-large-mod": "これは大きなModです!",
"this-is-a-very-large-mod": "これは非常に大きなModです!"
}
},
"notifications": {
"all-mods-already-installed": {
"title": "すでにインストール済みのMOD",
"description": "選択したすべてのMODはすでにインストールされています"
},
"outdated-mods": {
"title": "古いMod",
"title-plural": "古いMod",
"description": "{name} Modが古くなっています。更新しますか?",
"description-plural": "このバージョンには古いModが{nb}個あります。更新しますか?",
"dont-remind-me": "もう知らせない",
"update": "更新する"
}
},
"drop-zone": {
@@ -227,11 +254,14 @@
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"pt-BR": "Português (Brasil)",
"ru-RU": "Русский",
"zh-CN": "简体中文",
"zh-TW": "正體中文",
"ja-JP": "日本語",
"ko-KR": "한국어",
"uk-UA": "Українська",
"tl-PH": "Filipino",
"translated": {
"en-EN": "イギリス英語",
"en-US": "アメリカ英語",
@@ -239,11 +269,14 @@
"es-ES": "スペイン語",
"it-IT": "イタリア語",
"de-DE": "ドイツ語",
"pt-BR": "ポルトガル語 (ブラジル)",
"ru-RU": "ロシア語",
"zh-CN": "簡体字中国",
"zh-TW": "繁体字中国",
"ja-JP": "日本語",
"ko-KR": "韓国語"
"ko-KR": "韓国語",
"uk-UA": "ウクライナ語",
"tl-PH": "フィリピン人"
}
}
},
@@ -382,17 +415,20 @@
"oculus-download": {
"errors": {
"msg": {
"DOWNLOAD_MANIFEST_FAILED": "このバージョンのマニフェストをダウンロードできません。ログイントークンが無効可能性があります。",
"DOWNLOAD_MANIFEST_FAILED": "このバージョンのマニフェストをダウンロードできません。ログイントークンが無効であるか、Beat Saberのデスクトップ版を所有していない可能性があります。",
"MANIFEST_FILE_NOT_FOUND": "このバージョンのマニフェストが見つかりません。",
"PARSE_MANIFEST_FILE_FAILED": "マニフェストの読み取り中にエラーが発生しました。",
"ALREADY_DOWNLOADING": "バージョンはすでにダウンロードされています。",
"UNABLE_TO_GET_MANIFEST": "ダウンロードに必要なマニフェストを取得できません。",
"UNABLE_TO_GET_MANIFEST": "ダウンロードに必要なマニフェストを取得できません。ログイントークンが無効であるか、Beat Saberのデスクトップ版を所有していない可能性があります。",
"VERIFY_INTEGRITY_FAILED": "ファイルの検証中にエラーが発生しました。",
"SOME_FILES_FAILED_TO_DOWNLOAD": "一部のファイルのダウンロードに失敗しました。",
"META_LOGIN_TIMED_OUT": "ログイントークンの取得に時間がかかりすぎました。",
"META_LOGIN_WINDOW_CLOSED_BY_USER": "メタ・ログイン・ウィンドウが閉じられた。",
"NO_META_AUTH_TOKEN": "ダウンロードに必要な Meta ログイン トークンを取得できません。",
"UNKNOWN_ERROR": "不明なエラーが発生しました。"
},
"actions": {
"claim-your-desktop-version": "デスクトップ版を取得"
}
}
}
@@ -573,7 +609,8 @@
"msg": {
"success": "すべてのModをアンインストールしました。",
"errors": {
"no-mods": "このバージョンのModはインストールされていません 😑"
"no-mods": "このバージョンのModはインストールされていません 😑",
"no-wineprefix": "BSManager WINEPREFIXのパスが見つかりませんでした。まずBSManagerでBeat Saberを起動してください。"
}
}
},
@@ -641,6 +678,7 @@
"EPERM": "BSManagerにはフォルダをリンクするための必要な権限がありません。",
"EACCES": "BSManagerにはフォルダをリンクするための必要な権限がありません。",
"ENOSPC": "ディスクがいっぱいです。空き容量を作ってもう一度試してください。",
"LinkingNotSupported": "このファイルシステムではフォルダのリンクはサポートされていません。",
"UNKNOWN_ERROR": "フォルダをリンク中に不明なエラーが発生しました。"
}
},
@@ -666,6 +704,16 @@
"do-not-remind": "再度通知しない",
"ok": "OK"
}
},
"bs-uninstall": {
"errors": {
"title": "バージョンをアンインストールできません",
"desc": {
"CantDeleteOfficialVersion": "BSManagerはBeat Saberのオリジナルバージョンをアンインストールできません",
"VersionNotFound": "バージョンが見つかりません。",
"delete-folder": "バージョンを削除できません。Beat Saberが実行中でないことを確認してください。"
}
}
}
},
"modals": {
@@ -870,7 +918,8 @@
"token-needed": "Beat SaberのダウンロードにはMetaのログイントークンが必要です。",
"need-cookie-enabled": "Metaにログインするとログインウィンドウが開き、ログインプロセスを開始することができます。Cookieを受け入れるようにしてください。そうしないとダウンロードを開始するためのトークンを取得できない場合があります。",
"enter-token-manually": "ログイントークンを手動で入力する",
"enter-token-manually-tooltip": "これにより、Metaログインを経由せずにログイントークンを入力できるようになります。"
"enter-token-manually-tooltip": "これにより、Metaログインを経由せずにログイントークンを入力できるようになります。",
"must-own-desktop-version": "Beat Saberのデスクトップ版を所有している必要があります。スタンドアロン版を購入した場合は、{link} を確認して取得してください。"
},
"stay": "記憶する",
"connect-to-meta": "Metaに接続する"
@@ -925,12 +974,20 @@
},
"bs-version-outdated": {
"body": "バージョン {outdatedVersion} は古いため、一部のモッドや機能が期待通りに動作しない場合があります。最新の機能やバグ修正を楽しむために、推奨される最新バージョンの Beat Saber ({recommendedVersion}) をダウンロードしてください。"
},
"custom-launch-option": {
"title": "カスタム起動オプション",
"title-placeholder": "私のオプション",
"desc": "特定のコマンドでBeat Saberを起動するための再利用可能な起動オプションを作成します。",
"name": "名前",
"launch-option": "起動オプション",
"create": "作成"
}
},
"maps": {
"map-filter-panel": {
"duration": "尺",
"nps" : "秒間ノート数",
"nps": "秒間ノート数",
"njs": "ノートジャンプ速度",
"tags": "タグ",
"specificities": "一般",
@@ -1011,11 +1068,11 @@
"by": "By {songAutor}",
"mapped-by": "作成者 ",
"delete": "削除",
"preview" : "マッププレビュー",
"bsr-code" : "BSRコード",
"download" : "マップをダウンロード",
"downloading" :"マップをダウンロード中",
"cancel-download" : "ダウンロードをキャンセル",
"preview": "マッププレビュー",
"bsr-code": "BSRコード",
"download": "マップをダウンロード",
"downloading": "マップをダウンロード中",
"cancel-download": "ダウンロードをキャンセル",
"hightlight-difficulty": "難易度をハイライト"
}
},
+68 -60
View File
@@ -11,7 +11,7 @@
"avatars": "아바타",
"sabers": "세이버",
"platforms": "플랫폼",
"blocks": "블록",
"bloqs": "블록",
"cancel": "취소",
"delete": "삭제",
"accept": "동의",
@@ -26,7 +26,15 @@
"continue": "계속",
"shared": "공유",
"pin": "고정",
"unpin": "고정 해제"
"unpin": "고정 해제",
"this-guide": "이 가이드",
"create": "만들기",
"edit": "수정"
},
"generic": {
"env": {
"parse": "환경 변수 문자열을 제대로 파싱할 수 없습니다."
}
},
"title-bar": {
"outdated": "구식"
@@ -51,7 +59,8 @@
"outdated-tippy": "이 버전은 오래되어 일부 모드나 기능이 예상대로 작동하지 않을 수 있습니다. 최신 기능과 버그 수정을 사용하려면 권장 버전({recommendedVersion})의 Beat Saber를 사용하는 것이 좋습니다.",
"advanced-launch": {
"button": "실행 옵션",
"placeholder": "추가 인수 예: --revert; --nowait"
"placeholder": "실행 옵션 예: KEY=VALUE %command% fpfc",
"create-launch-option": "실행 옵션 만들기"
},
"skipsteam": "Steam 실행 건너뛰기",
"skipsteam-description": "Beat Saber와 함께 Steam이 자동으로 열리는 것을 방지합니다. SteamVR이 간섭할 수 있는 WiVRn 또는 Monado와 같은 다른 VR 런타임을 사용하는 경우 활성화하세요.",
@@ -126,6 +135,11 @@
"loading-mods": "모드를 불러오는 중...",
"no-internet": "인터넷 연결이 없습니다!",
"mods-not-available": "이 버전에서 사용할 수 있는 모드가 아직 없습니다.",
"status": {
"no-wineprefix": "BSManager WINEPREFIX 경로를 찾을 수 없습니다. 먼저 BSManager에서 Beat Saber를 실행하세요.",
"beatmods-down": "현재 Beatmods에 접근할 수 없습니다. 나중에 다시 시도해 주세요. 문제가 계속되면 {links}에서 알려주세요.",
"unknown": "알 수 없는 오류가 발생했습니다 (´・ω・`)"
},
"buttons": {
"more-infos": "자세히 보기",
"install-or-update": "설치 및 업데이트",
@@ -134,6 +148,7 @@
"mods-grid": {
"header-bar": {
"name": "이름",
"size": "크기",
"installed": "설치됨",
"latest": "최신",
"description": "설명",
@@ -142,12 +157,24 @@
"uninstall-all": "모두 제거",
"unselect-all": "모두 선택 해제"
}
},
"mod-item": {
"this-is-a-large-mod": "이것은 큰 모드입니다!",
"this-is-a-very-large-mod": "이것은 매우 큰 모드입니다!"
}
},
"notifications": {
"all-mods-already-installed": {
"title": "이미 설치된 모드",
"description": "선택한 모든 모드가 이미 설치되었습니다."
},
"outdated-mods": {
"title": "구식 모드",
"title-plural": "구식 모드",
"description": "{name} 모드가 오래되었습니다. 업데이트하시겠습니까?",
"description-plural": "이 버전에는 {nb}개의 구식 모드가 있습니다. 업데이트하시겠습니까?",
"dont-remind-me": "다시는 알리지 않기",
"update": "업데이트"
}
},
"drop-zone": {
@@ -227,11 +254,14 @@
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"pt-BR": "Português (Brasil)",
"ru-RU": "Русский",
"zh-CN": "简体中文",
"zh-TW": "正體中文",
"ja-JP": "日本語",
"ko-KR": "한국어",
"uk-UA": "Українська",
"tl-PH": "Filipino",
"translated": {
"en-EN": "영어(영국)",
"en-US": "영어(미국)",
@@ -239,11 +269,14 @@
"es-ES": "스페인어",
"it-IT": "이탈리아어",
"de-DE": "독일어",
"pt-BR": "포르투갈 인 (브라질)",
"ru-RU": "러시아어",
"zh-CN": "중국어 (간체)",
"zh-TW": "중국어 (번체)",
"ja-JP": "일본어",
"ko-KR": "한국어"
"ko-KR": "한국어",
"uk-UA": "우크라이나어",
"tl-PH": "필리핀인"
}
}
},
@@ -382,17 +415,20 @@
"oculus-download": {
"errors": {
"msg": {
"DOWNLOAD_MANIFEST_FAILED": "이 버전의 매니페스트를 다운로드할 수 없습니다. 로그인 토큰이 무효일 수 있습니다.",
"DOWNLOAD_MANIFEST_FAILED": "매니페스트를 다운로드할 수 없습니다. 로그인 토큰이 유효하지 않거나 Beat Saber 데스크톱 버전을 소유하고 있지 않을 수 있습니다.",
"MANIFEST_FILE_NOT_FOUND": "이 버전의 매니페스트를 찾을 수 없습니다.",
"PARSE_MANIFEST_FILE_FAILED": "매니페스트를 읽는 중 오류가 발생했습니다.",
"ALREADY_DOWNLOADING": "버전이 이미 다운로드 중입니다.",
"UNABLE_TO_GET_MANIFEST": "다운로드에 필요한 매니페스트를 가져올 수 없습니다.",
"UNABLE_TO_GET_MANIFEST": "다운로드에 필요한 매니페스트를 가져올 수 없습니다. 로그인 토큰이 유효하지 않거나 Beat Saber 데스크톱 버전을 소유하고 있지 않을 수 있습니다.",
"VERIFY_INTEGRITY_FAILED": "파일 검증 중 오류가 발생했습니다.",
"SOME_FILES_FAILED_TO_DOWNLOAD": "일부 파일을 다운로드하지 못했습니다.",
"META_LOGIN_TIMED_OUT": "로그인 토큰을 얻는 데 시간이 너무 오래 걸렸습니다.",
"META_LOGIN_WINDOW_CLOSED_BY_USER": "Meta 로그인 창이 닫혔습니다.",
"NO_META_AUTH_TOKEN": "다운로드에 필요한 Meta 로그인 토큰을 얻을 수 없습니다.",
"UNKNOWN_ERROR": "알 수 없는 오류가 발생했습니다."
},
"actions": {
"claim-your-desktop-version": "데스크톱 버전 받기"
}
}
}
@@ -573,7 +609,8 @@
"msg": {
"success": "모든 Mod가 제거되었습니다.",
"errors": {
"no-mods": "이 버전에 설치된 Mod가 없습니다 😑"
"no-mods": "이 버전에 설치된 Mod가 없습니다 😑",
"no-wineprefix": "BSManager WINEPREFIX 경로를 찾을 수 없습니다. 먼저 BSManager에서 Beat Saber를 실행하세요."
}
}
},
@@ -641,6 +678,7 @@
"EPERM": "BSManager에 폴더를 연결하기 위한 권한이 부족합니다.",
"EACCES": "BSManager에 폴더를 연결하기 위한 권한이 부족합니다.",
"ENOSPC": "디스크가 가득 찼습니다. 공간을 확보하고 다시 시도하세요.",
"LinkingNotSupported": "이 파일 시스템에서는 폴더 연결이 지원되지 않습니다.",
"UNKNOWN_ERROR": "폴더를 연결하는 중 알 수 없는 오류가 발생했습니다."
}
},
@@ -666,6 +704,16 @@
"do-not-remind": "다시 알리지 않기",
"ok": "확인"
}
},
"bs-uninstall": {
"errors": {
"title": "버전을 제거할 수 없습니다",
"desc": {
"CantDeleteOfficialVersion": "BSManager는 원본 Beat Saber 버전을 제거할 수 없습니다",
"VersionNotFound": "버전을 찾을 수 없습니다.",
"delete-folder": "버전을 삭제할 수 없습니다. Beat Saber가 실행 중이지 않은지 확인하세요."
}
}
}
},
"modals": {
@@ -870,7 +918,8 @@
"token-needed": "Beat Saber를 다운로드하려면 Meta 로그인 토큰이 필요합니다.",
"need-cookie-enabled": "Meta에 로그인하면 로그인 창이 열리며 로그인 프로세스를 시작할 수 있습니다. 쿠키를 허용해 주세요. 그렇지 않으면 다운로드를 시작하기 위한 토큰을 얻지 못할 수 있습니다.",
"enter-token-manually": "로그인 토큰을 수동으로 입력하기",
"enter-token-manually-tooltip": "이를 통해 Meta 로그인을 거치지 않고 로그인 토큰을 입력할 수 있습니다."
"enter-token-manually-tooltip": "이를 통해 Meta 로그인을 거치지 않고 로그인 토큰을 입력할 수 있습니다.",
"must-own-desktop-version": "Beat Saber의 데스크톱 버전을 보유해야 합니다. 독립형 버전을 구매했다면 {link}를 따라가서 받으세요."
},
"stay": "기억하기",
"connect-to-meta": "Meta에 연결하기"
@@ -925,6 +974,14 @@
},
"bs-version-outdated": {
"body": "버전 {outdatedVersion}은(는) 오래되어 일부 모드나 기능이 예상대로 작동하지 않을 수 있습니다. 최신 기능과 버그 수정 사항을 즐기기 위해 Beat Saber의 최신 권장 버전 ({recommendedVersion})을 다운로드하십시오."
},
"custom-launch-option": {
"title": "사용자 정의 실행 옵션",
"title-placeholder": "내 옵션",
"desc": "특정 명령으로 Beat Saber를 실행하기 위한 재사용 가능한 실행 옵션을 만듭니다.",
"name": "이름",
"launch-option": "실행 옵션",
"create": "만들기"
}
},
"maps": {
@@ -1248,57 +1305,8 @@
}
},
"dateformat": {
"dayNames": [
"일",
"",
"화",
"수",
"목",
"금",
"토",
"일요일",
"월요일",
"화요일",
"수요일",
"목요일",
"금요일",
"토요일"
],
"monthNames": [
"1월",
"2월",
"3월",
"4월",
"5월",
"6월",
"7월",
"8월",
"9월",
"10월",
"11월",
"12월",
"1월",
"2월",
"3월",
"4월",
"5월",
"6월",
"7월",
"8월",
"9월",
"10월",
"11월",
"12월"
],
"timeNames": [
"오전",
"오후",
"오전",
"오후",
"오전",
"오후",
"오전",
"오후"
]
"dayNames": ["일", "월", "화", "수", "목", "금", "토", "일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"],
"monthNames": ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월", "1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
"timeNames": ["오전", "오후", "오전", "오후", "오전", "오후", "오전", "오후"]
}
}
File diff suppressed because it is too large Load Diff
+71 -14
View File
@@ -11,7 +11,7 @@
"avatars": "Аватары",
"sabers": "Сабли",
"platforms": "Платформы",
"blocks": "Блоки",
"bloqs": "Блоки",
"cancel": "Отмена",
"delete": "Удалить",
"accept": "Принять",
@@ -26,7 +26,15 @@
"continue": "Продолжить",
"shared": "Общий",
"pin": "Закрепить",
"unpin": "Открепить"
"unpin": "Открепить",
"this-guide": "это руководство",
"create": "Создать",
"edit": "Редактировать"
},
"generic": {
"env": {
"parse": "Не удалось правильно разобрать строку переменной окружения."
}
},
"title-bar": {
"outdated": "устаревший"
@@ -51,7 +59,8 @@
"outdated-tippy": "Эта версия устарела, и некоторые моды или функции могут работать не так, как ожидалось. Рекомендуется использовать рекомендуемую версию ({recommendedVersion}) Beat Saber, чтобы воспользоваться последними функциями и исправлениями ошибок.",
"advanced-launch": {
"button": "Параметры запуска",
"placeholder": "Параметры запуска, например: --revert; --nowait"
"placeholder": "Параметры запуска, например: KEY=VALUE %command% fpfc",
"create-launch-option": "Создать параметр запуска"
},
"skipsteam": "Пропустить Steam",
"skipsteam-description": "Предотвращает автоматическое открытие Steam с Beat Saber, включите, если вы используете другую VR-среду, такую как WiVRn или Monado, с которой SteamVR может мешать.",
@@ -126,6 +135,11 @@
"loading-mods": "Загрузка модов...",
"no-internet": "Нет интернета",
"mods-not-available": "Не найдены моды для этой версии Beat Saber",
"status": {
"no-wineprefix": "Не удалось найти путь WINEPREFIX BSManager. Пожалуйста, сначала запустите Beat Saber в BSManager.",
"beatmods-down": "Сейчас Beatmods недоступен. Пожалуйста, попробуйте позже. Если проблема сохраняется, сообщите нам в {links}.",
"unknown": "Неизвестная ошибка ¯\\_(ツ)_/¯"
},
"buttons": {
"more-infos": "Подробнее",
"install-or-update": "Установить или обновить",
@@ -134,6 +148,7 @@
"mods-grid": {
"header-bar": {
"name": "Имя",
"size": "Размер",
"installed": "Установлено",
"latest": "Latest",
"description": "Описание",
@@ -142,12 +157,24 @@
"uninstall-all": "Удалить всё",
"unselect-all": "Снять все выделения"
}
},
"mod-item": {
"this-is-a-large-mod": "Это большой мод!",
"this-is-a-very-large-mod": "Это очень большой мод!"
}
},
"notifications": {
"all-mods-already-installed": {
"title": "Моды уже установлены",
"description": "Все выбранные моды уже установлены"
},
"outdated-mods": {
"title": "Устаревший мод",
"title-plural": "Устаревшие моды",
"description": "Мод {name} устарел. Хотите его обновить?",
"description-plural": "В этой версии есть {nb} устаревших модов. Хотите их обновить?",
"dont-remind-me": "Не напоминать мне",
"update": "Обновить"
}
},
"drop-zone": {
@@ -227,11 +254,14 @@
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"pt-BR": "Português (Brasil)",
"ru-RU": "Русский",
"zh-CN": "简体中文",
"zh-TW": "正體中文",
"ja-JP": "日本語",
"ko-KR": "한국어",
"uk-UA": "Українська",
"tl-PH": "Filipino",
"translated": {
"en-EN": "Английский, Великобритания",
"en-US": "Английский, США",
@@ -239,11 +269,14 @@
"es-ES": "Испанский",
"it-IT": "Итальянец",
"de-DE": "Немецкий",
"pt-BR": "португальский (Бразилия)",
"ru-RU": "Русский",
"zh-CN": "Китайский (упрощенный)",
"zh-TW": "Китайский (традиционный)",
"ja-JP": "Японский",
"ko-KR": "Корейский"
"ko-KR": "Корейский",
"uk-UA": "Украинский",
"tl-PH": "Филиппинец"
}
}
},
@@ -382,17 +415,20 @@
"oculus-download": {
"errors": {
"msg": {
"DOWNLOAD_MANIFEST_FAILED": "Невозможно загрузить манифест этой версии; ваш токен входа, возможно, недействителен.",
"DOWNLOAD_MANIFEST_FAILED": "Невозможно загрузить манифест этой версии. Ваш токен входа, возможно, недействителен, или у вас нет настольной версии Beat Saber.",
"MANIFEST_FILE_NOT_FOUND": "Невозможно найти манифест этой версии.",
"PARSE_MANIFEST_FILE_FAILED": "При чтении манифеста произошла ошибка.",
"ALREADY_DOWNLOADING": "Версия уже загружается.",
"UNABLE_TO_GET_MANIFEST": "Невозможно получить манифест, необходимый для загрузки.",
"UNABLE_TO_GET_MANIFEST": "Невозможно получить манифест, необходимый для загрузки. Ваш токен входа, возможно, недействителен, или у вас нет настольной версии Beat Saber.",
"VERIFY_INTEGRITY_FAILED": "При проверке файлов произошла ошибка.",
"SOME_FILES_FAILED_TO_DOWNLOAD": "Некоторые файлы не удалось загрузить.",
"META_LOGIN_TIMED_OUT": "Получение токена входа заняло слишком много времени.",
"META_LOGIN_WINDOW_CLOSED_BY_USER": "Окно входа в Meta было закрыто.",
"NO_META_AUTH_TOKEN": "Невозможно получить токен входа в Meta, необходимый для загрузки.",
"UNKNOWN_ERROR": "Произошла неизвестная ошибка."
},
"actions": {
"claim-your-desktop-version": "Получить настольную версию"
}
}
}
@@ -573,7 +609,8 @@
"msg": {
"success": "Все моды были удалены.",
"errors": {
"no-mods": "Для этой версии не было установлено модов 😑"
"no-mods": "Для этой версии не было установлено модов 😑",
"no-wineprefix": "Не удалось найти путь WINEPREFIX BSManager. Пожалуйста, сначала запустите Beat Saber в BSManager."
}
}
},
@@ -641,6 +678,7 @@
"EPERM": "BSManager не имеет необходимых прав для связывания папки.",
"EACCES": "BSManager не имеет необходимых прав для связывания папки.",
"ENOSPC": "Диск заполнен, освободите место и попробуйте снова.",
"LinkingNotSupported": "Ссылки на папки не поддерживаются в этой файловой системе.",
"UNKNOWN_ERROR": "Произошла неизвестная ошибка при связывании папки."
}
},
@@ -666,6 +704,16 @@
"do-not-remind": "Больше не напоминать",
"ok": "Ок"
}
},
"bs-uninstall": {
"errors": {
"title": "Невозможно удалить версию",
"desc": {
"CantDeleteOfficialVersion": "BSManager не может удалить оригинальную версию Beat Saber",
"VersionNotFound": "Версия не найдена.",
"delete-folder": "Не удалось удалить версию. Убедитесь, что Beat Saber не запущен."
}
}
}
},
"modals": {
@@ -870,7 +918,8 @@
"token-needed": "Для скачивания Beat Saber необходим ваш токен подключения к Meta.",
"need-cookie-enabled": "При входе в Meta откроется окно входа, и вы сможете начать процесс входа. Пожалуйста, убедитесь, что вы приняли cookies, иначе мы можем не суметь получить ваш токен для начала загрузки.",
"enter-token-manually": "Ввести мой токен входа вручную",
"enter-token-manually-tooltip": "Это позволит вам ввести ваш токен входа без прохождения через вход в Meta."
"enter-token-manually-tooltip": "Это позволит вам ввести ваш токен входа без прохождения через вход в Meta.",
"must-own-desktop-version": "Вы должны владеть настольной версией Beat Saber. Если вы купили автономную версию, следуйте {link}, чтобы получить её."
},
"stay": "Запомнить меня",
"connect-to-meta": "Подключиться к Meta"
@@ -924,12 +973,20 @@
},
"bs-version-outdated": {
"body": "Версия {outdatedVersion} устарела, и некоторые моды или функции могут работать некорректно. Пожалуйста, скачайте последнюю рекомендуемую версию ({recommendedVersion}) Beat Saber, чтобы воспользоваться новейшими функциями и исправлениями ошибок."
},
"custom-launch-option": {
"title": "Опция запуска",
"title-placeholder": "Моя опция",
"desc": "Создайте повторно используемую опцию запуска для запуска Beat Saber с конкретной командой.",
"name": "Имя",
"launch-option": "Опция запуска",
"create": "Создать"
}
},
"maps": {
"map-filter-panel": {
"duration": "Длительность",
"nps" : "Нот в Секунду",
"nps": "Нот в Секунду",
"njs": "Скорость прыжка нот",
"tags": "тэги",
"specificities": "основное",
@@ -1010,11 +1067,11 @@
"by": "От {songAutor}",
"mapped-by": "автор карты ",
"delete": "Удалить",
"preview" : "Предпросмотр карты",
"bsr-code" : "Код BSR",
"download" : "Скачать карту",
"downloading" :"Загрузка карты",
"cancel-download" : "Отменить загрузку",
"preview": "Предпросмотр карты",
"bsr-code": "Код BSR",
"download": "Скачать карту",
"downloading": "Загрузка карты",
"cancel-download": "Отменить загрузку",
"hightlight-difficulty": "Выделить сложность"
}
},
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+65 -8
View File
@@ -11,7 +11,7 @@
"avatars": "頭像",
"sabers": "光劍",
"platforms": "平台",
"blocks": "方塊",
"bloqs": "方塊",
"cancel": "取消",
"delete": "刪除",
"accept": "接受",
@@ -26,7 +26,15 @@
"continue": "繼續",
"shared": "共享",
"pin": "固定",
"unpin": "取消固定"
"unpin": "取消固定",
"this-guide": "本指南",
"create": "新增",
"edit": "編輯"
},
"generic": {
"env": {
"parse": "無法正確解析環境變數字串。"
}
},
"title-bar": {
"outdated": "過時"
@@ -51,7 +59,8 @@
"outdated-tippy": "此版本已過時,某些模組或功能可能無法按預期運作。建議使用推薦版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修復。",
"advanced-launch": {
"button": "啟動選項",
"placeholder": "額外啟動參數,例如: --revert; --nowait"
"placeholder": "啟動選項,例如: KEY=VALUE %command% fpfc",
"create-launch-option": "新增啟動選項"
},
"skipsteam": "跳過 Steam",
"skipsteam-description": "防止 Steam 與 Beat Saber 自動打開,如果您使用的是其他 VR 執行時,如 WiVRn 或 Monado,SteamVR 可能會干擾,請啟用此選項。",
@@ -126,6 +135,11 @@
"loading-mods": "Mod 載入中...",
"no-internet": "無網路",
"mods-not-available": "該版本 BeatSaber 暫無可用 Mod",
"status": {
"no-wineprefix": "找不到 BSManager WINEPREFIX 路徑。請先在 BSManager 中啟動 Beat Saber。",
"beatmods-down": "Beatmods 目前無法訪問。請稍後再試。如果問題持續,請在 {links} 上告訴我們。",
"unknown": "發生了一個錯誤 ¯\\_(ツ)_/¯"
},
"buttons": {
"more-infos": "更多資訊",
"install-or-update": "安裝或更新",
@@ -134,6 +148,7 @@
"mods-grid": {
"header-bar": {
"name": "名稱",
"size": "大小",
"installed": "已安裝",
"latest": "最新",
"description": "描述",
@@ -142,12 +157,24 @@
"uninstall-all": "全部移除",
"unselect-all": "取消全選"
}
},
"mod-item": {
"this-is-a-large-mod": "這是一個大型Mod",
"this-is-a-very-large-mod": "這是一個非常大型的Mod"
}
},
"notifications": {
"all-mods-already-installed": {
"title": "模組已安裝",
"description": "所有選中的模組已經安裝"
},
"outdated-mods": {
"title": "過時的模組",
"title-plural": "過時的模組",
"description": "{name} 模組已過時。您想更新嗎?",
"description-plural": "此版本有 {nb} 個過時的模組。您想更新它們嗎?",
"dont-remind-me": "不要提醒我",
"update": "更新"
}
},
"drop-zone": {
@@ -227,11 +254,14 @@
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"pt-BR": "Português (Brasil)",
"ru-RU": "Русский",
"zh-CN": "簡體中文",
"zh-TW": "正體中文",
"ja-JP": "日本語",
"ko-KR": "한국어",
"uk-UA": "Українська",
"tl-PH": "Filipino",
"translated": {
"en-EN": "英式英語",
"en-US": "美式英語",
@@ -239,11 +269,14 @@
"es-ES": "西班牙語",
"it-IT": "義大利語",
"de-DE": "德語",
"pt-BR": "葡萄牙語 (巴西)",
"ru-RU": "俄語",
"zh-CN": "簡體中文",
"zh-TW": "正體中文",
"ja-JP": "日語",
"ko-KR": "韓語"
"ko-KR": "韓語",
"uk-UA": "烏克蘭",
"tl-PH": "菲律賓人"
}
}
},
@@ -382,17 +415,20 @@
"oculus-download": {
"errors": {
"msg": {
"DOWNLOAD_MANIFEST_FAILED": "無法下載此版本的清單;你的登令牌可能無效。",
"DOWNLOAD_MANIFEST_FAILED": "無法下載清單。您的登令牌可能無效,或者您未擁有 Beat Saber 的桌面版本。",
"MANIFEST_FILE_NOT_FOUND": "找不到此版本的清單。",
"PARSE_MANIFEST_FILE_FAILED": "讀取清單時發生錯誤。",
"ALREADY_DOWNLOADING": "已經有一個版本在下載中。",
"UNABLE_TO_GET_MANIFEST": "無法獲取下載所需的清單。",
"UNABLE_TO_GET_MANIFEST": "無法獲取下載所需的清單。您的登入令牌可能無效,或者您未擁有 Beat Saber 的桌面版本。",
"VERIFY_INTEGRITY_FAILED": "文件驗證過程中發生錯誤。",
"SOME_FILES_FAILED_TO_DOWNLOAD": "一些文件下載失敗。",
"META_LOGIN_TIMED_OUT": "獲取登錄令牌用時過長。",
"META_LOGIN_WINDOW_CLOSED_BY_USER": "Meta 登錄窗口被關閉。",
"NO_META_AUTH_TOKEN": "無法獲取下載所需的 Meta 登錄令牌。",
"UNKNOWN_ERROR": "發生未知錯誤。"
},
"actions": {
"claim-your-desktop-version": "領取您的桌面版本"
}
}
}
@@ -573,7 +609,8 @@
"msg": {
"success": "所有 Mod 已被移除。",
"errors": {
"no-mods": "該版本中沒有安裝 Mod 😑"
"no-mods": "該版本中沒有安裝 Mod 😑",
"no-wineprefix": "找不到 BSManager WINEPREFIX 路徑。請先在 BSManager 中啟動 Beat Saber。"
}
}
},
@@ -641,6 +678,7 @@
"EPERM": "BSManager沒有必要的權限來連結文件夾。",
"EACCES": "BSManager沒有必要的權限來連結文件夾。",
"ENOSPC": "磁碟已滿,請騰出空間後再試。",
"LinkingNotSupported": "該檔案系統不支援資料夾連結。",
"UNKNOWN_ERROR": "連結文件夾時發生未知錯誤。"
}
},
@@ -666,6 +704,16 @@
"do-not-remind": "不要再提醒我",
"ok": "確定"
}
},
"bs-uninstall": {
"errors": {
"title": "無法卸載版本",
"desc": {
"CantDeleteOfficialVersion": "BSManager無法卸載Beat Saber的原始版本",
"VersionNotFound": "找不到版本。",
"delete-folder": "無法刪除版本。確保Beat Saber未運行。"
}
}
}
},
"modals": {
@@ -870,7 +918,8 @@
"token-needed": "需要 Meta 登錄令牌用於下載 Beat Saber。",
"need-cookie-enabled": "通過登錄 Meta,將打開一個登錄窗口,然後你可以開始登錄過程。請確保接受 cookie,否則我們可能無法檢索你的令牌以開始下載。",
"enter-token-manually": "手動輸入我的登錄令牌",
"enter-token-manually-tooltip": "這將允許你在不通過 Meta 登錄的情況下輸入你的登錄令牌。"
"enter-token-manually-tooltip": "這將允許你在不通過 Meta 登錄的情況下輸入你的登錄令牌。",
"must-own-desktop-version": "您必須擁有 Beat Saber 的桌面版本。如果您購買了獨立版本,請按照 {link} 領取。"
},
"stay": "記住我",
"connect-to-meta": "連接到 Meta"
@@ -925,6 +974,14 @@
},
"bs-version-outdated": {
"body": "版本 {outdatedVersion} 已過時,某些模組或功能可能無法正常運作。請下載最新推薦的 Beat Saber 版本 ({recommendedVersion}),以享受最新功能和修正。"
},
"custom-launch-option": {
"title": "自訂啟動選項",
"title-placeholder": "我的選項",
"desc": "建立一個可重用的啟動選項,以特定命令啟動 Beat Saber。",
"name": "名稱",
"launch-option": "啟動選項",
"create": "建立"
}
},
"maps": {
+72 -15
View File
@@ -11,7 +11,7 @@
"avatars": "头像",
"sabers": "光剑",
"platforms": "平台",
"blocks": "方块",
"bloqs": "方块",
"cancel": "取消",
"delete": "删除",
"accept": "接受",
@@ -21,12 +21,20 @@
"copied": "已复制!",
"confirm": "确认",
"choose-folder": "选择文件夹",
"Unknown": "发生了一个错误 ¯\\_(ツ)_/¯",
"unknown": "发生了一个错误 ¯\\_(ツ)_/¯",
"warning": "警告",
"continue": "继续",
"shared": "共享",
"pin": "固定",
"unpin": "取消固定"
"unpin": "取消固定",
"this-guide": "本指南",
"create": "创建",
"edit": "编辑"
},
"generic": {
"env": {
"parse": "无法正确解析环境变量字符串。"
}
},
"title-bar": {
"outdated": "过时"
@@ -51,7 +59,8 @@
"outdated-tippy": "此版本已过时,某些模组或功能可能无法按预期运行。建议使用推荐版本的 Beat Saber ({recommendedVersion}),以享受最新功能和修复。",
"advanced-launch": {
"button": "启动选项",
"placeholder": "额外启动参数,例如: --revert; --nowait"
"placeholder": "启动选项,例如: KEY=VALUE %command% fpfc",
"create-launch-option": "创建启动选项"
},
"skipsteam": "跳过 Steam",
"skipsteam-description": "防止 Steam 与 Beat Saber 自动打开,如果您使用的是其他 VR 运行时,如 WiVRn 或 Monado,SteamVR 可能会干扰,请启用此选项。",
@@ -126,6 +135,11 @@
"loading-mods": "Mod 加载中...",
"no-internet": "无网络",
"mods-not-available": "该版本 BeatSaber 暂无可用 Mod",
"status": {
"no-wineprefix": "找不到 BSManager WINEPREFIX 路径。请先在 BSManager 中启动 Beat Saber。",
"beatmods-down": "Beatmods 目前无法访问。请稍后再试。如果问题持续,请在 {links} 上告知我们。",
"unknown": "发生了一个错误 ¯\\_(ツ)_/¯"
},
"buttons": {
"more-infos": "更多信息",
"install-or-update": "安装或更新",
@@ -134,6 +148,7 @@
"mods-grid": {
"header-bar": {
"name": "名称",
"size": "大小",
"installed": "已安装",
"latest": "最新",
"description": "描述",
@@ -142,12 +157,24 @@
"uninstall-all": "全部卸载",
"unselect-all": "取消全选"
}
},
"mod-item": {
"this-is-a-large-mod": "这是一个大型Mod",
"this-is-a-very-large-mod": "这是一个非常大型的Mod"
}
},
"notifications": {
"all-mods-already-installed": {
"title": "模组已安装",
"description": "所有选中的模组已经安装"
},
"outdated-mods": {
"title": "过时的模组",
"title-plural": "过时的模组",
"description": "{name} 模组已过时。您想更新吗?",
"description-plural": "此版本有 {nb} 个过时的模组。您想更新它们吗?",
"dont-remind-me": "不要提醒我",
"update": "更新"
}
},
"drop-zone": {
@@ -227,11 +254,14 @@
"es-ES": "Español",
"it-IT": "Italiano",
"de-DE": "Deutsch",
"pt-BR": "Português (Brasil)",
"ru-RU": "Русский",
"zh-CN": "简体中文",
"zh-TW": "正體中文",
"ja-JP": "日本語",
"ko-KR": "한국어",
"uk-UA": "Українська",
"tl-PH": "Filipino",
"translated": {
"en-EN": "英式英语",
"en-US": "美式英语",
@@ -239,11 +269,14 @@
"es-ES": "西班牙语",
"it-IT": "意大利语",
"de-DE": "德语",
"pt-BR": "葡萄牙语 (巴西)",
"ru-RU": "俄语",
"zh-CN": "简体中文",
"zh-TW": "正体中文",
"ja-JP": "日语",
"ko-KR": "韩语"
"ko-KR": "韩语",
"uk-UA": "烏克蘭",
"tl-PH": "菲律宾人"
}
}
},
@@ -382,17 +415,20 @@
"oculus-download": {
"errors": {
"msg": {
"DOWNLOAD_MANIFEST_FAILED": "无法下载此版本的清单;你的登录令牌可能无效。",
"DOWNLOAD_MANIFEST_FAILED": "无法下载清单。您的登录令牌可能无效,或者您未拥有 Beat Saber 的桌面版本。",
"MANIFEST_FILE_NOT_FOUND": "找不到此版本的清单。",
"PARSE_MANIFEST_FILE_FAILED": "读取清单时发生错误。",
"ALREADY_DOWNLOADING": "已经有一个版本在下载中。",
"UNABLE_TO_GET_MANIFEST": "无法获取下载所需的清单。",
"UNABLE_TO_GET_MANIFEST": "无法获取下载所需的清单。您的登录令牌可能无效,或者您未拥有 Beat Saber 的桌面版本。",
"VERIFY_INTEGRITY_FAILED": "文件验证过程中发生错误。",
"SOME_FILES_FAILED_TO_DOWNLOAD": "一些文件下载失败。",
"META_LOGIN_TIMED_OUT": "获取登录令牌用时过长。",
"META_LOGIN_WINDOW_CLOSED_BY_USER": "Meta 登录窗口被关闭。",
"NO_META_AUTH_TOKEN": "无法获取下载所需的 Meta 登录令牌。",
"UNKNOWN_ERROR": "发生未知错误。"
},
"actions": {
"claim-your-desktop-version": "领取您的桌面版本"
}
}
}
@@ -573,7 +609,8 @@
"msg": {
"success": "所有 Mod 已被卸载。",
"errors": {
"no-mods": "该版本中没有安装 Mod 😑"
"no-mods": "该版本中没有安装 Mod 😑",
"no-wineprefix": "找不到 BSManager WINEPREFIX 路径。请先在 BSManager 中启动 Beat Saber。"
}
}
},
@@ -641,6 +678,7 @@
"EPERM": "BSManager没有必要的权限来链接文件夹。",
"EACCES": "BSManager没有必要的权限来链接文件夹。",
"ENOSPC": "磁盘已满,请腾出空间后再试。",
"LinkingNotSupported": "该文件系统不支持文件夹链接。",
"UNKNOWN_ERROR": "链接文件夹时发生未知错误。"
}
},
@@ -666,6 +704,16 @@
"do-not-remind": "不再提醒",
"ok": "确定"
}
},
"bs-uninstall": {
"errors": {
"title": "无法卸载版本",
"desc": {
"CantDeleteOfficialVersion": "BSManager无法卸载Beat Saber的原始版本",
"VersionNotFound": "未找到版本。",
"delete-folder": "无法删除版本。确保Beat Saber未运行。"
}
}
}
},
"modals": {
@@ -870,7 +918,8 @@
"token-needed": "需要 Meta 登录令牌用于下载 Beat Saber。",
"need-cookie-enabled": "通过登录 Meta,将打开一个登录窗口,然后你可以开始登录过程。请确保接受 cookie,否则我们可能无法检索你的令牌以开始下载。",
"enter-token-manually": "手动输入我的登录令牌",
"enter-token-manually-tooltip": "这将允许你在不通过 Meta 登录的情况下输入你的登录令牌。"
"enter-token-manually-tooltip": "这将允许你在不通过 Meta 登录的情况下输入你的登录令牌。",
"must-own-desktop-version": "您必须拥有 Beat Saber 的桌面版本。如果您购买了独立版本,请按照 {link} 领取。"
},
"stay": "记住我",
"connect-to-meta": "连接到 Meta"
@@ -925,12 +974,20 @@
},
"bs-version-outdated": {
"body": "版本 {outdatedVersion} 已过时,某些模组或功能可能无法正常工作。请下载最新推荐的 Beat Saber 版本 ({recommendedVersion}),以享受最新功能和修复。"
},
"custom-launch-option": {
"title": "自定义启动选项",
"title-placeholder": "我的选项",
"desc": "创建一个可重用的启动选项,以特定命令启动 Beat Saber。",
"name": "名称",
"launch-option": "启动选项",
"create": "创建"
}
},
"maps": {
"map-filter-panel": {
"duration": "时长",
"nps" : "每秒音符数",
"nps": "每秒音符数",
"njs": "音符跳跃速度",
"tags": "标签",
"specificities": "general",
@@ -1011,11 +1068,11 @@
"by": "By {songAutor}",
"mapped-by": "mapped by",
"delete": "删除",
"preview" : "预览地图",
"bsr-code" : "BSR代码",
"download" : "下载地图",
"downloading" :"正在下载地图",
"cancel-download" : "取消下载",
"preview": "预览地图",
"bsr-code": "BSR代码",
"download": "下载地图",
"downloading": "正在下载地图",
"cancel-download": "取消下载",
"hightlight-difficulty": "突出显示难度"
}
},
-4
View File
@@ -1,6 +1,2 @@
# Add write permissions to anybody so that this can be sync with the
# github's bs-versions.json when starting bsmanager
/usr/bin/chmod +002 /opt/BSManager/resources/assets/jsons/bs-versions.json
# https://github.com/electron/electron/issues/42510
/usr/bin/chmod 4755 /opt/BSManager/chrome-sandbox
@@ -1,12 +1,44 @@
## Table of Contents
- [With BSManager](#with-bsmanager)
- [Install playlists](#how-to-install-playlists-with-bsmanager)
- [Import playlists](#how-to-import-playlist-with-bsmanager)
- [Manual Installation](#manual-installation)
- [One-Click Installation](#one-click-installation)
## With BSManager
(Coming with version 1.5.0)
### How to Install Playlists with BSManager
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
- [Official Website](https://www.bsmanager.io)
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
2. Launch BSManager and select the version of *Beat Saber* where you want to install playlists from the list on the left side of the interface.
3. Go to the `Playlist` section by clicking on the __`Maps`__ tab.
4. Click the __`Add`__ button, then select __`Browse playlist`__ to open the list of online playlists.
5. Use the search bar at the top of the interface to filter playlists or sort the list.
6. Download to install it.
7. Wait for the installation to complete. Once it's finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) and enjoy your new playlists!
> [!NOTE]
> You can click on a playlist's image to view the maps it contains and download only the maps you want.
### How to Import Playlist with BSManager
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
- [Official Website](https://www.bsmanager.io)
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
2. Launch BSManager and select the version of *Beat Saber* where you want to install playlists from the list on the left side of the interface.
3. Go to the `playlist` section by clicking on the __`Maps`__ tab.
4. From here, there are two possible methods:
1. Click the __`Add`__ button, then select __`Import a playlist`__ and choose the file to import.
2. Drag and drop your file directly onto the playlist interface in BSManager.
5. Wait for the import to complete. Once finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) and enjoy your new playlists!
> [!Important]
> The file must be in `.bplist` or `.json` format to be imported.
## Manual Installation
@@ -1,18 +1,41 @@
## Table of Contents
- [With BSManager](#with-bsmanager)
- [Install maps](#how-to-install-maps-with-bsmanager)
- [Import maps](#how-to-import-maps-with-bsmanager)
- [Manual Installation](#manual-installation)
- [One-Click Installation](#one-click-installation)
## With BSManager
1. Download and install [BSManager](https://www.bsmanager.io) from [GitHub](https://github.com/Zagrios/bs-manager/releases/latest).
2. Launch [BSManager](https://www.bsmanager.io) and select the Beat Saber version where you want to install maps from the list on the left of the interface.
3. Go to maps by clicking on the __`Maps`__ tab.
4. Click on the __`Add`__ button to access the list of online maps.
5. Find the maps that you want to install from the provided list. You can use the search bar at the top of the interface to filter maps by __`name`__, __`type`__, __`mods`__, __`tags`__, __`notes per second`__, __`duration`__, and order the list by __`latest`__, __`relevance`__, __`rating`__, or __`curated`__.
6. Double-click on the map or simply click on the download button to install the map.
7. Wait for the installation to complete. Once it's finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) to enjoy your new maps!
### How to Install Maps with BSManager
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
- [Official Website](https://www.bsmanager.io)
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
2. Launch BSManager and select the version of *Beat Saber* where you want to install maps from the list on the left side of the interface.
3. Go to the `Maps` section by clicking on the __`Maps`__ tab.
4. Click the __`Add`__ button to access the list of online maps.
5. Use the search bar at the top of the interface to filter maps by __`name`__, __`type`__, __`mods`__, __`tags`__, __`notes per second`__, or __`duration`__, and order the list by __`latest`__, __`relevance`__, __`rating`__, or __`curated`__.
6. Download to install it.
7. Wait for the installation to complete. Once it's finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) to enjoy your new maps!
### How to Import Maps with BSManager
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
- [Official Website](https://www.bsmanager.io)
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
2. Launch BSManager and select the version of *Beat Saber* where you want to install maps from the list on the left side of the interface.
3. Go to the `Maps` section by clicking on the __`Maps`__ tab.
4. From here, there are two possible methods:
1. Click the __`Add`__ button, then select __`Import maps`__ and choose the .zip file to import.
2. Drag and drop your .zip file directly onto the maps interface in BSManager.
5. Wait for the import to complete. Once finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) to enjoy your new maps!
> [!Important]
> The file must be in `.zip` format to be imported.
## Manual Installation
@@ -12,17 +12,20 @@
- [With BSManager](#with-bsmanager)
- [One-Click Installation](#one-click-installation)
# With BSManager
## With BSManager
1. Download and install [BSManager](https://www.bsmanager.io) from [GitHub](https://github.com/Zagrios/bs-manager/releases/latest), [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files), or [Website](https://www.bsmanager.io).
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
- [Official Website](https://www.bsmanager.io)
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
2. Launch [BSManager](https://www.bsmanager.io) and select the Beat Saber version where you want to install models from the list on the left of the interface.
3. Go to models by clicking on the __`Models`__ tab.
4. Click on the __`Add`__ button to access the list of online models.
5. Find the models that you want to install from the provided list. You can use the search bar at the top of the interface to filter models by __`name`__, __`type`__, and order the list by __`date`__, __`name`__, or __`author`__.
6. Double-click on the model or simply click on the download button to install the model.
6. Download to install the model.
7. Wait for the installation to complete. Once it's finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) to enjoy your new models!
# One-Click Installation
## One-Click Installation
1. Launch [BSManager](https://www.bsmanager.io) and make sure that the One-Click installation feature is enabled for models.
2. Go to one of the supported websites and search for the models you want to install.
@@ -1,11 +1,18 @@
## Table of Contents
- [With BSManager](#with-bsmanager)
- [Install mods](#how-to-install-mods-with-bsmanager)
- [Import mods](#how-to-import-mods-with-bsmanager)
- [Manual Installation](#manual-installation)
## With BSManager
1. Download and install [BSManager](https://www.bsmanager.io) from [GitHub](https://github.com/Zagrios/bs-manager/releases/latest), [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files), or the [official website](https://www.bsmanager.io).
### How to Install Mods with BSManager
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
- [Official Website](https://www.bsmanager.io)
2. Launch [BSManager](https://www.bsmanager.io) and select the version of Beat Saber that you want to use from the list on the left side of the interface.
3. Go to mods by clicking on the __`Mods`__ tab.
4. Find the mods that you want to install from the provided list. You can use the search bar at the top of the interface to filter mods by name.
@@ -13,6 +20,22 @@
6. Click __`Install or Update`__ at the bottom-right corner of the interface.
7. Wait for the installation to complete. Once it's finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) to enjoy your new mods!
### How to Import Mods with BSManager
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
- [Official Website](https://www.bsmanager.io)
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
2. Launch BSManager and select the version of *Beat Saber* where you want to install mods from the list on the left side of the interface.
3. Go to the `Mods` tab.
4. From here, there are two possible methods:
1. Click on __`⁝`__ button, then select __`Import mods`__ and choose the .zip or .dll file to import.
2. Drag and drop your .zip or .dll file directly onto the mods interface in BSManager.
5. Wait for the import to complete. Once finished, you can launch Beat Saber directly from [BSManager](https://www.bsmanager.io) to enjoy your new mods!
> [!Important]
> The file must be in `.zip` or `.dll` format to be imported.
## Manual Installation
> [!IMPORTANT]
Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

@@ -15,6 +15,10 @@
4. Choose your launch options.
5. Click on __`Create a Shortcut`__. It should now appear on your desktop.
<div align="center">
<img src="../wiki/Guides/Installation-and-updates/Create-a-shortcut/BsmShortcutInterface.png" alt="SeeTxtFile.png" />
</div>
### On Taskbar
1. Open Notepad and drag & drop the version shortcut you need.
@@ -31,25 +35,12 @@ start <version link>
## How to create a shortcut on Steam
1. In [BSManager](https://www.bsmanager.io), select the version you want to open in Steam from the list on the left side.
2. Click the gear icon at the top-right corner and select __`Create a Shortcut`__. This will create a shortcut on your desktop for launching the game.
3. Open Notepad (or any other text editor) and drag & drop the shortcut file into the text editor. You will see a string of text similar to this:
```yaml
bsmanager://launch/?version=1.29.1&versionIno=3940649674381032
```
4. Copy this text and remove any spaces.
<div align="center">
<img src="../wiki/Guides/Installation-and-updates/Create-a-shortcut/SeeTxtFile.png" alt="SeeTxtFile.png" />
</div>
5. Open Steam and add [BSManager](https://www.bsmanager.io) as a non-Steam game.
6. In your Steam library, right-click on [BSManager](https://www.bsmanager.io), select __`Properties`__, and add the link from step 4 as a launch parameter.
7. Disable "Desktop Game Theatre" and scroll down to enable "Include in VR Library" so you can launch it from your VR headset.
8. Rename this entry to whatever you want and customize the name and the icon of the shortcut if desired.
1. Launch [BSManager](https://www.bsmanager.io) and select your version.
2. Click on the gear icon at the top-right corner of the version.
3. Click on the __`Create a Shortcut`__ button.
4. Choose your launch options.
5. Click on __`Create a Shortcut`__. It should now appear on your desktop.
<div align="center">
<img src="../wiki/Guides/Installation-and-updates/Create-a-shortcut/SteamShortcut.png" alt="SteamShortcut.png" />
<img src="../wiki/Guides/Installation-and-updates/Create-a-shortcut/SteamShortcutCheck.png" alt="SeeTxtFile.png" />
</div>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

@@ -10,7 +10,10 @@
## Install a Version with Steam
1. Download and install [BSManager](https://www.bsmanager.io) from [GitHub](https://github.com/Zagrios/bs-manager/releases/latest), [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files), or the [official website](https://www.bsmanager.io).
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
- [Official Website](https://www.bsmanager.io)
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
2. Open the __`Add Versions`__ interface and select the version you wish to download.
3. Choose __`Steam`__ as your platform.
4. Enter your Steam credentials (ID and password) or scan the QR code using the Steam app on your phone.
@@ -28,15 +31,29 @@
> - Ensure that Beat Saber is in your __Oculus PCVR__ library to enable downgrading.
> - You must run Beat Saber at least once on your computer before attempting to launch retrograde Oculus versions from [BSManager](https://www.bsmanager.io).
1. Download and install [BSManager](https://www.bsmanager.io) from [GitHub](https://github.com/Zagrios/bs-manager/releases/latest), [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files), or the [official website](https://www.bsmanager.io).
1. Download and install [BSManager](https://www.bsmanager.io) from one of the following sources:
- [GitHub](https://github.com/Zagrios/bs-manager/releases/latest)
- [Official Website](https://www.bsmanager.io)
- [Nexus Mods](https://www.nexusmods.com/beatsaber/mods/18?tab=files)
2. Open the __`Add Versions`__ interface and select the version you wish to download.
3. Choose __`Oculus Store`__ as your platform.
4. Follow [this guide](How-to-obtain-your-Oculus-Token) to obtain your Oculus token.
5. Once logged into Oculus, the download will start automatically. Please wait for it to complete.
6. When the download is finished, select the downloaded version and click the __`Launch`__ button to verify that the game launches correctly.
4. Click on __`Connect to Meta`__ or __`Enter my login token manually`__ based on your preferred login method.
### If you selected __`Connect to Meta`__
1. You will be redirected to the Oculus website. Enter your email and follow the instructions to log in.
2. Once logged in, the download will start automatically. Please wait for it to complete.
3. When the download finishes, select the downloaded version and click the __`Launch`__ button to verify that the game launches correctly.
### If you selected __`Enter my login token manually`__
1. Follow [this guide](How-to-obtain-your-Oculus-Token) to obtain your Oculus token.
2. Once the token is entered and validated, the download will start automatically. Please wait for it to complete.
3. When the download finishes, select the downloaded version and click the __`Launch`__ button to verify that the game launches correctly.
<div align="center">
<img src="../wiki/Guides/Installation-and-updates/Install-or-import-a-version/OculusLogin.png" alt="OculusLogin.png" />
<img src="../wiki/Guides/Installation-and-updates/Install-or-import-a-version/OculusLoginToken.png" alt="OculusLoginToken.png" />
</div>
## How to Import a Version
Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

@@ -5,6 +5,7 @@
- [PPA Repository](#ppa-repository)
- [dpkg Install](#dpkg-install)
- [Arch (AUR)](#arch-aur)
- [NixOS/Nix](#nixosnix)
- [Universal (flatpak)](#universal-flatpak)
- [Proton Setup](#proton-setup)
@@ -40,10 +41,10 @@ Download the `.deb` file in the releases and run the following command:
dpkg -i ./bsmanager.deb
```
**NOTE:** When installed using dpkg, BSManager will not automatically update to the latest version. You have to either:
- Download the latest `.deb` file on the Releases page; or
- Install thru PPA repository to automatically update with `sudo apt update & sudo apt upgrade`.
> [!NOTE]
> When installed using dpkg, BSManager will not automatically update to the latest version. You have to either:
> - Download the latest `.deb` file on the Releases page; or
> - Install thru PPA repository to automatically update with `sudo apt update & sudo apt upgrade`.
### Arch (AUR)
@@ -51,6 +52,28 @@ Refer to [bs-manager-git](https://aur.archlinux.org/packages/bs-manager-git).
To install AUR packages, you need to install [yay](https://github.com/Jguer/yay).
### NixOS/Nix
Refer to [bs-manager](https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/bs/bs-manager/package.nix).
Available in nixpkgs on `24.11` and `unstable` branches
> [!NOTE]
> bs-manager will not be automatically updated in the client. You must update your channel or `flake.lock` to get the latest version in nixpkgs.
> Additionally updates to the nix package will lag behind main due to nixpkgs being run by volunteers
#### Installation
BSManager can be installed via your configuration or [home-manager](https://github.com/nix-community/home-manager).
To try out bs-manager without installing, please run:
```bash
nix-shell -p bs-manager --run bs-manager
```
> [!WARNING]
> bs-manager has been untested on non-nixos systems running the nix package manager; if you encounter issues, please contact the nix package maintainer
### Universal (flatpak)
This should work on any linux distribution. You are only required to have `flatpak` installed in your system. If it is not installed, then go to [flatpak](https://flatpak.org/setup/) to look for a guide on how to install it on your distro.
@@ -18,7 +18,7 @@
2. Install the Oculus Rift app on your computer.
> [!NOTE]
>__If you bought Beat Saber from the Quest store, it won't appear in your Rift library by default. To download it with [BSManager](https://www.bsmanager.io), first claim it from its store page.__
>__If you bought Beat Saber from the Quest store, it won't appear in your Rift library by default. To download it with [BSManager](https://www.bsmanager.io), first follow [this guide](https://github.com/Zagrios/bs-manager/wiki/how-to-claim-oculus-desktop-version) claim it from its store page.__
## Step 2 - Open developer tools
Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

@@ -0,0 +1,21 @@
## Table of Contents
- [Step 1 - Install and log into the Oculus Rift app](#step-1---install-and-log-into-the-oculus-rift-app)
- [Step 2 - Claim your desktop version of Beat Saber](#step-2---claim-your-desktop-version-of-beat-saber)
## Step 1 - Install and log into the Oculus Rift app
1. Download the Oculus Rift app setup from the [Meta website](https://www.oculus.com/rift/setup/).
2. Install the Oculus Rift app on your computer.
## Step 2 - Claim your desktop version of Beat Saber
1. Open the Oculus app.
2. Go to the __`Store`__ tab.
3. Search for __`Beat Saber`__.
4. Click on the __`Get`__ button to claim the game.
<div align="center">
<img src="../wiki/Guides/Login/how-to-claim-oculus-desktop-version/claimOculusDesktopVersion.png" alt="claimOculusDesktopVersion.png" />
</div>
-1
View File
@@ -49,7 +49,6 @@ Guidance on fixing errors during setup or version updates
- __🐧Linux__:
- [Missing Icons in Game]([Linux]-Missing-Icons-in-Game)
- [Permission Denied on "bs-versions.json"]([Linux]-Permission-Denied-on-bs-version.json)
- [[Deb] The SUID Sandbox Helper Binary Was Found]([Linux]-[deb]-The-SUID-Sandbox-Helper-Binary-Was-Found)
- [[Flatpak] Steam Beat Saber Version Not Showing / Proton Not Detected]([Linux]-[Flatpak]-Steam-Beat-Saber-Version-Not-Showing-Proton-Not-Detected)
- [[Flatpak] Changing Installation Folder]([Linux]-[Flatpak]-Changing-Installation-Folder)
@@ -1,13 +0,0 @@
<pre>
Unhandled Exception UnhandledRejection Error: EACCES: permission denied, open '/opt/BSManager/resources/assets/jsons/bs-versions.json'
</pre>
To fix this issue, the current user must have write permissions to the "bs-versions.json". To correct the permissions do command below:
```bash
chmod +002 /opt/BSManager/resources/assets/jsons/bs-versions.json
# or
chown $(whoami) /opt/BSManager/resources/assets/jsons/bs-versions.json
```
+1 -1
View File
@@ -19,6 +19,7 @@
<summary>🔑 Login</summary>
<ul>
<li><a href="How-to-obtain-your-Oculus-Token">How to obtain your Oculus Token</a></li>
<li><a href="how-to-claim-oculus-desktop-version">How to claim your Oculus Desktop version of Beat Saber</a></li>
</ul>
</details>
@@ -71,7 +72,6 @@
<summary>🐧 Linux</summary>
<ul>
<li><a href="%5BLinux%5D-Missing-Icons-in-Game">Missing Icons in Game</a></li>
<li><a href="%5BLinux%5D-Permission-Denied-on-bs-version.json">Permission Denied on "bs-versions.json"</a></li>
<li><a href="%5BLinux%5D-%5Bdeb%5D-The-SUID-Sandbox-Helper-Binary-Was-Found">[Deb] The SUID Sandbox Helper Binary Was Found</a></li>
<li><a href="%5BLinux%5D-%5BFlatpak%5D-Steam-Beat-Saber-Version-Not-Showing-Proton-Not-Detected">[Flatpak] Steam Beat Saber Version Not Showing / Proton Not Detected</a></li>
<li><a href="%5BLinux%5D-%5BFlatpak%5D-Changing-Installation-Folder">[Flatpak] Changing Installation Folder</a></li>
+3
View File
@@ -29,6 +29,7 @@ const config = {
linux: {
target: [
"deb",
"rpm",
],
icon: "./build/icons/png",
category: "Utility;Game;",
@@ -69,6 +70,8 @@ const config = {
"--filesystem=~/.steam/steam/steamapps:ro", // for the libraryfolders.vdf
"--filesystem=~/.steam/steam/steamapps/common:create", // Steam game folder
"--filesystem=~/.steam/steam/steamapps/common/Beat Saber:create", // For installing mods/maps to original Beat Saber version
"--filesystem=~/Desktop", // allow writing shortcuts to desktop
"--filesystem=~/.steam/steam/userdata", // allow writing steam shortcuts
// Allow BSManager to create the compat folder if it does not exist
"--filesystem=~/.steam/steam/steamapps/compatdata/620980:create",
// Allow communication with network
+2960 -67
View File
File diff suppressed because it is too large Load Diff
+14 -14
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.8",
"version": "1.5.3",
"scripts": {
"build-rust-scripts": "ts-node ./.erb/scripts/build-rust-scripts.js",
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\"",
@@ -55,7 +55,7 @@
"url": "https://github.com/Zagrios"
},
"contributors": [],
"license": "MIT",
"license": "GPL-3.0-only",
"bugs": {
"url": "https://github.com/Zagrios/bs-manager/issues"
},
@@ -108,10 +108,10 @@
"css-loader": "^6.10.0",
"css-minimizer-webpack-plugin": "^6.0.0",
"detect-port": "^1.5.1",
"electron": "^33.3.1",
"electron": "^35.1.4",
"electron-builder": "^25.1.8",
"electron-devtools-installer": "^4.0.0",
"electronmon": "^2.0.2",
"electronmon": "^2.0.3",
"eslint": "^8.56.0",
"eslint-config-airbnb-base": "^15.0.0",
"eslint-config-erb": "^4.1.0",
@@ -160,24 +160,24 @@
"@tippyjs/react": "^4.2.6",
"archiver": "^7.0.1",
"clsx": "^2.1.1",
"color": "^4.2.3",
"color": "^5.0.0",
"crypto-js": "^4.2.0",
"dateformat": "^5.0.3",
"dot-prop": "^8.0.2",
"dot-prop": "^9.0.0",
"electron-debug": "^3.2.0",
"electron-log": "^4.4.8",
"electron-store": "^8.1.0",
"electron-updater": "^6.3.9",
"fast-deep-equal": "^3.1.3",
"format-duration": "^3.0.2",
"framer-motion": "^11.2.6",
"fs-extra": "^11.2.0",
"framer-motion": "^12.6.3",
"fs-extra": "^11.3.0",
"global-agent": "^3.0.0",
"got": "^14.4.5",
"got": "^14.4.7",
"history": "^5.3.0",
"is-elevated": "^4.0.0",
"md5-file": "^5.0.0",
"node-abi": "^3.65.0",
"node-abi": "^4.2.0",
"node-fetch": "^3.3.2",
"pako": "^2.1.0",
"protobufjs": "^7.4.0",
@@ -193,12 +193,12 @@
"react-window": "^1.8.11",
"recursive-readdir": "^2.2.3",
"rfdc": "^1.4.1",
"rxjs": "^7.8.1",
"rxjs": "^7.8.2",
"sanitize-filename": "^1.6.3",
"semver": "^7.6.3",
"semver": "^7.7.1",
"serialize-error": "^12.0.0",
"striptags": "^4.0.0-alpha.4",
"tailwind-merge": "^2.6.0",
"tailwind-merge": "^3.0.2",
"tailwindcss-scoped-groups": "^2.0.0",
"tippy.js": "^6.3.7",
"to-ico": "^1.1.5",
@@ -244,6 +244,6 @@
"logLevel": "quiet"
},
"volta": {
"node": "22.11.0"
"node": "22.14.0"
}
}
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.8",
"version": "1.5.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "bs-manager",
"version": "1.5.0-alpha.8",
"version": "1.5.3",
"hasInstallScript": true,
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "bs-manager",
"version": "1.5.0-alpha.8",
"version": "1.5.3",
"description": "BSManager",
"main": "./dist/main/main.js",
"author": {
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+61
View File
@@ -0,0 +1,61 @@
import { parseEnvString } from "main/helpers/env.helpers";
describe("Test parseEnvString", () => {
it("Empty", () => {
const envVars = parseEnvString("");
expect(envVars).toEqual({});
});
it("Single test; no quotes", () => {
const envString = "HELLO=World!";
const envVars = parseEnvString(envString);
expect(envVars).toEqual({
HELLO: "World!",
});
});
it("Single test; single quotes", () => {
const envString = "SINGLE_QOUTE='Single quote with spaces'";
const envVars = parseEnvString(envString);
expect(envVars).toEqual({
SINGLE_QOUTE: "Single quote with spaces",
});
});
it("Single test; double quotes", () => {
const envString = 'DOUBLE_QOUTE="Some random quote."';
const envVars = parseEnvString(envString);
expect(envVars).toEqual({
DOUBLE_QOUTE: "Some random quote.",
});
});
it("Single test; empty value", () => {
const envString = "EMPTY=";
const envVars = parseEnvString(envString);
expect(envVars).toEqual({
EMPTY: "",
});
});
it("Multiple test; combined", () => {
const envString = `HELLO=World! DOUBLE_QUOTE="Two Words" SINGLE_QUOTE='' EMPTY=`
const envVars = parseEnvString(envString);
expect(envVars).toEqual(expect.objectContaining({
HELLO: "World!",
DOUBLE_QUOTE: "Two Words",
SINGLE_QUOTE: "",
EMPTY: ""
}));
});
it("Key with numbers and lower case", () => {
const envString = "H3ll0=world";
const envVars = parseEnvString(envString);
expect(envVars).toEqual({
H3ll0: "world",
});
});
});
+101
View File
@@ -0,0 +1,101 @@
import { mkdir, pathExistsSync, rm, writeFile } from "fs-extra";
import { getSize } from "main/helpers/fs.helpers";
import path from "path";
jest.mock("electron", () => ({ app: {
getPath: () => "",
getName: () => "",
}}));
jest.mock("electron-log", () => ({
info: jest.fn(),
error: jest.fn(),
}));
const TEST_FOLDER = path.resolve(__dirname, "..", "assets", "fs");
describe("Test fs.helpers getSize", () => {
beforeEach(async () => {
if (pathExistsSync(TEST_FOLDER)) {
await rm(TEST_FOLDER, { recursive: true, force: true });
}
await mkdir(TEST_FOLDER);
});
afterEach(async () => {
await rm(TEST_FOLDER, { recursive: true, force: true });
});
it("should return 0 for empty folder", async () => {
const size = await getSize(TEST_FOLDER);
expect(size).toBe(0);
});
it("should throw error for non-existing folder", async () => {
await expect(getSize(`${TEST_FOLDER}1`)).rejects.toThrow();
});
it("should return the total size of files in the directory", async () => {
const filePath = path.join(TEST_FOLDER, "testFile.bin");
const buffer = Buffer.alloc(10);
await writeFile(filePath, buffer);
const size = await getSize(TEST_FOLDER);
expect(size).toBe(10);
});
it("should include the sizes of all files in the directory", async () => {
const filePath1 = path.join(TEST_FOLDER, "testFile1.bin");
const filePath2 = path.join(TEST_FOLDER, "testFile2.bin");
const buffer = Buffer.alloc(10);
await writeFile(filePath1, buffer);
await writeFile(filePath2, buffer);
const size = await getSize(TEST_FOLDER);
expect(size).toBe(20);
});
it("should include the sizes of files in nested directories", async () => {
const subFolder = path.join(TEST_FOLDER, "subFolder");
const filePath1 = path.join(TEST_FOLDER, "testFile1.bin");
const filePath2 = path.join(subFolder, "testFile2.bin");
const buffer = Buffer.alloc(10);
await mkdir(subFolder);
await writeFile(filePath1, buffer);
await writeFile(filePath2, buffer);
const size = await getSize(TEST_FOLDER);
expect(size).toBe(20);
});
it("should not include files beyond the default depth limit", async () => {
const subFolder = path.join(TEST_FOLDER, "1", "2", "3", "4", "5");
const filePath = path.join(subFolder, "testFile.bin");
const buffer = Buffer.alloc(10);
await mkdir(subFolder, { recursive: true });
await writeFile(filePath, buffer);
const size = await getSize(TEST_FOLDER);
expect(size).toBe(0);
});
it("should include files within the specified depth limit", async () => {
const subFolder = path.join(TEST_FOLDER, "1", "2", "3", "4", "5");
const filePath = path.join(subFolder, "testFile.bin");
const filePath2 = path.join(TEST_FOLDER, "1", "2", "testFile2.bin");
const buffer = Buffer.alloc(10);
await mkdir(subFolder, { recursive: true });
await writeFile(filePath, buffer);
await writeFile(filePath2, buffer);
const size = await getSize(TEST_FOLDER, 6);
expect(size).toBe(20);
});
});
+62
View File
@@ -0,0 +1,62 @@
import { parseMetadata } from "main/helpers/mods.helpers";
import path from "path";
const TEST_FOLDER = path.resolve(__dirname, "..", "assets", "dll");
describe("Test mods.helpers.test", () => {
test("parseMetadata of BSML.dll", async () => {
const filepath = path.join(TEST_FOLDER, "BSML.dll");
const data = await parseMetadata(filepath);
expect(data).toBeTruthy();
expect(data.name).toBe("BeatSaberMarkupLanguage");
expect(data.id).toBe("BeatSaberMarkupLanguage");
expect(data.author).toBeTruthy();
expect(data.version).toBeTruthy();
expect(data.gameVersion).toBeTruthy();
});
test("parseMetadata of Bugsmash.dll", async () => {
const filepath = path.join(TEST_FOLDER, "Bugsmash.dll");
const data = await parseMetadata(filepath);
expect(data).toBeTruthy();
expect(data.name).toBe("Bugsmash");
expect(data.id).toBe("Bugsmash");
expect(data.author).toBeTruthy();
expect(data.version).toBeTruthy();
expect(data.gameVersion).toBeTruthy();
});
test("parseMetadata of PlaylistManager.dll", async () => {
const filepath = path.join(TEST_FOLDER, "PlaylistManager.dll");
const data = await parseMetadata(filepath);
expect(data).toBeTruthy();
expect(data.name).toBe("PlaylistManager");
expect(data.id).toBe("PlaylistManager");
expect(data.author).toBeTruthy();
expect(data.version).toBeTruthy();
expect(data.gameVersion).toBeTruthy();
});
test("parseMetadata of SongCore.dll", async () => {
const filepath = path.join(TEST_FOLDER, "SongCore.dll");
const data = await parseMetadata(filepath);
expect(data).toBeTruthy();
expect(data.name).toBe("SongCore");
expect(data.id).toBe("SongCore");
expect(data.author).toBeTruthy();
expect(data.version).toBeTruthy();
expect(data.gameVersion).toBeTruthy();
});
});
-67
View File
@@ -1,67 +0,0 @@
import log from "electron-log";
import { isPromise } from "../../shared/helpers/promise.helpers";
import { tryit } from "../../shared/helpers/error.helpers";
type LogOptions = {
logInput?: boolean;
logOutput?: boolean;
logArgs?: boolean;
};
const stringifyArgs = (args: unknown[]) => {
return args?.map(a => JSON.stringify(a)).join(', ');
};
const logInfo = (message: string, propertyKey: string, args: unknown[], logArgs: boolean, result: unknown) => {
log.info(`[${message}] ${propertyKey}(${logArgs ? stringifyArgs(args) : ''})`, result);
};
const logError = (propertyKey: string, args: unknown[], logArgs: boolean, error: unknown) => {
log.error(`[ERROR] ${propertyKey}(${logArgs ? stringifyArgs(args) : ''})`, error);
throw error;
};
export function Log(options?: LogOptions){
const logInput = options?.logInput ?? false;
const logOutput = options?.logOutput ?? true;
const logArgs = options?.logArgs ?? true;
return (target: unknown, propertyKey: string, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
descriptor.value = function(...args: unknown[]) {
if (logInput) {
logInfo('INPUT', propertyKey, args, logArgs, null);
}
const outcome = tryit(() => originalMethod.apply(this, args));
if (isPromise(outcome)) {
return outcome.then(({ result, error }) => {
if (error) {
logError(propertyKey, args, logArgs, error);
}
if (logOutput) {
logInfo('OUTPUT', propertyKey, args, logArgs, result);
}
return result;
});
}
const { result, error } = outcome;
if (error) {
logError(propertyKey, args, logArgs, error);
}
if (logOutput) {
logInfo('OUTPUT', propertyKey, args, logArgs, result);
}
return result;
};
};
}
+116 -1
View File
@@ -1,3 +1,4 @@
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { ProviderPlatform } from "shared/models/provider-platform.enum";
export function execOnOs<T>(executions: { [key in ProviderPlatform]?: () => T }, noError = false): T {
@@ -10,4 +11,118 @@ export function execOnOs<T>(executions: { [key in ProviderPlatform]?: () => T },
}
return undefined;
}
}
enum EnvParserState {
NAME_START,
NAME,
VALUE_START,
VALUE,
QUOTE_VALUE,
DQUOTE_VALUE,
SPACE,
ERROR,
};
const isAlphaCharacter = (c: string) =>
(c >= "a" && c <= "z") || (c >= "A" && c <= "Z");
const isNumber = (c: string) => c >= "0" && c <= "9";
export function parseEnvString(envString: string): Record<string, string> {
const envVars: Record<string, string> = {};
let state: EnvParserState = EnvParserState.NAME_START;
let index = 0;
let newName = "";
for (let pos = 0; pos < envString.length; ++pos) {
const c = envString[pos];
switch (state) {
case EnvParserState.NAME_START:
if (isAlphaCharacter(c) || c === "_") {
state = EnvParserState.NAME;
index = pos;
} else if (c !== " ") {
state = EnvParserState.ERROR;
}
break;
case EnvParserState.NAME:
if (c === "=") {
state = EnvParserState.VALUE_START;
newName = envString.substring(index, pos);
index = pos + 1;
} else if (!isAlphaCharacter(c) && !isNumber(c) && c !== "_") {
state = EnvParserState.ERROR;
}
break;
case EnvParserState.VALUE_START:
if (c === "'") {
++index;
state = EnvParserState.QUOTE_VALUE;
} else if (c === '"') {
++index;
state = EnvParserState.DQUOTE_VALUE;
} else if (c === " ") {
state = EnvParserState.NAME_START;
envVars[newName] = "";
} else {
state = EnvParserState.VALUE;
}
break;
case EnvParserState.VALUE:
if (c === " ") {
state = EnvParserState.NAME_START;
envVars[newName] = envString.substring(index, pos);
}
break;
case EnvParserState.QUOTE_VALUE:
if (c === "'") {
state = EnvParserState.SPACE;
envVars[newName] = envString.substring(index, pos);
}
break;
case EnvParserState.DQUOTE_VALUE:
if (c === '"') {
state = EnvParserState.SPACE;
envVars[newName] = envString.substring(index, pos);
}
break;
case EnvParserState.SPACE:
if (c === " ") {
state = EnvParserState.NAME_START;
} else {
state = EnvParserState.ERROR;
}
break;
default:
}
if (state === EnvParserState.ERROR) {
throw new CustomError(
`parseEnvString failed: invalid character at position ${pos}`,
"generic.env.parse"
);
}
}
if (state === EnvParserState.VALUE_START || state === EnvParserState.VALUE) {
envVars[newName] = envString.substring(index);
return envVars;
}
if (state === EnvParserState.NAME_START || state === EnvParserState.SPACE) {
return envVars;
}
throw new CustomError(
"parseEnvString failed: invalid ending state",
"generic.env.parse"
);
}
+108 -28
View File
@@ -1,9 +1,8 @@
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 { CopyOptions, MoveOptions, RmOptions, copy, createReadStream, ensureDir, move, pathExists, pathExistsSync, realpath, rm, stat, symlink, unlink, unlinkSync, rmSync } from "fs-extra";
import { access, mkdir, readdir, lstat, readlink } from "fs/promises";
import path from "path";
import { Observable, concatMap, from } from "rxjs";
import log from "electron-log";
import { BsmException } from "shared/models/bsm-exception.model";
import crypto from "crypto";
import { execSync } from "child_process";
import { tryit } from "../../shared/helpers/error.helpers";
@@ -28,18 +27,54 @@ export async function ensureFolderExist(path: string): Promise<void> {
.then(() => {});
}
export async function deleteFolder(folderPath: string): Promise<void> {
export async function deleteFile(filepath: string) {
try {
log.info("Deleting file", `"${filepath}"`);
await unlink(filepath);
} catch (error: any) {
log.error("Could not delete file", `"${filepath}"`);
throw CustomError.fromError(error, "generic.fs.delete-file");
}
}
export function deleteFileSync(filepath: string) {
try {
log.info("Deleting file", `"${filepath}"`);
unlinkSync(filepath);
} catch (error: any) {
log.error("Could not delete file", `"${filepath}"`);
throw CustomError.fromError(error, "generic.fs.delete-file");
}
}
export async function deleteFolder(folderPath: string, options?: RmOptions) {
if (!(await pathExist(folderPath))) {
return;
}
return rm(folderPath, { recursive: true, force: true });
try {
options = options || { recursive: true, force: true };
log.info("Deleting folder", `"${folderPath}"`, options);
await rm(folderPath, options);
} catch (error: any) {
log.error("Could not delete folder", `"${folderPath}"`);
throw CustomError.fromError(error, "generic.fs.delete-folder");
}
}
export async function unlinkPath(path: string): Promise<void> {
if (!(await pathExist(path))) {
export function deleteFolderSync(folderPath: string, options?: RmOptions) {
if (!pathExistsSync(folderPath)) {
return;
}
return unlink(path);
try {
options = options || { recursive: true, force: true };
log.info("Deleting folder", `"${folderPath}"`, options);
rmSync(folderPath, options);
} catch (error: any) {
log.error("Could not delete folder", `"${folderPath}"`);
throw CustomError.fromError(error, "generic.fs.delete-folder");
}
}
export async function getFoldersInFolder(folderPath: string, opts?: { ignoreSymlinkTargetError?: boolean }): Promise<string[]> {
@@ -81,6 +116,7 @@ export async function getFilesInFolder(folderPath: string): Promise<string[]> {
}
export function moveFolderContent(src: string, dest: string, option?: MoveOptions): Observable<Progression> {
log.info(`(moveFolderContent) Moving ${src} to ${dest}`);
const progress: Progression = { current: 0, total: 0 };
return new Observable<Progression>(subscriber => {
subscriber.next(progress);
@@ -96,21 +132,32 @@ export function moveFolderContent(src: string, dest: string, option?: MoveOption
const files = await readdir(src, { encoding: "utf-8", withFileTypes: true });
progress.total = files.length;
for(const file of files){
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){
if (file.isFile() || !allChildsAlreadyExist) {
const prevSize = await getSize(srcFullPath);
await move(srcFullPath, destFullPath, option);
const afterSize = await getSize(destFullPath);
// The size after moving should be the same or greater than the size before moving but never less
if (afterSize < prevSize) {
throw new CustomError(`File size mismath. before: ${prevSize}, after: ${afterSize} (${srcFullPath})`, "FILE_SIZE_MISMATCH");
}
} else {
log.info(`Skipping ${srcFullPath} to ${destFullPath}, all child already exist in destination`);
}
progress.current++;
subscriber.next(progress);
}
})().catch(err => subscriber.error(CustomError.fromError(err, err?.code))).finally(() => subscriber.complete());
})()
.catch(err => subscriber.error(CustomError.fromError(err, err?.code)))
.finally(() => subscriber.complete());
});
}
@@ -133,7 +180,7 @@ export function isSubdirectory(parent: string, child: string): boolean {
export async function copyDirectoryWithJunctions(src: string, dest: string, options?: CopyOptions): Promise<void> {
if (isSubdirectory(src, dest)) {
throw { message: `Cannot copy directory '${src}' into itself '${dest}'.`, code: "COPY_TO_SUBPATH" } as BsmException;
throw new CustomError(`Cannot copy directory '${src}' into itself '${dest}'.`, "COPY_TO_SUBPATH");
}
await ensureDir(dest);
@@ -149,7 +196,7 @@ export async function copyDirectoryWithJunctions(src: string, dest: string, opti
await copy(sourcePath, destinationPath, options);
} else if (item.isSymbolicLink()) {
if (options?.overwrite) {
await unlinkPath(destinationPath);
await deleteFile(destinationPath);
}
const symlinkTarget = await readlink(sourcePath);
const relativePath = path.relative(src, symlinkTarget);
@@ -169,8 +216,7 @@ export function hashFile(filePath: string, algorithm = "sha256"): Promise<string
});
}
export async function dirSize(dirPath: string): Promise<number>{
export async function dirSize(dirPath: string): Promise<number> {
const entries = await readdir(dirPath);
const paths = entries.map(async entry => {
@@ -189,11 +235,10 @@ export async function dirSize(dirPath: string): Promise<number>{
return 0;
});
return (await Promise.all(paths)).flat(Infinity).reduce((acc, size ) => acc + size, 0);
return (await Promise.all(paths)).flat(Infinity).reduce((acc, size) => acc + size, 0);
}
export function rxCopy(src: string, dest: string, option?: CopyOptions): Observable<Progression> {
const dirSizePromise = dirSize(src).catch(err => {
log.error("dirSizePromise", err);
return 0;
@@ -205,15 +250,19 @@ export function rxCopy(src: string, dest: string, option?: CopyOptions): Observa
return new Observable<Progression>(sub => {
sub.next(progress);
copy(src, dest, {...option, filter: (src) => {
stat(src).then(stats => {
progress.current += stats.size;
sub.next(progress);
});
return true;
}})
.then(() => sub.complete()).catch(err => sub.error(err))
})
copy(src, dest, {
...option,
filter: src => {
stat(src).then(stats => {
progress.current += stats.size;
sub.next(progress);
});
return true;
},
})
.then(() => sub.complete())
.catch(err => sub.error(err));
});
})
);
}
@@ -246,7 +295,7 @@ export function ensurePathNotAlreadyExistSync(path: string): string {
return destPath;
}
export async function isJunction(path: string): Promise<boolean>{
export async function isJunction(path: string): Promise<boolean> {
const [stats, lstats] = await Promise.all([stat(path), lstat(path)]);
return lstats.isSymbolicLink() && stats.isDirectory();
}
@@ -254,7 +303,7 @@ export async function isJunction(path: string): Promise<boolean>{
export function resolveGUIDPath(guidPath: string): string {
const guidVolume = path.parse(guidPath).root;
const command = `powershell -command "(Get-WmiObject -Class Win32_Volume | Where-Object { $_.DeviceID -like '${guidVolume}' }).DriveLetter"`;
const {result: driveLetter, error} = tryit(() => execSync(command).toString().trim());
const { result: driveLetter, error } = tryit(() => execSync(command).toString().trim());
if (!driveLetter || error) {
throw new Error("Unable to resolve GUID path", error);
}
@@ -274,6 +323,37 @@ export function getUniqueFileNamePath(filePath: string): string {
return path.join(dir, newFileName);
}
/**
* @throws {Error} Can throw file system errors
*/
export async function getSize(targetPath: string, maxDepth = 5): Promise<number> {
const visited = new Set<string>();
const computeSize = async (currentPath: string, depth: number): Promise<number> => {
if (visited.has(currentPath)) {
return 0;
}
visited.add(currentPath);
const stats = await stat(currentPath);
if (stats.isFile()) {
return stats.size;
}
if (!stats.isDirectory() || depth >= maxDepth) {
return 0;
}
const entries = await readdir(currentPath);
const sizes = await Promise.all(entries.map(entry => computeSize(path.join(currentPath, entry), depth + 1)));
return sizes.reduce((acc, cur) => acc + cur, 0);
};
return computeSize(targetPath, 0);
}
export interface Progression<T = unknown, D = unknown> {
total: number;
current: number;
+57
View File
@@ -0,0 +1,57 @@
import { readFile } from "fs-extra";
import { ModMetadata } from "shared/models/mods/mod.interface";
// NOTE: getMetadata is slow since its using a search string and object parsing.
// Ways to optimize:
// - Find a way to locate the metadata string within the pdb/dll file
// - Correct object sizing, since the first byte does not match sometimes with the size somehow
// ref: https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json
const SEARCH_ARRAY = [
0x22, 0x24, 0x73, 0x63, 0x68,
0x65, 0x6D, 0x61, 0x22
].reverse(); // '"$schema"'
export async function parseMetadata(filepath: string): Promise<ModMetadata | null> {
const data = await readFile(filepath);
// Find the word SEARCH_ARRAY from end to start of the buffer data
// NOTE: Inefficient algorithm
let start = data.length - 1;
for (let i = 0; start >= 0; --start) {
if (data[start] !== SEARCH_ARRAY[i]) {
i = 0;
} else if (++i === SEARCH_ARRAY.length) {
break;
}
}
if (start < 4) {
return null;
}
// Search for the starting left curly brace '{'
while (data[--start] !== 0x7B);
// Find the ending right curly brace '}'
// NOTE: Inefficient algorithm
let braces = 1;
let end = start + 1;
for (; end < data.length && braces > 0; ++end) {
if (data[end] === 0x7B) { // {
++braces;
} else if (data[end] === 0x7D) { // }
--braces;
}
}
// Does not work with every dll (eg. Bugsmash.dll)
// const length = (data[start - 1] << 24)
// + (data[start - 2] << 16)
// + (data[start - 3] << 8)
// + (data[start - 4]);
// const end = start + length;
return JSON.parse(data.toString(undefined, start, end)) as ModMetadata;
}
+6
View File
@@ -1,6 +1,7 @@
import { BsModsManagerService } from "../services/mods/bs-mods-manager.service";
import { IpcService } from "../services/ipc.service";
import { from } from "rxjs";
import { BeatModsApiService } from "main/services/mods/beat-mods-api.service";
const ipc = IpcService.getInstance();
@@ -33,3 +34,8 @@ ipc.on("bs-mods.uninstall-all-mods", (args, reply) => {
const modsManager = BsModsManagerService.getInstance();
reply(modsManager.uninstallAllMods(args));
});
ipc.on("bs-mods.beatmods-up", (_, reply) => {
const beatMods = BeatModsApiService.getInstance();
reply(from(beatMods.isUp()));
})
+5
View File
@@ -9,3 +9,8 @@ ipc.on("linux.verify-proton-folder", (_, reply) => {
reply(of(linuxService.verifyProtonPath()));
});
ipc.on("linux.get-wine-prefix-path", (_, reply) => {
const linuxService = LinuxService.getInstance();
reply(of(linuxService.getWinePrefixPath()));
});
+57 -58
View File
@@ -23,9 +23,11 @@ import { LivShortcut } from "./services/liv/liv-shortcut.service";
import { SteamLauncherService } from "./services/bs-launcher/steam-launcher.service";
import { FileAssociationService } from "./services/file-association.service";
import { SongDetailsCacheService } from "./services/additional-content/maps/song-details-cache.service";
import { readdirSync, statSync, unlinkSync } from "fs-extra";
import { Dirent, readdirSync } from "fs-extra";
import { StaticConfigurationService } from "./services/static-configuration.service";
import { configureProxy } from './helpers/proxy.helpers';
import { deleteFileSync, deleteFolderSync } from "./helpers/fs.helpers";
import { tryit } from "shared/helpers/error.helpers";
const isDebug = process.env.NODE_ENV === "development" || process.env.DEBUG_PROD === "true";
const staticConfig = StaticConfigurationService.getInstance();
@@ -37,7 +39,7 @@ export const filterPatterns = new Set<RegExp>();
filterPatterns.add(/(FRL|OC)\S{10,}/g);
initLogger();
deleteOlestLogs();
deleteOldestLogs();
deleteOldLogs();
staticConfig.take("disable-hadware-acceleration", disabled => {
@@ -156,11 +158,40 @@ if (!gotTheLock) {
}).catch(log.error);
}
function convertDateToDateString(date: Date): string {
const month = (date.getMonth() + 1).toString().padStart(2, "0");
const day = date.getDate().toString().padStart(2, "0");
return `${date.getFullYear()}-${month}-${day}`;
}
function getReadableTime(date: Date): string {
return `${
date.getHours().toString().padStart(2, "0")
}-${
date.getMinutes().toString().padStart(2, "0")
}-${
date.getSeconds().toString().padStart(2, "0")
}`;
}
function initLogger(){
log.transports.file.level = "info";
let filepath = "";
let currentDateString = convertDateToDateString(new Date());
log.transports.file.resolvePath = () => {
const now = new Date();
return path.join(app.getPath("logs"), `${now.getFullYear()}-${now.getMonth() + 1}-${now.getDate()}-v${app.getVersion()}.log`);
const nowString = convertDateToDateString(now);
if (filepath && nowString === currentDateString) {
return filepath;
}
filepath = path.join(
app.getPath("logs"), nowString,
`${nowString}_${getReadableTime(now)}_v${app.getVersion()}.log`
);
currentDateString = nowString;
return filepath;
};
log.hooks.push((message) => {
@@ -203,70 +234,38 @@ function initLogger(){
log.catchErrors();
}
function getLogFilesEntries() {
// Keep only the past week (7 days) of logs
function deleteOldLogs(): void {
let deleteLogFolders: Dirent[] = [];
try {
const logsFolder = app.getPath("logs");
let logs = readdirSync(logsFolder, { withFileTypes: true });
const filterDate = convertDateToDateString(
new Date(Date.now() - 7 * 24 * 60 * 60 * 1000) // 7 days
);
deleteLogFolders = readdirSync(app.getPath("logs"), { withFileTypes: true })
.filter(folder => folder.isDirectory() && folder.name <= filterDate);
} catch (error) {
log.error("Error while deleting old logs:", error);
return;
}
logs = logs.filter(file => file.isFile() && path.extname(file.name) === ".log");
logs.sort((a, b) => {
const aStat = statSync(path.join(logsFolder, a.name));
const bStat = statSync(path.join(logsFolder, b.name));
return bStat.mtime.getTime() - aStat.mtime.getTime();
});
return logs.map(file => {
const filePath = path.join(logsFolder, file.name);
const stat = statSync(filePath);
return {
path: filePath,
name: file.name,
stats: stat
};
});
} catch (err) {
log.error('Error while retrieving log files entries:', err);
return [];
for (const folder of deleteLogFolders) {
const folderPath = path.join(folder.parentPath, folder.name);
tryit(() => deleteFolderSync(folderPath));
}
}
// keep only the last 5 logs
function deleteOldLogs(): void{
try {
let logs = getLogFilesEntries();
// Obsolete behavior, delete log files that are on the parent log folder
function deleteOldestLogs(): void {
const logsFolder = app.getPath("logs");
const logs = readdirSync(logsFolder, { withFileTypes: true })
.filter(file => file.isFile() && path.extname(file.name) === ".log");
logs = logs.slice(5);
logs.forEach(file => {
try {
unlinkSync(file.path);
log.info(`Deleted log file: ${file.path}`);
} catch (err) {
log.error(`Error deleting file ${file.path}:`, err);
}
});
} catch (err) {
log.error("Error while deleting old logs:", err);
for (const file of logs) {
const filepath = path.join(file.parentPath, file.name);
tryit(() => deleteFileSync(filepath));
}
}
// Temporary function to delete logs before 2024-07-31
function deleteOlestLogs(): void{
// delete all logs before 2024-07-31
const date = new Date(2024, 6, 31); // month is 0-based
const logs = getLogFilesEntries().filter(file => file.stats.mtime.getTime() < date.getTime());
logs.forEach(file => {
try {
unlinkSync(file.path);
log.info(`Deleted log file: ${file.path}`);
} catch (err) {
log.error(`Error deleting file ${file.path}:`, err);
}
});
}
export function addFilterStringLog(filter: string): void {
filterStrings.add(filter);
}
+30 -6
View File
@@ -1,3 +1,4 @@
import log from "electron-log";
import fetch from "node-fetch";
import { CustomError } from "../../shared/models/exceptions/custom-error.class";
import { mkdirs, createWriteStream, pathExists, WriteStream } from "fs-extra";
@@ -87,7 +88,10 @@ export class OculusDownloader {
sub.next(progress);
}
})().catch(err => sub.error(err)).finally(() => {
})()
.then(() => {
log.info("Downloaded file to", `"${destination}"`);
}).catch(err => sub.error(err)).finally(() => {
sub.complete();
writeStream?.end();
});
@@ -100,12 +104,25 @@ export class OculusDownloader {
}
private async isFileIntegrityValid(file: OculusFileWithName, folder: string): Promise<boolean> {
const [fileName, fileData] = file;
const destination = path.join(folder, fileName);
const [filename, fileData] = file;
const destination = path.join(
folder,
process.platform === "win32"
? filename
: filename.replaceAll("\\", "/")
);
return pathExists(destination).then(exists => {
if(!exists){ return false; }
return hashFile(destination, "sha256").then(hash => hash === fileData.sha256);
if(!exists){
log.info("File not found", `"${destination}"`);
return false;
}
return hashFile(destination, "sha256").then(hash => {
const isValid = hash === fileData.sha256;
log.info("File integrity", isValid ? "VALID" : "INVALID", `"${destination}"`);
return isValid;
});
});
}
@@ -124,6 +141,7 @@ export class OculusDownloader {
if(canceled){ return; }
if(!(await this.isFileIntegrityValid(oculusFile, folder))){
log.info("File integrity invalid", `"${oculusFile[0]}"`);
wrongFiles.push(oculusFile);
}
@@ -176,7 +194,13 @@ export class OculusDownloader {
return from([res])
}
const target = path.join(options.destination, filename);
const target = path.join(
options.destination,
process.platform === "win32"
? filename
: filename.replaceAll("\\", "/")
);
return this.downloadManifestFile(file, target).pipe(
catchError(err => {
this.options.logger?.error(err);
@@ -8,7 +8,7 @@ import path from "path";
import { RequestService } from "../request.service";
import { copyFileSync } from "fs-extra";
import sanitize from "sanitize-filename";
import { Progression, ensureFolderExist, unlinkPath } from "../../helpers/fs.helpers";
import { Progression, deleteFile, ensureFolderExist } from "../../helpers/fs.helpers";
import { MODEL_FILE_EXTENSIONS, MODEL_TYPES, MODEL_TYPE_FOLDERS } from "../../../shared/models/models/constants";
import { InstallationLocationService } from "../installation-location.service";
import { Observable, Subscription, lastValueFrom } from "rxjs";
@@ -105,7 +105,9 @@ export class LocalModelsManagerService {
}
public async oneClickDownloadModel(model: MSModel): Promise<void> {
if (!model) { return; }
if (!model) {
return;
}
const versions = await this.localVersion.getInstalledVersions();
const downloaded = await lastValueFrom(this.downloadModel(model, versions.pop()));
@@ -198,7 +200,7 @@ export class LocalModelsManagerService {
};
for (const model of models) {
await unlinkPath(model.path);
await deleteFile(model.path);
progression.data.push(model);
progression.current = progression.data.length;
subscriber.next(progression);
@@ -11,7 +11,7 @@ import { BPList, DownloadPlaylistProgressionData, PlaylistSong } from "shared/mo
import { readFileSync, Stats } from "fs";
import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service";
import { copy, ensureDir, pathExists, pathExistsSync, realpath, writeFileSync } from "fs-extra";
import { Progression, getUniqueFileNamePath, unlinkPath } from "../../helpers/fs.helpers";
import { Progression, deleteFile, getUniqueFileNamePath } from "../../helpers/fs.helpers";
import { FileAssociationService } from "../file-association.service";
import { SongDetailsCacheService } from "./maps/song-details-cache.service";
import { sToMs } from "shared/helpers/time.helpers";
@@ -242,7 +242,9 @@ export class LocalPlaylistsManagerService {
...localBPList,
duration: 0,
nbMaps: localBPList.songs?.length ?? 0,
id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined
id: localBPList.customData?.syncURL ? tryExtractPlaylistId(localBPList.customData.syncURL) : undefined,
minNps: Infinity,
maxNps: -Infinity,
}
const mappers = new Set<number>();
@@ -254,12 +256,25 @@ export class LocalPlaylistsManagerService {
bpListDetails.duration += songDetails?.duration ? +songDetails.duration : 0;
mappers.add(songDetails.uploader?.id);
bpListDetails.minNps = Math.min(bpListDetails?.minNps ?? 0, Math.min(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0]));
bpListDetails.maxNps = Math.max(bpListDetails?.maxNps ?? 0, Math.max(...songDetails.difficulties?.map(d => d?.nps || 0) ?? [0]));
const mapNps = songDetails.difficulties?.map(d => d?.nps).filter(nps => typeof nps === "number" && !Number.isNaN(nps));
if(mapNps?.length){
bpListDetails.minNps = Math.min(bpListDetails.minNps, ...mapNps);
bpListDetails.maxNps = Math.max(bpListDetails.maxNps, ...mapNps);
}
song.songDetails = songDetails;
}
if(!Number.isFinite(bpListDetails.minNps)){
bpListDetails.minNps = 0;
}
if(!Number.isFinite(bpListDetails.maxNps)){
bpListDetails.maxNps = 0;
}
bpListDetails.nbMappers = mappers.size;
return bpListDetails;
@@ -394,7 +409,7 @@ export class LocalPlaylistsManagerService {
}
public deletePlaylistFile(bpList: LocalBPList): Observable<void>{
return from(unlinkPath(bpList.path));
return from(deleteFile(bpList.path));
}
public exportPlaylists(opt: {version?: BSVersion, bpLists: LocalBPList[], dest: string, playlistsMaps?: BsmLocalMap[]}): Observable<Progression<string>> {
@@ -7,7 +7,7 @@ import { InstallationLocationService } from "../../installation-location.service
import { UtilsService } from "../../utils.service";
import crypto, { BinaryLike } from "crypto";
import { lstatSync } from "fs";
import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath, unlink } from "fs-extra";
import { copy, createReadStream, ensureDir, pathExists, pathExistsSync, realpath } from "fs-extra";
import { RequestService } from "../../request.service";
import sanitize from "sanitize-filename";
import { DeepLinkService } from "../../deep-link.service";
@@ -15,7 +15,7 @@ import log from "electron-log";
import { WindowManagerService } from "../../window-manager.service";
import { Observable, Subject, lastValueFrom } from "rxjs";
import { Archive } from "../../../models/archive.class";
import { Progression, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers";
import { Progression, deleteFile, deleteFolder, ensureFolderExist, getFilesInFolder, getFoldersInFolder, pathExist } from "../../../helpers/fs.helpers";
import { readFile } from "fs/promises";
import { FolderLinkerService } from "../../folder-linker.service";
import { allSettled } from "../../../../shared/helpers/promise.helpers";
@@ -280,6 +280,8 @@ export class LocalMapsManagerService {
return new Observable<Progression>(observer => {
const progress: Progression = { total: hashs.length, current: 0 };
observer.next(progress);
(async () => {
const versionMapsPath = await this.getMapsFolderPath(version);
const mapsPaths = await getFoldersInFolder(versionMapsPath);
@@ -367,10 +369,12 @@ export class LocalMapsManagerService {
for (const folder of mapsFolders) {
log.info(">", folder);
const regex = new RegExp(`^${escapeRegExp(folder)}\\/`);
const entriesNames = isRoot
? null
: [ new RegExp(`^${escapeRegExp(folder)}\\/`) ];
const exported = await zip.extract(destination, {
entriesNames: [regex],
entriesNames,
abortToken: abortController
});
@@ -426,9 +430,10 @@ export class LocalMapsManagerService {
throw new Error("Cannot download map, no hash found");
}
log.info("Downloading map", map.name, map.id);
const zipUrl = map.versions.at(0).downloadURL;
log.info("Downloading map", map.name, map.id, zipUrl);
const mapFolderName = sanitize(`${map.id} (${map.metadata.songName} - ${map.metadata.levelAuthorName})`);
const mapsFolder = await this.getMapsFolderPath(version);
@@ -447,7 +452,7 @@ export class LocalMapsManagerService {
const zip = await BsmZipExtractor.fromPath(zipPath);
await zip.extract(mapPath);
zip.close();
await unlink(zipPath);
await deleteFile(zipPath);
const localMap = await this.loadMapInfoFromPath(mapPath);
localMap.songDetails = this.songDetailsCache.getSongDetails(localMap.hash);
@@ -19,7 +19,6 @@ export class SongCacheService {
private readonly mapsInfoCache: JsonCache<CachedMapInfoWithHash>;
private constructor(){
console.log(this.MAPS_INFO_CACHE_PATH);
this.mapsInfoCache = new JsonCache(this.MAPS_INFO_CACHE_PATH);
}
@@ -8,6 +8,34 @@ import { LinuxService } from "../linux.service";
import { BsmShellLog, bsmSpawn } from "main/helpers/os.helpers";
import { IS_FLATPAK } from "main/constants";
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
import { parseEnvString } from "main/helpers/env.helpers";
export function buildBsLaunchArgs(launchOptions: LaunchOption): string[] {
const launchArgs = [];
if(!launchOptions.version.steam && !launchOptions.version.oculus){
launchArgs.push("--no-yeet")
}
if(launchOptions.launchMods?.includes(LaunchMods.OCULUS)) {
launchArgs.push("-vrmode");
launchArgs.push("oculus");
}
if(launchOptions.launchMods?.includes(LaunchMods.FPFC)) {
launchArgs.push("fpfc");
}
if(launchOptions.launchMods?.includes(LaunchMods.DEBUG)) {
launchArgs.push("--verbose");
}
if(launchOptions.launchMods?.includes(LaunchMods.EDITOR)) {
launchArgs.push("editor");
}
if (launchOptions.command) {
launchArgs.push(launchOptions.command);
}
return Array.from(new Set(launchArgs).values());
}
export abstract class AbstractLauncherService {
@@ -19,34 +47,9 @@ export abstract class AbstractLauncherService {
this.localVersions = BSLocalVersionService.getInstance();
}
protected buildBsLaunchArgs(launchOptions: LaunchOption): string[]{
const launchArgs = [];
private readonly COMMAND_FORMAT = "%command%";
if(!launchOptions.version.steam && !launchOptions.version.oculus){
launchArgs.push("--no-yeet")
}
if(launchOptions.launchMods?.includes(LaunchMods.OCULUS)) {
launchArgs.push("-vrmode");
launchArgs.push("oculus");
}
if(launchOptions.launchMods?.includes(LaunchMods.FPFC)) {
launchArgs.push("fpfc");
}
if(launchOptions.launchMods?.includes(LaunchMods.DEBUG)) {
launchArgs.push("--verbose");
}
if(launchOptions.launchMods?.includes(LaunchMods.EDITOR)) {
launchArgs.push("editor");
}
if (launchOptions.additionalArgs) {
launchArgs.push(...launchOptions.additionalArgs);
}
return Array.from(new Set(launchArgs).values());
}
protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnOptionsWithoutStdio): ChildProcessWithoutNullStreams {
protected launchBSProcess(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): ChildProcessWithoutNullStreams {
const spawnOptions: SpawnOptionsWithoutStdio = { detached: true, cwd: path.dirname(bsExePath), ...(options || {}) };
@@ -57,7 +60,7 @@ export abstract class AbstractLauncherService {
spawnOptions.shell = true; // For windows to spawn properly
return bsmSpawn(`"${bsExePath}"`, {
args, options: spawnOptions, log: BsmShellLog.Command,
linux: { prefix: this.linux.getProtonPrefix() },
linux: { prefix: options?.protonPrefix || "" },
flatpak: {
host: IS_FLATPAK,
env: [
@@ -70,6 +73,8 @@ export abstract class AbstractLauncherService {
"STEAM_COMPAT_CLIENT_INSTALL_PATH",
"STEAM_COMPAT_APP_ID",
"SteamEnv",
"PROTON_LOG",
"PROTON_LOG_DIR",
],
},
});
@@ -78,7 +83,7 @@ export abstract class AbstractLauncherService {
protected launchBs(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
const process = this.launchBSProcess(bsExePath, args, options);
let timoutId: NodeJS.Timeout;
let timeoutId: NodeJS.Timeout;
const exit = new Promise<number>((resolve, reject) => {
// Don't remove, useful for debugging!
@@ -101,7 +106,7 @@ export abstract class AbstractLauncherService {
const unrefAfter = options?.unrefAfter ?? sToMs(10);
timoutId = setTimeout(() => {
timeoutId = setTimeout(() => {
log.error("BS process unref after timeout", unrefAfter);
process.unref();
process.removeAllListeners();
@@ -109,13 +114,43 @@ export abstract class AbstractLauncherService {
}, unrefAfter);
}).finally(() => {
clearTimeout(timoutId);
clearTimeout(timeoutId);
});
return { process, exit };
}
protected injectAdditionalArgsEnvs(
launchOptions: LaunchOption,
env: Record<string, string>
) {
if (!launchOptions.command) {
return;
}
const { command } = launchOptions;
const index = command.indexOf(this.COMMAND_FORMAT);
if (index === -1) {
return;
}
const envString = command.substring(0, index);
log.info("Parsing env string ", `"${envString}"`)
for (const [ key, value ] of Object.entries(parseEnvString(envString))) {
log.info(
key in env ? "Overriding" : "Injecting",
`${key}="${value}"`,
"to the env launch command"
);
env[key] = value;
}
launchOptions.command = command.substring(index + this.COMMAND_FORMAT.length);
}
}
export type SpawnBsProcessOptions = {
protonPrefix?: string;
unrefAfter?: number;
} & SpawnOptionsWithoutStdio;
@@ -24,6 +24,7 @@ import { LaunchMod, LaunchMods } from "shared/models/bs-launch/launch-option.int
import { StaticConfigurationService } from "../static-configuration.service";
import { SteamService } from "../steam.service";
import { tryit } from "shared/helpers/error.helpers";
import { LinuxService } from "../linux.service";
export class BSLauncherService {
private static instance: BSLauncherService;
@@ -37,6 +38,7 @@ export class BSLauncherService {
private readonly steam: SteamService;
private readonly oculusLauncher: OculusLauncherService;
private readonly staticConfig: StaticConfigurationService;
private readonly linux: LinuxService;
public static getInstance(): BSLauncherService {
if (!BSLauncherService.instance) {
@@ -55,6 +57,7 @@ export class BSLauncherService {
this.oculusLauncher = OculusLauncherService.getInstance();
this.staticConfig = StaticConfigurationService.getInstance();
this.steam = SteamService.getInstance();
this.linux = LinuxService.getInstance();
this.bsmProtocolService.on("launch", link => {
log.info("Launch from bsm protocol", link.toString());
@@ -92,10 +95,6 @@ export class BSLauncherService {
const params = objectFromEntries(shortcutLink.searchParams.entries()) as ShortcutParams;
if(typeof params.additionalArgs === "string"){
params.additionalArgs = [params.additionalArgs];
}
return params;
}
@@ -120,7 +119,7 @@ export class BSLauncherService {
oculus: params.versionOculus === "true",
ino: +params.versionIno
},
additionalArgs: params.additionalArgs,
command: params.command,
launchMods,
};
@@ -138,7 +137,7 @@ export class BSLauncherService {
if(launchOptions.launchMods?.includes(LaunchMods.OCULUS)){ res.oculusMode = "true"; }
if(launchOptions.launchMods?.includes(LaunchMods.FPFC)){ res.desktopMode = "true"; }
if(launchOptions.launchMods?.includes(LaunchMods.DEBUG)){ res.debug = "true"; }
if(launchOptions.additionalArgs){ res.additionalArgs = launchOptions.additionalArgs; }
if(launchOptions.command){ res.command = launchOptions.command; }
if(launchOptions.launchMods?.includes(LaunchMods.SKIP_STEAM)){ res.skipSteam = "true"; }
if(launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)){ res.protonLogs = "true"; }
@@ -182,7 +181,7 @@ export class BSLauncherService {
* @returns {Promise<string>} Path of the icon
*/
private async createShortcutIco(color: Color): Promise<string>{
const pngBuffer = await this.createShortcutPngBuffer(color);
const pngBuffer = this.createShortcutPngBuffer(color);
const icoBuffer = await toIco([pngBuffer]);
await ensureDir(IMAGE_CACHE_PATH);
@@ -208,17 +207,28 @@ export class BSLauncherService {
if(steamShortcut){
const userId = await tryit(() => this.steam.getActiveUser());
const exePath = app.getPath("exe");
return this.steam.createShortcut({
AppName: shortcutName,
Exe: exePath,
StartDir: path.dirname(exePath),
LaunchOptions: this.createLaunchLink(launchOptions),
icon: await this.createShortcutPng(shortcutIconColor),
OpenVR: "\u0001"
}, userId.result).then(() => true).catch(e => {
log.error(e);
return false;
});
const icon = await this.createShortcutPng(shortcutIconColor)
return this.steam.createShortcut(
process.platform === "win32"
? {
AppName: shortcutName,
Exe: exePath,
StartDir: path.dirname(exePath),
LaunchOptions: this.createLaunchLink(launchOptions),
OpenVR: "\u0001", icon,
}
: await this.linux.getSteamShortcutData(
shortcutName, icon, launchOptions,
await this.steam.getSteamPath(),
await this.localVersionService.getVersionPath(launchOptions.version)
),
userId.result
)
.then(() => true)
.catch(e => {
log.error(e);
return false;
});
}
return execOnOs({
@@ -230,12 +240,13 @@ export class BSLauncherService {
description: [shortcutName, launchOptions.version.color].join(" "), // <= Need color in description to help windows know that the shortcut is different
})
),
linux: async () => (
createDesktopUrlShortcut(path.join(app.getPath("desktop"), `${shortcutName}.desktop`), {
name: shortcutName,
url: shortcutUrl,
icon: await this.createShortcutPng(shortcutIconColor),
})
linux: async () => this.linux.createDesktopShortcut(
path.join(app.getPath("desktop"), `${shortcutName}.desktop`),
shortcutName,
await this.createShortcutPng(shortcutIconColor),
launchOptions,
await this.steam.getSteamPath(),
await this.localVersionService.getVersionPath(launchOptions.version)
)
})
@@ -272,7 +283,7 @@ type ShortcutParams = {
oculusMode?: string;
desktopMode?: string;
debug?: string;
additionalArgs?: string[];
command?: string;
skipSteam?: string;
protonLogs?: string;
version: string;
@@ -282,26 +293,3 @@ type ShortcutParams = {
versionOculus?: string;
}
/**
* Create .desktop file for url shortcut (only for linux)
* @param {string} shortcutPath
* @param options
* @returns
*/
function createDesktopUrlShortcut(shortcutPath: string, options?: {
url: string
name: string,
icon: string
}): Promise<boolean> {
const { url, name, icon } = options || {};
const data = [
"[Desktop Entry]",
"Type=Link",
`Name=${name}`,
`Icon=${icon}`,
`URL=${url}`
].join("\n");
return writeFile(shortcutPath, data).then(() => true);
}
@@ -1,4 +1,4 @@
import { Observable, ReplaySubject } from "rxjs";
import { Observable } from "rxjs";
import { StoreLauncherInterface } from "./store-launcher.interface";
import { BSLaunchError, BSLaunchEvent, BSLaunchEventData, LaunchOption } from "../../../shared/models/bs-launch";
import { OculusService } from "../oculus.service";
@@ -6,10 +6,9 @@ import { BS_EXECUTABLE } from "../../constants";
import path from "path";
import log from "electron-log";
import { pathExists } from "fs-extra";
import { AbstractLauncherService } from "./abstract-launcher.service";
import { AbstractLauncherService, buildBsLaunchArgs } from "./abstract-launcher.service";
import { isProcessRunning } from "../../helpers/os.helpers";
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
import { UtilsService } from "../utils.service";
export class OculusLauncherService extends AbstractLauncherService implements StoreLauncherInterface {
@@ -23,14 +22,10 @@ export class OculusLauncherService extends AbstractLauncherService implements St
}
private readonly oculus: OculusService;
private readonly util: UtilsService;
private readonly oculusLib$ = new ReplaySubject<string>();
private constructor() {
super();
this.oculus = OculusService.getInstance();
this.util = UtilsService.getInstance();
}
public launch(launchOptions: LaunchOption): Observable<BSLaunchEventData> {
@@ -54,12 +49,21 @@ export class OculusLauncherService extends AbstractLauncherService implements St
// Make sure Oculus is running
await this.oculus.startOculus().catch(err => log.error("Error while starting Oculus", err));
const env: Record<string, string> = {
...process.env,
};
this.injectAdditionalArgsEnvs(launchOptions, env);
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
// Launch Beat Saber
const process = this.launchBs(exePath, this.buildBsLaunchArgs(launchOptions));
const bsProcess = this.launchBs(
exePath,
buildBsLaunchArgs(launchOptions),
{ env }
);
return process.exit.catch(err => {
return bsProcess.exit.catch(err => {
throw CustomError.fromError(err, BSLaunchError.BS_EXIT_ERROR);
});
@@ -6,12 +6,12 @@ import { SteamService } from "../steam.service";
import path from "path";
import { BS_APP_ID, BS_EXECUTABLE, STEAMVR_APP_ID } from "../../constants";
import log from "electron-log";
import { AbstractLauncherService } from "./abstract-launcher.service";
import { AbstractLauncherService, buildBsLaunchArgs, SpawnBsProcessOptions } from "./abstract-launcher.service";
import { CustomError } from "../../../shared/models/exceptions/custom-error.class";
import { UtilsService } from "../utils.service";
import { exec } from "child_process";
import { exec, ChildProcessWithoutNullStreams } from "child_process";
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
import { app } from "electron";
import { app, Event } from "electron";
export class SteamLauncherService extends AbstractLauncherService implements StoreLauncherInterface{
@@ -64,6 +64,48 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
});
}
protected launchBs(bsExePath: string, args: string[], options?: SpawnBsProcessOptions): {process: ChildProcessWithoutNullStreams, exit: Promise<number>} {
const process = this.launchBSProcess(bsExePath, args, options);
const exit = new Promise<number>((resolve, reject) => {
// Don't remove, useful for debugging!
// process.stdout.on("data", (data) => {
// log.info(`BS stdout: ${data}`);
// });
// process.stderr.on("data", (data) => {
// log.error(`BS stderr: ${data}`);
// });
const onWillQuitHandler = async (event: Event) => {
app.removeListener('will-quit', onWillQuitHandler);
if (!process.killed) {
event.preventDefault();
log.info(`Unref'ing BS process ${process.pid} on app will-quit`);
process.unref();
await this.restoreSteamVR().catch(log.error);
resolve(-1);
app.quit();
}
};
process.on("error", (err) => {
log.error(`Error while launching BS`, err);
reject(err);
app.removeListener('will-quit', onWillQuitHandler);
});
process.on("exit", (code) => {
log.info(`BS process exit with code ${code}`);
resolve(code);
app.removeListener('will-quit', onWillQuitHandler);
});
app.on('will-quit', onWillQuitHandler);
});
return { process, exit };
}
public launch(launchOptions: LaunchOption): Observable<BSLaunchEventData>{
return new Observable<BSLaunchEventData>(obs => {(async () => {
@@ -89,7 +131,7 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
obs.next({type: BSLaunchWarning.UNABLE_TO_LAUNCH_STEAM});
});
}
else {
else if(skipSteam) {
obs.next({ type: BSLaunchEvent.SKIPPING_STEAM_LAUNCH});
}
@@ -102,7 +144,6 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
await this.restoreSteamVR().catch(log.error);
}
const launchArgs = this.buildBsLaunchArgs(launchOptions);
const steamPath = await this.steam.getSteamPath();
const env = {
@@ -112,17 +153,28 @@ export class SteamLauncherService extends AbstractLauncherService implements Sto
"SteamGameId": BS_APP_ID,
};
let protonPrefix = "";
// Linux setup
if (process.platform === "linux") {
await this.linux.setupLaunch(launchOptions, steamPath, bsFolderPath, env);
const linuxSetup = await this.linux.setupLaunch(
launchOptions, steamPath, bsFolderPath
);
protonPrefix = linuxSetup.protonPrefix;
Object.assign(env, linuxSetup.env);
}
this.injectAdditionalArgsEnvs(launchOptions, env);
const launchArgs = buildBsLaunchArgs(launchOptions);
obs.next({type: BSLaunchEvent.BS_LAUNCHING});
const spawnOpts = { env, cwd: bsFolderPath };
const launchPromise = !launchOptions.admin ? (
this.launchBs(bsExePath, launchArgs, spawnOpts).exit
this.launchBs(bsExePath, launchArgs, {
...spawnOpts,
protonPrefix
}).exit
) : (
new Promise<number>(resolve => {
const adminProcess = exec(`"${this.getStartBsAsAdminExePath()}" "${bsExePath}" ${launchArgs.join(" ")} --log-path "${path.join(app.getPath("logs"), "bs-admin-start.log")}"`, spawnOpts);
@@ -106,10 +106,10 @@ export class BSLocalVersionService {
const versionFilePath = path.join(bsPath, 'Beat Saber_Data', 'globalgamemanagers');
const folderVersion = await this.getVersionFromGlobalGameManagerFile(versionFilePath);
folderVersion.path = bsPath;
if(!folderVersion){ return null; }
folderVersion.path = bsPath;
if(options?.steam || options?.oculus){
return {...folderVersion, ...options};
}
@@ -306,14 +306,12 @@ export class BSLocalVersionService {
return versions;
}
public async deleteVersion(version: BSVersion): Promise<boolean>{
if(version.steam || version.oculus){ return false; }
public async deleteVersion(version: BSVersion): Promise<void>{
if(version.steam || version.oculus){ throw new CustomError("BSManager is not able to delete official Beat Saber versions", "CantDeleteOfficialVersion"); }
const versionFolder = await this.getVersionPath(version);
if(!(await pathExists(versionFolder))){ return true; }
if(!(await pathExists(versionFolder))){ throw new CustomError("Version not found", "VersionNotFound"); }
return deleteFolder(versionFolder)
.then(() => { return true; })
.catch(() => { return false; })
return deleteFolder(versionFolder);
}
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
@@ -107,7 +107,7 @@ export class BsOculusDownloaderService {
let downloadVersion: BSVersion
return of(downloadInfo.token).pipe(
switchMap(token => {
switchMap(async token => {
isOculusTokenValid(token, log.info); // Log token validity
if(!downloadInfo.isVerification){
return this.createDownloadVersion(downloadInfo.bsVersion).then(({version, dest}) => ({token, version, dest}))
+2 -11
View File
@@ -5,8 +5,6 @@ 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 { LinuxService } from "./linux.service";
import { IS_FLATPAK } from "main/constants";
import { StaticConfigurationService } from "./static-configuration.service";
export class BSVersionLibService {
@@ -15,7 +13,6 @@ export class BSVersionLibService {
private static instance: BSVersionLibService;
private readonly linuxService: LinuxService;
private readonly utilsService: UtilsService;
private readonly requestService: RequestService;
private readonly configService: StaticConfigurationService;
@@ -23,7 +20,6 @@ export class BSVersionLibService {
private bsVersions: BSVersion[];
private constructor() {
this.linuxService = LinuxService.getInstance();
this.utilsService = UtilsService.getInstance();
this.requestService = RequestService.getInstance();
this.configService = StaticConfigurationService.getInstance();
@@ -40,15 +36,10 @@ export class BSVersionLibService {
return this.requestService.getJSON<BSVersion[]>(this.REMOTE_BS_VERSIONS_URL).then(res => res.data);
}
private async shouldLoadFromConfig(): Promise<boolean> {
// Some special cases of readonly memory installations
return process.platform === "linux" && (IS_FLATPAK || this.linuxService.isNixOS());
}
private async getLocalVersions(): Promise<BSVersion[]> {
const localVersionsPath = path.join(this.utilsService.getAssestsJsonsPath(), this.VERSIONS_FILE);
if (!(await this.shouldLoadFromConfig())) {
if (process.platform !== "linux") {
return readJSON(localVersionsPath);
}
@@ -60,7 +51,7 @@ export class BSVersionLibService {
}
private async updateLocalVersions(versions: BSVersion[]): Promise<void> {
if (await this.shouldLoadFromConfig()) {
if (process.platform === "linux") {
this.configService.set("versions", versions);
return;
}
+34 -4
View File
@@ -1,12 +1,15 @@
import { InstallationLocationService } from "./installation-location.service";
import log from "electron-log";
import { deleteFolder, ensureFolderExist, moveFolderContent, pathExist, unlinkPath } from "../helpers/fs.helpers";
import { deleteFileSync, deleteFolder, deleteFolderSync, ensureFolderExist, moveFolderContent, pathExist } from "../helpers/fs.helpers";
import { lstat, symlink } from "fs/promises";
import path from "path";
import { copy, readlink } from "fs-extra";
import { copy, mkdirSync, readlink, symlinkSync } from "fs-extra";
import { lastValueFrom } from "rxjs";
import { noop } from "shared/helpers/function.helpers";
import { StaticConfigurationService } from "./static-configuration.service";
import { randomUUID } from "crypto";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { tryit } from "shared/helpers/error.helpers";
export class FolderLinkerService {
private static instance: FolderLinkerService;
@@ -41,6 +44,29 @@ export class FolderLinkerService {
}
}
public isLinkingSupported(): boolean {
const uuid = randomUUID();
const installationPath = this.installLocationService.installationDirectory();
const testFolder = path.join(installationPath, uuid);
const testLink = path.join(installationPath, `${uuid}_link`);
const resLink = tryit(() => {
mkdirSync(testFolder);
symlinkSync(testFolder, testLink, this.getLinkingType());
});
tryit(() => {
deleteFolderSync(testFolder);
deleteFileSync(testLink);
});
if(resLink.error){
log.error("Unable to create symlink", resLink.error);
}
return !resLink.error;
}
public sharedFolder(): string {
return this.installLocationService.sharedContentPath();
}
@@ -74,6 +100,10 @@ export class FolderLinkerService {
}
public async linkFolder(folderPath: string, options?: LinkOptions): Promise<void> {
if(!this.isLinkingSupported()){
throw new CustomError("Linking is not supported on this platform", "LinkingNotSupported");
}
const sharedPath = await this.getSharedFolder(folderPath, options?.intermediateFolder);
if (await this.isFolderSymlink(folderPath)) {
@@ -83,7 +113,7 @@ export class FolderLinkerService {
if (isTargetedToSharedPath) {
return;
}
await unlinkPath(folderPath);
await deleteFolder(folderPath);
log.info(`Linking ${folderPath} to ${sharedPath}; type: ${this.linkingType}`);
return symlink(sharedPath, folderPath, this.getLinkingType());
@@ -111,7 +141,7 @@ export class FolderLinkerService {
if (!(await this.isFolderSymlink(folderPath))) {
return;
}
await unlinkPath(folderPath);
await deleteFolder(folderPath);
const sharedPath = await this.getSharedFolder(folderPath, options?.intermediateFolder);
+149 -39
View File
@@ -1,13 +1,15 @@
import fs from "fs-extra";
import log from "electron-log";
import path from "path";
import { BS_APP_ID, IS_FLATPAK, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
import { BS_APP_ID, BS_EXECUTABLE, IS_FLATPAK, PROTON_BINARY_PREFIX, WINE_BINARY_PREFIX } from "main/constants";
import { InstallationLocationService } from "./installation-location.service";
import { StaticConfigurationService } from "./static-configuration.service";
import { SteamService } from "./steam.service";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { BSLaunchError, LaunchOption } from "shared/models/bs-launch";
import { BsmShellLog, bsmExec } from "main/helpers/os.helpers";
import { LaunchMods } from "shared/models/bs-launch/launch-option.interface";
import { SteamShortcutData } from "shared/models/steam/shortcut.model";
import { buildBsLaunchArgs } from "./bs-launcher/abstract-launcher.service";
export class LinuxService {
private static instance: LinuxService;
@@ -19,46 +21,46 @@ export class LinuxService {
return LinuxService.instance;
}
private readonly installLocationService: InstallationLocationService;
private readonly staticConfig: StaticConfigurationService;
private readonly steamService: SteamService;
private protonPrefix = "";
private nixOS: boolean | undefined;
private constructor() {
this.installLocationService = InstallationLocationService.getInstance();
this.staticConfig = StaticConfigurationService.getInstance();
this.steamService = SteamService.getInstance();
}
// === Launching === //
private async getCompatDataPath() {
// Create the compat data path if it doesn't exist.
// If the user never ran Beat Saber through steam before
// using bsmanager, it won't exist, and proton will fail
// to launch the game.
const commonFolder = await this.steamService.getGameFolder(BS_APP_ID);
const compatDataPath = path.resolve(commonFolder, "..", "compatdata", BS_APP_ID);
if (!fs.existsSync(compatDataPath)) {
log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`);
fs.mkdirSync(compatDataPath);
}
return compatDataPath;
private getCompatDataPath() {
const sharedFolder = this.installLocationService.sharedContentPath();
return path.resolve(sharedFolder, "compatdata");
}
public async setupLaunch(
launchOptions: LaunchOption,
steamPath: string,
bsFolderPath: string,
env: Record<string, string>
) {
bsFolderPath: string
): Promise<{
protonPrefix: string;
env: Record<string, string>;
}> {
if (launchOptions.admin) {
log.warn("Launching as admin is not supported on Linux! Starting the game as a normal user.");
launchOptions.admin = false;
}
const compatDataPath = this.getCompatDataPath();
const protonPath = await this.getProtonPath();
return {
protonPrefix: await this.isNixOS()
? `steam-run "${protonPath}" run`
: `"${protonPath}" run`,
env: await this.buildEnvVariables(launchOptions, steamPath, bsFolderPath)
};
}
private async getProtonPath(): Promise<string> {
if (!this.staticConfig.has("proton-folder")) {
throw CustomError.fromError(
new Error("Proton folder not set"),
@@ -76,27 +78,41 @@ export class LinuxService {
);
}
this.protonPrefix = await this.isNixOS()
? `steam-run "${protonPath}" run`
: `"${protonPath}" run`;
return protonPath;
}
private async buildEnvVariables(
launchOptions: LaunchOption,
steamPath: string,
bsFolderPath: string
): Promise<Record<string, string>> {
// Create the compat data path if it doesn't exist.
// If the user never ran Beat Saber through steam before
// using bsmanager, it won't exist, and proton will fail
// to launch the game.
const compatDataPath = this.getCompatDataPath();
if (!fs.existsSync(compatDataPath)) {
log.info(`Proton compat data path not found at '${compatDataPath}', creating directory`);
await fs.ensureDir(compatDataPath);
}
// Setup Proton environment variables
Object.assign(env, {
const envVars: Record<string, string> = {
"WINEDLLOVERRIDES": "winhttp=n,b", // Required for mods to work
"STEAM_COMPAT_DATA_PATH": compatDataPath,
"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,
});
"SteamEnv": "1",
};
if (launchOptions.launchMods?.includes(LaunchMods.PROTON_LOGS)) {
Object.assign(env, {
"PROTON_LOG": 1,
"PROTON_LOG_DIR": path.join(bsFolderPath, "Logs"),
});
envVars.PROTON_LOG = "1";
envVars.PROTON_LOG_DIR = path.join(bsFolderPath, "Logs");
}
return envVars;
}
public verifyProtonPath(protonFolder: string = ""): boolean {
@@ -113,7 +129,12 @@ export class LinuxService {
return fs.pathExistsSync(protonPath) && fs.pathExistsSync(winePath);
}
public getWinePath(): string {
public async getWinePath(): Promise<string> {
if (await this.isNixOS()) {
// Use system wine for nixos
return "wine";
}
if (!this.staticConfig.has("proton-folder")) {
throw new Error("proton-folder variable not set");
}
@@ -129,13 +150,10 @@ export class LinuxService {
return winePath;
}
public async getWinePrefixPath(): Promise<string> {
return path.join(await this.getCompatDataPath(), "pfx");
}
public getProtonPrefix(): string {
// Set in setupLaunch
return this.protonPrefix;
public getWinePrefixPath(): string {
const compatDataPath = this.getCompatDataPath();
return fs.existsSync(compatDataPath)
? path.join(compatDataPath, "pfx") : "";
}
// === NixOS Specific === //
@@ -158,4 +176,96 @@ export class LinuxService {
return this.nixOS;
}
// === Shortcuts === //
private getCommand(
protonPrefix: string,
bsFolderPath: string,
env: Record<string, string>,
launchOptions: LaunchOption
): string {
const envString = Object.entries(env)
.map(([ key, value ]) => `${key}="${value}"`)
.join(" ");
const bsExe = path.join(bsFolderPath, BS_EXECUTABLE);
const args = buildBsLaunchArgs(launchOptions).join(" ");
return `${envString} ${protonPrefix} "${bsExe}" ${args}`;
}
public async createDesktopShortcut(
shortcutPath: string,
name: string,
icon: string,
launchOptions: LaunchOption,
steamPath: string,
bsFolderPath: string
): Promise<boolean> {
try {
const {
protonPrefix, env
} = await this.setupLaunch(launchOptions, steamPath, bsFolderPath);
Object.assign(env, {
"SteamAppId": BS_APP_ID,
"SteamOverlayGameId": BS_APP_ID,
"SteamGameId": BS_APP_ID,
});
const command = this.getCommand(
protonPrefix, bsFolderPath,
env, launchOptions
);
const desktopEntry = [
"[Desktop Entry]",
"Type=Application",
`Name=${name}`,
`Icon=${icon}`,
`Path=${bsFolderPath}`,
`Exec=${command}`
].join("\n");
await fs.writeFile(shortcutPath, desktopEntry);
log.info("Created shorcut at ", `"${shortcutPath}/${name}"`);
return true;
} catch (error) {
log.error("Could not create shortcut", error);
return false;
}
}
public async getSteamShortcutData(
shortcutName: string,
icon: string,
launchOptions: LaunchOption,
steamPath: string,
bsFolderPath: string
): Promise<SteamShortcutData> {
const env = await this.buildEnvVariables(
launchOptions, steamPath, bsFolderPath
);
Object.assign(env, {
"SteamAppId": BS_APP_ID,
"SteamOverlayGameId": BS_APP_ID,
"SteamGameId": BS_APP_ID,
});
const protonPrefix = await this.isNixOS()
? "steam-run %command% run"
: "%command% run";
return {
AppName: shortcutName,
Exe: await this.getProtonPath(),
StartDir: bsFolderPath,
icon,
OpenVR: "\x01",
LaunchOptions: this.getCommand(
protonPrefix, bsFolderPath,
env, launchOptions
)
};
}
}
@@ -26,6 +26,17 @@ export class BeatModsApiService {
this.requestService = RequestService.getInstance();
}
public async isUp(): Promise<boolean> {
try {
// The data in status can be dropped
await this.requestService.getJSON<{}>(`${this.MODS_REPO_API_URL}/status`);
return true;
} catch (error) {
log.error("Could not connect to beatmods", error);
return false;
}
}
private getVersionModsUrl(version: BSVersion): string {
const platform: BbmPlatform = version.oculus || version.metadata?.store === BsStore.OCULUS ? BbmPlatform.OculusPC : BbmPlatform.SteamPC;
return `${this.MODS_REPO_API_URL}/mods?status=verified&gameVersion=${version.BSVersion}&gameName=BeatSaber&platform=${platform}`;
@@ -59,17 +70,17 @@ export class BeatModsApiService {
});
}
public getModByHash(hash: string): Promise<BbmModVersion|undefined> {
public async getModByHash(hash: string): Promise<BbmModVersion> {
if (this.modsHashCache.has(hash)) {
return Promise.resolve(this.modsHashCache.get(hash));
}
return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`).then(({ data }) => {
return this.requestService.getJSON<{ modVersions: BbmModVersion[] }>(
`${this.MODS_REPO_API_URL}/hashlookup?hash=${hash}`,
{ silentError: true }
).then(({ data }) => {
this.updateModsHashCache(data?.modVersions ?? []);
return data?.modVersions?.at(0);
}).catch((e): undefined => {
log.error(`Failed to get mod by hash: ${hash}`, e);
return undefined;
});
}
}
+127 -50
View File
@@ -6,11 +6,11 @@ import md5File from "md5-file";
import { RequestService } from "../request.service";
import { BS_EXECUTABLE } from "../../constants";
import log from "electron-log";
import { deleteFolder, pathExist, Progression, unlinkPath } from "../../helpers/fs.helpers";
import { deleteFile, deleteFolder, pathExist, Progression } from "../../helpers/fs.helpers";
import { lastValueFrom, Observable } from "rxjs";
import recursiveReadDir from "recursive-readdir";
import { sToMs } from "../../../shared/helpers/time.helpers";
import { copyFile, ensureDir, pathExistsSync } from "fs-extra";
import { copyFile, ensureDir, pathExistsSync, readdirSync } from "fs-extra";
import { CustomError } from "shared/models/exceptions/custom-error.class";
import { popElement } from "shared/helpers/array.helpers";
import { LinuxService } from "../linux.service";
@@ -28,8 +28,6 @@ export class BsModsManagerService {
private readonly linuxService: LinuxService;
private readonly requestService: RequestService;
private manifestMatches: BbmModVersion[];
public static getInstance(): BsModsManagerService {
if (!BsModsManagerService.instance) {
BsModsManagerService.instance = new BsModsManagerService();
@@ -45,7 +43,11 @@ export class BsModsManagerService {
}
private async getModFromHash(hash: string): Promise<BbmModVersion | undefined> {
const mod = await this.beatModsApi.getModByHash(hash);
const mod: BbmModVersion | undefined = await this.beatModsApi.getModByHash(hash)
.catch((error): undefined => {
log.warn("Could not get mod with hash", hash, "cause", error?.message);
return undefined;
});
if(mod?.contentHashes?.some(content => content.path.includes("IPA.exe"))){
return undefined;
@@ -55,7 +57,11 @@ export class BsModsManagerService {
}
private async getModsInDir(version: BSVersion, modsDir: ModsInstallFolder): Promise<BbmModVersion[]> {
private async getModsInDir(
version: BSVersion,
modsDir: ModsInstallFolder,
manifestMatches: BbmModVersion[]
): Promise<BbmModVersion[]> {
const bsPath = await this.bsLocalService.getVersionPath(version);
const modsPath = path.join(bsPath, modsDir);
@@ -66,38 +72,47 @@ export class BsModsManagerService {
const files = await recursiveReadDir(modsPath);
const promises = files.map(async filePath => {
const ext = path.extname(filePath);
const ext = path.extname(filePath);
if (ext !== ".dll" && ext !== ".exe" && ext !== ".manifest") {
return undefined;
}
if (ext !== ".dll" && ext !== ".exe" && ext !== ".manifest") {
return undefined;
}
const hash = await md5File(filePath);
const mod = await this.getModFromHash(hash);
log.info("Getting mod manifest", filePath);
const hash = await md5File(filePath);
const mod = await this.getModFromHash(hash);
if (!mod) {
if (!mod) {
return undefined;
}
if (ext === ".manifest") {
manifestMatches.push(mod);
return undefined;
}
if (modsDir === ModsInstallFolder.LIBS || modsDir === ModsInstallFolder.LIBS_PENDING) {
const manifestIndex = manifestMatches.findIndex(m => m.id === mod.id);
if (manifestIndex < 0) {
log.warn("No matching manifest for", `"${filePath}"`, "with hash:", hash);
return undefined;
}
if (ext === ".manifest") {
this.manifestMatches.push(mod);
return undefined;
}
manifestMatches.splice(manifestIndex, 1);
}
if (filePath.toLowerCase().includes("libs")) {
const manifestIndex = this.manifestMatches.findIndex(m => m.id === mod.id);
if (manifestIndex < 0) {
return undefined;
}
this.manifestMatches.splice(manifestIndex, 1);
}
return mod;
return mod;
});
const mods = await Promise.all(promises);
return mods.filter(Boolean);
const results = await Promise.allSettled(promises);
return results.reduce((mods, mod) => {
if (mod?.status === "fulfilled" && mod?.value) {
mods.push(mod.value);
} else if (mod?.status === "rejected") {
log.error("MOD FAILED", mod);
}
return mods;
}, [] as BbmModVersion[]);
}
private async getBsipaInstalled(version: BSVersion): Promise<BbmModVersion> {
@@ -107,11 +122,14 @@ export class BsModsManagerService {
return undefined;
}
const injectorMd5 = await md5File(injectorPath);
return this.beatModsApi.getModByHash(injectorMd5);
return this.beatModsApi.getModByHash(injectorMd5).catch((error): undefined => {
log.error("Could not get bsipa mod", error?.message);
return undefined;
});
}
private async downloadZip(zipUrl: string): Promise<BsmZipExtractor> {
zipUrl = path.join(this.beatModsApi.MODS_REPO_URL, zipUrl);
zipUrl = new URL(zipUrl, this.beatModsApi.MODS_REPO_URL).href;
log.info("Download mod zip", zipUrl);
@@ -144,20 +162,18 @@ export class BsModsManagerService {
let winePath: string = "";
if (process.platform === "linux") {
const { error: winePathError, result: winePathResult } =
tryit(() => this.linuxService.getWinePath());
await tryit(async () => this.linuxService.getWinePath());
if (winePathError) {
log.error(winePathError);
return false;
}
winePath = `"${winePathResult}"`;
const { error: winePrefixError, result: winePrefixResult } =
await tryit(async () => this.linuxService.getWinePrefixPath());
if (winePrefixError) {
log.warn("Could not get WINEPREFIX value", winePrefixError);
} else {
env.WINEPREFIX = winePrefixResult;
const winePrefix = this.linuxService.getWinePrefixPath();
if (!winePrefix) {
throw new CustomError("Could not find BSManager WINEPREFIX path", "no-wineprefix");
}
env.WINEPREFIX = winePrefix;
}
return new Promise<boolean>(resolve => {
@@ -204,6 +220,12 @@ export class BsModsManagerService {
private async installMod(mod: BbmFullMod, version: BSVersion): Promise<boolean> {
log.info("INSTALL MOD", mod.mod.name, "for version", `${version.BSVersion} - ${version.name}`);
const isBSIPA = mod.mod.name.toLowerCase() === "bsipa";
if(isBSIPA){
await this.clearIpaFolder(version).catch(e => log.error("Error while clearing IPA folder", e));
}
const downloadUrl = this.getModDownload(mod.version);
if (!downloadUrl) {
@@ -232,7 +254,6 @@ export class BsModsManagerService {
}
const versionPath = await this.bsLocalService.getVersionPath(version);
const isBSIPA = mod.mod.name.toLowerCase() === "bsipa";
const destDir = isBSIPA ? versionPath : path.join(versionPath, ModsInstallFolder.PENDING);
log.info("Start extracting mod zip", mod.mod.name, "to", destDir);
@@ -259,6 +280,41 @@ export class BsModsManagerService {
return res;
}
private async clearIpaFolder(version: BSVersion): Promise<void> {
log.info("Clearing IPA folder");
const versionPath = await this.bsLocalService.getVersionPath(version);
const ipaPath = path.join(versionPath, ModsInstallFolder.IPA);
if(!pathExistsSync(ipaPath)){
log.info("IPA folder does not exist, skipping");
return;
}
const contents = readdirSync(ipaPath, { withFileTypes: true });
for(const content of contents){
if (content.name === 'Backups' || content.name === 'Pending') {
continue;
}
const contentPath = path.join(ipaPath, content.name);
const res = await tryit(() => content.isDirectory()
? deleteFolder(contentPath)
: deleteFile(contentPath)
);
if(res.error){
log.error("Error while clearing IPA folder content", content.name, res.error);
}
}
log.info("IPA folder cleared successfully");
}
private async uninstallBSIPA(mod: BbmFullMod, version: BSVersion): Promise<void> {
const verionPath = await this.bsLocalService.getVersionPath(version);
const hasIPAExe = await pathExist(path.join(verionPath, "IPA.exe"));
@@ -272,7 +328,7 @@ export class BsModsManagerService {
const promises = mod.version.contentHashes.map(content => {
const file = content.path.replaceAll("IPA/", "").replaceAll("Data", "Beat Saber_Data");
return unlinkPath(path.join(verionPath, file));
return deleteFile(path.join(verionPath, file));
});
await Promise.all(promises);
@@ -284,8 +340,22 @@ export class BsModsManagerService {
const versionPath = await this.bsLocalService.getVersionPath(version);
const promises = mod.version.contentHashes.map(async content => {
return Promise.all([unlinkPath(path.join(versionPath, content.path)), unlinkPath(path.join(versionPath, "IPA", "Pending", content.path))]);
const promises: Promise<void>[] = mod.version.contentHashes.map(async content => {
return (async () => {
const modPath = path.join(versionPath, content.path);
if(pathExistsSync(modPath)){
log.info("Deleting mod", modPath);
await deleteFile(modPath);
}
const pendingPath = path.join(versionPath, "IPA", "Pending", content.path);
if(pathExistsSync(pendingPath)){
log.info("Deleting pending mod", pendingPath);
return deleteFile(pendingPath);
}
log.warn("Mod file not found in", versionPath, "or pending folder", pendingPath);
})() ;
});
await Promise.all(promises);
@@ -298,12 +368,17 @@ export class BsModsManagerService {
}
public async getInstalledMods(version: BSVersion): Promise<BbmModVersion[]> {
this.manifestMatches = [];
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)]);
const manifestMatches: BbmModVersion[] = [];
const pluginsMods = await Promise.all([
this.getModsInDir(version, ModsInstallFolder.PLUGINS_PENDING, manifestMatches),
this.getModsInDir(version, ModsInstallFolder.PLUGINS, manifestMatches)
]);
const libsMods = await Promise.all([
this.getModsInDir(version, ModsInstallFolder.LIBS_PENDING, manifestMatches),
this.getModsInDir(version, ModsInstallFolder.LIBS, manifestMatches)
]);
const dirMods = pluginsMods.flat().concat(libsMods.flat());
@@ -491,9 +566,11 @@ export class BsModsManagerService {
const versionMods = await this.getAvailableMods(version);
const installedMods = await this.getInstalledMods(version);
const fullInstalledMods: BbmFullMod[] = installedMods?.map(version => {
return { version, mod: versionMods.find(mod => version.modId === mod.mod.id)?.mod };
}) ?? [];
const fullInstalledMods = (installedMods || []).reduce((mods, version) => {
const mod = versionMods.find(mod => version.modId === mod.mod.id)?.mod;
if (mod) { mods.push({ version, mod }); }
return mods;
}, [] as BbmFullMod[]);
const progress = { current: 0, total: fullInstalledMods.length };
+77 -110
View File
@@ -1,49 +1,47 @@
import { createWriteStream, WriteStream } from 'fs';
import { Progression } from 'main/helpers/fs.helpers';
import { deleteFileSync, Progression } from 'main/helpers/fs.helpers';
import { Observable } from 'rxjs';
import { shareReplay, tap } from 'rxjs/operators';
import log from 'electron-log';
import got from 'got';
import { IncomingHttpHeaders, IncomingMessage } from 'http';
import { unlinkSync } from 'fs-extra';
import { tryit } from 'shared/helpers/error.helpers';
import path from 'path';
import { pipeline } from 'stream/promises';
import sanitize from 'sanitize-filename';
import internal from 'stream';
import { app } from 'electron';
export class RequestService {
private static instance: RequestService;
private preferredFamily: number | undefined = undefined;
private readonly baseHeaders = {
'User-Agent': `BSManager/${app.getVersion()} (Electron/${process.versions.electron} Chrome/${process.versions.chrome} Node/${process.versions.node})`,
}
public static getInstance(): RequestService {
if (!RequestService.instance) {
RequestService.instance = new RequestService();
}
return RequestService.instance;
}
private constructor() {}
public async getJSON<T = unknown>(url: string): Promise<{ data: T; headers: IncomingHttpHeaders }> {
public async getJSON<T = unknown>(url: string, options?: {
silentError?: boolean
}): Promise<{ data: T; headers: IncomingHttpHeaders }> {
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
for (const family of familiesToTry) {
try {
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
const res = await got(url, { dnsLookupIpVersion: family, responseType: 'json' });
this.preferredFamily = family;
return { data: res.body as T, headers: res.headers };
} catch (err) {
log.warn(`IPv${family} request failed, trying next one... URL: ${url}`, err);
try {
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
const res = await got(url, { responseType: 'json', headers: this.baseHeaders });
return { data: res.body as T, headers: res.headers };
} catch (err) {
if (options?.silentError !== true) {
log.error(`Failed to get JSON from URL: ${url}`, err);
}
throw err;
}
log.error(`IPv4 and IPv6 requests failed for URL: ${url}`);
this.preferredFamily = undefined;
throw new Error(`IPv4 and IPv6 requests failed for URL: ${url}`);
}
public downloadFile(
@@ -53,63 +51,47 @@ export class RequestService {
): Observable<Progression<string>> {
return new Observable<Progression<string>>((subscriber) => {
const progress: Progression<string> = { current: 0, total: 0 };
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
let attempt = 0;
let stream: got.GotEmitter & internal.Duplex;
let file: WriteStream | undefined;
const tryNextFamily = () => {
if (attempt >= familiesToTry.length) {
subscriber.error(new Error(`Download failed over IPv4 and IPv6 for URL: ${url}`));
return;
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
const stream = got.stream(url, { headers: this.baseHeaders });
stream.on('response', (response) => {
const filename = opt?.preferContentDisposition ? this.getFilenameFromContentDisposition(response.headers['content-disposition']) : null;
if (filename) {
dest = path.join(path.dirname(dest), sanitize(filename));
}
const family = familiesToTry[attempt++];
let file: WriteStream | undefined;
progress.data = dest;
file = createWriteStream(dest);
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
stream = got.stream(url, { dnsLookupIpVersion: family });
stream.on('response', (response) => {
this.preferredFamily = family;
const filename = opt?.preferContentDisposition ? this.getFilenameFromContentDisposition(response.headers['content-disposition']) : null;
if (filename) {
dest = path.join(path.dirname(dest), sanitize(filename));
}
progress.data = dest;
file = createWriteStream(dest);
pipeline(stream, file).catch(err => {
file?.destroy();
tryit(() => unlinkSync(dest));
subscriber.error(err);
});
});
stream.on('downloadProgress', ({ transferred, total }) => {
progress.current = transferred;
progress.total = total;
subscriber.next(progress);
});
stream.on('error', err => {
log.warn(`Download failed over IPv${family} for URL: ${url}`, err);
stream.destroy();
pipeline(stream, file).catch(err => {
file?.destroy();
tryNextFamily();
tryit(() => deleteFileSync(dest));
subscriber.error(err);
});
});
stream.on('end', () => {
file?.end();
subscriber.next(progress);
subscriber.complete();
});
};
stream.on('downloadProgress', ({ transferred, total }) => {
progress.current = transferred;
progress.total = total;
subscriber.next(progress);
});
tryNextFamily();
stream.on('error', err => {
log.error(`Download failed for URL: ${url}`, err);
stream.destroy();
file?.destroy();
});
stream.on('end', () => {
file?.end();
subscriber.next(progress);
subscriber.complete();
});
return () => {
stream?.destroy();
@@ -124,6 +106,7 @@ export class RequestService {
url: string,
options?: got.GotOptions<null>
): Observable<Progression<Buffer, IncomingMessage>> {
return new Observable<Progression<Buffer, IncomingMessage>>((subscriber) => {
const progress: Progression<Buffer, IncomingMessage> = {
current: 0,
@@ -131,55 +114,39 @@ export class RequestService {
data: null,
};
const familiesToTry = this.preferredFamily ? [this.preferredFamily, this.preferredFamily === 4 ? 6 : 4] : [4, 6];
const headers = { ...this.baseHeaders, ...(options?.headers ?? {}) };
let attempt = 0;
let stream: got.GotEmitter & internal.Duplex;
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
const stream = got.stream(url, { ...(options ?? {}), headers });
const tryNextFamily = () => {
if (attempt >= familiesToTry.length) {
subscriber.error(new Error(`Download failed over IPv4 and IPv6 for URL: ${url}`));
return;
}
let data = Buffer.alloc(0);
let response: IncomingMessage;
const family = familiesToTry[attempt++];
stream.once('response', (res) => {
response = res;
});
// @ts-ignore (ESM is not well supported in this project, We need to move out electron-react-boilerplate, and use Vite)
stream = got.stream(url, { dnsLookupIpVersion: family, ...(options ?? {}) });
stream.on('data', (chunk: Buffer) => {
data = Buffer.concat([data, chunk]);
});
let data = Buffer.alloc(0);
let response: IncomingMessage;
stream.on('downloadProgress', ({ transferred, total }) => {
progress.current = transferred;
progress.total = total;
subscriber.next(progress);
});
stream.once('response', (res) => {
this.preferredFamily = family;
response = res;
});
stream.on('error', err => {
log.error(`Download failed for URL: ${url}`, err);
stream.destroy();
});
stream.on('data', (chunk: Buffer) => {
data = Buffer.concat([data, chunk]);
});
stream.on('downloadProgress', ({ transferred, total }) => {
progress.current = transferred;
progress.total = total;
subscriber.next(progress);
});
stream.on('error', err => {
log.warn(`Download failed over IPv${family} for URL: ${url}`, err);
stream.destroy();
tryNextFamily();
});
stream.on('end', () => {
progress.data = data;
progress.extra = response;
subscriber.next(progress);
subscriber.complete();
});
};
tryNextFamily();
stream.on('end', () => {
progress.data = data;
progress.extra = response;
subscriber.next(progress);
subscriber.complete();
});
return () => {
stream?.destroy();
+1 -1
View File
@@ -211,7 +211,7 @@ export class SteamService {
for (const userId of userIds) {
const shortcuts = await this.getShortcuts(userId).catch(e => {
log.warn("Error while reading shortcuts", e);
return [];
return [] as SteamShortcut[];
});
shortcuts.push(new SteamShortcut(shortcutData));
@@ -21,7 +21,6 @@ import { useObservable } from "renderer/hooks/use-observable.hook";
import { PlaylistDownloaderService } from "renderer/services/playlist-downloader.service";
import { NotificationService } from "renderer/services/notification.service";
import { DeletePlaylistModal } from "renderer/components/modal/modal-types/playlist/delete-playlist-modal.component";
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
import { PlaylistItemComponentPropsMapper } from "shared/mappers/playlist/playlist-item-component-props.mapper";
import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual-scroll.component";
import { LocalPlaylistFilter, LocalPlaylistSort } from "./local-playlist-filter-panel.component";
@@ -75,10 +74,8 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
const playlistDownloader = useService(PlaylistDownloaderService);
const modals = useService(ModalService);
const ipc = useService(IpcService);
const osDiagnostic = useService(OsDiagnosticService);
const notification = useService(NotificationService);
const isOnline = useObservable(() => osDiagnostic.isOnline$, false);
const isActiveOnce = useChangeUntilEqual(isActive, { untilEqual: true });
const { maps$, playlists$, setPlaylists, setMaps } = useContext(InstalledMapsContext);
@@ -121,7 +118,6 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
},
syncPlaylists: async (playlists?: LocalBPList[]) => {
if(!isOnline){ return; }
const toSync = playlists ?? (selectedPlaylists$.value?.length ? selectedPlaylists$.value : playlists$.value);
if(!toSync.length){ return; }
@@ -391,13 +387,13 @@ export const LocalPlaylistsListPanel = forwardRef<LocalPlaylistsListRef, Props>(
}}
onClickOpen={() => openPlaylistDetails(playlist.path)}
onClickDelete={() => deletePlaylists([playlist])}
onClickSync={(playlist?.songs?.length && isOnline) && (() => handleClickSync(playlist))}
onClickSync={!!playlist?.songs?.length && (() => handleClickSync(playlist))}
onClickOpenFile={() => viewPlaylistFile(playlist.path)}
onClickCancelDownload={() => playlistDownloader.cancelDownload(playlist.customData?.syncURL ?? playlist.path, version)}
onClickEdit={() => editPlaylist(playlist)}
/>
);
}, [isOnline, version]);
}, [version]);
const filteredPlaylists = useMemo(() => {
if(!playlists){ return []; }
@@ -15,7 +15,7 @@ import { NotificationService } from "renderer/services/notification.service";
export const LoginToMetaModal: ModalComponent<string> = ({ resolver }) => {
const { text: t } = useTranslationV2();
const { text: t, element: te } = useTranslationV2();
const ipc = useService(IpcService);
const modal = useService(ModalService);
@@ -60,6 +60,12 @@ export const LoginToMetaModal: ModalComponent<string> = ({ resolver }) => {
<p className="font-bold">{t("modals.connect-to-meta.body.token-needed")}</p>
<p>{t("modals.connect-to-meta.body.need-cookie-enabled")}</p>
<p className="text-warning-400">{(
te("modals.connect-to-meta.body.must-own-desktop-version", {link: (
<a className="underline cursor-pointer" href="https://github.com/Zagrios/bs-manager/wiki/how-to-claim-oculus-desktop-version" target="_blank">{t("misc.this-guide")}</a>
)})
)}</p>
<div className="w-full flex flex-row justify-start items-center gap-1.5">
<BsmCheckbox className="relative z-[1] w-6 aspect-square" checked={stay} onChange={enable => setStay(() => enable)}/>
<span>{t("modals.connect-to-meta.stay")}</span>
@@ -0,0 +1,60 @@
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
import BeatRunning from "../../../../../assets/images/apngs/beat-running.png";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import { CustomLaunchOption } from "renderer/components/version-viewer/slides/launch/launch-options-panel.component";
import { FormEvent, useState } from "react";
import { useConstant } from "renderer/hooks/use-constant.hook";
type Data = Partial<CustomLaunchOption>;
export const CreateCustomLaunchOptionModal: ModalComponent<CustomLaunchOption, Data> = ({resolver, options}) => {
const { text: t } = useTranslationV2();
const uid = useConstant(() => options?.data?.id ?? crypto.randomUUID());
const [name, setName] = useState(options?.data?.label ?? "");
const [command, setCommand] = useState(options?.data?.data?.command ?? "");
const onSubmit = (e: FormEvent<HTMLFormElement>) => {
e.preventDefault();
resolver({
exitCode: ModalExitCode.COMPLETED,
data: {
id: uid,
label: name,
data: {
command,
}
}
});
}
const cancel = () => {
resolver({
exitCode: ModalExitCode.CANCELED,
data: null,
});
}
return (
<form onSubmit={onSubmit} className="max-w-xs">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("modals.custom-launch-option.title")}</h1>
<BsmImage className="mx-auto h-24" image={BeatRunning} />
<p className="mb-2">{t("modals.custom-launch-option.desc")}</p>
<div className="flex flex-col gap-px mb-3">
<label className="font-bold cursor-pointer tracking-wide inline-block w-fit" htmlFor="name">{t("modals.custom-launch-option.name")}</label>
<input className="h-8 px-1.5 bg-light-main-color-1 dark:bg-main-color-1 rounded-md" type="text" id="name" required value={name} onChange={e => setName(e.target.value)} placeholder={t("modals.custom-launch-option.title-placeholder")} />
</div>
<div className="flex flex-col gap-px mb-3">
<label className="font-bold cursor-pointer tracking-wide inline-block w-fit" htmlFor="launch-option">{t("modals.custom-launch-option.launch-option")}</label>
<input className="h-8 px-1.5 bg-light-main-color-1 dark:bg-main-color-1 rounded-md" type="text" id="launch-option" required value={command} onChange={e => setCommand(e.target.value)} placeholder="--launch-option" />
</div>
<div className="flex gap-3">
<BsmButton className="rounded-md flex justify-center items-center transition-all h-10 grow shrink-0" text={t("misc.cancel")} typeColor="cancel" withBar={false} onClick={cancel}/>
<BsmButton className="rounded-md flex justify-center items-center transition-all h-10 grow shrink-0" type="submit" text={t("misc.create")} typeColor="primary" withBar={false} />
</div>
</form>
)
};
@@ -22,8 +22,8 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean,
const color = useThemeColor("second-color");
const [launchOption, setLaunchOptions] = useState(bsLauncher.getLaunchOptions(data));
const [advanced, setAdvanced] = useState(!!launchOption.additionalArgs?.length);
const [additionalArgsString, setAdditionalArgsString] = useState(launchOption.additionalArgs?.join("; ") ?? "");
const [advanced, setAdvanced] = useState(!!launchOption.command?.length);
const [command, setCommand] = useState(launchOption.command || "");
const [steamShortcut, setSteamShortcut] = useState(false);
const isSteamVersion = useMemo(() => {
@@ -31,13 +31,7 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean,
}, [data]);
const completeModal = () => {
if(advanced) {
launchOption.additionalArgs = additionalArgsString.split(";").map(arg => arg.trim()).filter(arg => arg.length);
} else {
launchOption.additionalArgs = undefined;
}
launchOption.command = command.trim();
resolver({exitCode: ModalExitCode.COMPLETED, data: { launchOption, steamShortcut }});
}
@@ -95,8 +89,8 @@ export const CreateLaunchShortcutModal: ModalComponent<{ steamShortcut: boolean,
type="text"
className="w-full rounded-md text-center outline-none bg-light-main-color-3 dark:bg-main-color-3"
placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")}
value={additionalArgsString}
onChange={e => setAdditionalArgsString(e.target.value)}
value={command}
onChange={e => setCommand(e.target.value)}
/>
</div>
</div>
@@ -15,7 +15,6 @@ import BeatConflictImg from "../../../../../assets/images/apngs/beat-conflict.pn
import equal from "fast-deep-equal/es6";
import { ProgressBarService } from "renderer/services/progress-bar.service";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { useService } from "renderer/hooks/use-service.hook";
import { useConstant } from "renderer/hooks/use-constant.hook";
@@ -29,7 +28,6 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
const config = useService(ConfigurationService);
const mapsDownloader = useService(MapsDownloaderService);
const progressBar = useService(ProgressBarService);
const os = useService(OsDiagnosticService);
const currentDownload = useObservable(() => mapsDownloader.currentMapDownload$);
const mapsInQueue = useObservable(() => mapsDownloader.mapsInQueue$);
@@ -41,7 +39,6 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
const [downloadbleMaps, setDownloadbleMaps] = useState<DownloadableMap[]>([]);
const [ownedMapHashs, setOwnedMapHashs] = useState<string[]>(ownedMaps?.map(map => map.hash) ?? []);
const [loading, setLoading] = useState(false);
const isOnline = useObservable(() => os.isOnline$);
const [searchParams, setSearchParams] = useState<SearchParams>({
sortOrder: config.get("map-sort-order"),
filter,
@@ -256,10 +253,7 @@ export const DownloadMapsModal: ModalComponent<void, { version: BSVersion; owned
if (loading) {
return t("modals.download-maps.loading-maps");
}
if (isOnline) {
return t("modals.download-maps.no-maps-found");
}
return t("modals.download-maps.no-internet");
return t("modals.download-maps.no-maps-found");
})()}
</span>
</div>
@@ -16,8 +16,9 @@ import { MODEL_TYPE_FOLDERS } from "shared/models/models/constants";
import { PlaylistsManagerService } from "renderer/services/playlists-manager.service";
import { MapsManagerService } from "renderer/services/maps-manager.service";
import { map } from "rxjs";
import { DISCORD_URL } from "shared/constants";
export const LinkContentModal: ModalComponent<boolean, {version: BSVersion, contentType: "maps"|"playlists"|"avatars"|"sabers"|"platforms"|"blocks"}> = ({options: { data: { version, contentType } }, resolver }) => {
export const LinkContentModal: ModalComponent<void, {version: BSVersion, contentType: "maps"|"playlists"|"avatars"|"sabers"|"platforms"|"bloqs"}> = ({options: { data: { version, contentType } }, resolver }) => {
const { text: t, element: te } = useTranslationV2();
const versionLinker = useService(VersionFolderLinkerService);
@@ -34,7 +35,7 @@ export const LinkContentModal: ModalComponent<boolean, {version: BSVersion, cont
return window.electron.path.join(sharedPath, MODEL_TYPE_FOLDERS.saber);
case "platforms":
return window.electron.path.join(sharedPath, MODEL_TYPE_FOLDERS.platform);
case "blocks":
case "bloqs":
return window.electron.path.join(sharedPath, MODEL_TYPE_FOLDERS.bloq);
default:
return "";
@@ -53,7 +54,7 @@ export const LinkContentModal: ModalComponent<boolean, {version: BSVersion, cont
return window.electron.path.join(version.path, MODEL_TYPE_FOLDERS.saber);
case "platforms":
return window.electron.path.join(version.path, MODEL_TYPE_FOLDERS.platform);
case "blocks":
case "bloqs":
return window.electron.path.join(version.path, MODEL_TYPE_FOLDERS.bloq);
default:
return "";
@@ -97,7 +98,7 @@ export const LinkContentModal: ModalComponent<boolean, {version: BSVersion, cont
<p className="max-w-sm w-full italic my-2 text-warning-400">{t("modals.link-contents.warning", {contentType: t(`misc.${contentType}`).toLowerCase()})}</p>
<div className="flex justify-center items-center gap-3 *:underline *:text-sm *:text-neutral-200">
<BsmLink href="https://en.qrwp.org/Symbolic_link">{t("modals.link-contents.what-is-a-symbolic-link")}</BsmLink>
<BsmLink href="https://discord.gg/uSqbHVpKdV">{t("modals.link-contents.i-need-help")}</BsmLink>
<BsmLink href={DISCORD_URL}>{t("modals.link-contents.i-need-help")}</BsmLink>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-2 mt-4 h-8">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all flex justify-center items-center" onClick={() => resolver({ exitCode: ModalExitCode.CANCELED })} withBar={false} text="misc.cancel" />
@@ -12,7 +12,6 @@ import { ModelDownload, ModelsDownloaderService } from "renderer/services/models
import { ModelSaberService } from "renderer/services/thrird-partys/model-saber.service";
import { useBehaviorSubject } from "renderer/hooks/use-behavior-subject.hook";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { useConstant } from "renderer/hooks/use-constant.hook";
import { MODEL_TYPES, MS_GET_QUERY_SORTS } from "shared/models/models/constants";
@@ -26,7 +25,6 @@ import { VirtualScroll } from "renderer/components/shared/virtual-scroll/virtual
export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; type: MSModelType; owned: BsmLocalModel[] }> = ({ options: {data: { version, type, owned }} }) => {
const modelsDownloader = useService(ModelsDownloaderService);
const modelSaber = useService(ModelSaberService);
const os = useService(OsDiagnosticService);
const t = useTranslation();
@@ -48,7 +46,6 @@ export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; typ
const downloadQueue = useObservable(() => modelsDownloader.getQueue$(), []);
const [msModels, msModels$] = useBehaviorSubject<MSModel[]>([]);
const [renderableModels, setRenderableModels] = useState<RenderableModel[]>([]);
const isOnline = useObservable(() => os.isOnline$, true);
const [error, error$] = useBehaviorSubject(false);
const [isLoading, isLoading$] = useBehaviorSubject(false);
const [ownedModels, setOwnedModels] = useState<BsmLocalModel[]>(owned ?? []);
@@ -197,13 +194,7 @@ export const DownloadModelsModal: ModalComponent<void, { version: BSVersion; typ
if (isLoading) {
return <span className="text-lg">{t("models.panel.grid.loading")}</span>;
}
if (isOnline) {
return <span className="text-lg">{t("models.modals.download-models.no-models")}</span>;
}
if (!isOnline) {
return <span className="text-lg">{t("models.modals.download-models.no-internet")}</span>;
}
return <span className="text-lg">{t("models.modals.download-models.error-occured")}</span>;
return <span className="text-lg">{t("models.modals.download-models.no-models")}</span>;
};
return (
@@ -8,6 +8,7 @@ import { IpcService } from "renderer/services/ipc.service";
import { lastValueFrom } from "rxjs";
import { logRenderError } from "renderer";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { DEFAULT_PLAYLIST_COVER } from "shared/models/playlists/local-playlist.models";
type OutProps = {
playlistTitle: string;
@@ -34,7 +35,9 @@ export const EditPlaylistInfosModal: ModalComponent<OutProps, Props> = ({ resolv
const [title, setTitle] = useState(playlistTitle);
const [description, setDescription] = useState(playlistDescription);
const [author, setAuthor] = useState(playlistAuthor ?? steamDownloader.getSteamUsername());
const [base64, setBase64] = useState(base64Image);
// Playlist cover or Default playlist cover
const [base64, setBase64] = useState(base64Image || DEFAULT_PLAYLIST_COVER);
const handleClickImage = async () => {
const res = await lastValueFrom(ipc.sendV2("choose-image", { base64: true })).catch(logRenderError) as string[];
@@ -60,12 +63,11 @@ export const EditPlaylistInfosModal: ModalComponent<OutProps, Props> = ({ resolv
</h1>
<div className="w-full flex flex-col justify-center items-center">
<button className="flex justify-center items-center relative size-36 border-2 border-gray-400 bg-theme-1 rounded-md overflow-hidden" onClick={handleClickImage}>
{base64 ? (
{base64 && (
<BsmImage className="absolute size-full cursor-pointer" base64={base64} />
) : (
<span className="absolute size-full flex justify-center items-center p-2">{t("playlist.choose-image")}</span>
)}
</button>
<span className="size-full flex justify-center items-center mb-1">{t("playlist.choose-image")}</span>
<div className="w-full">
<label className="font-bold cursor-pointer tracking-wide" htmlFor="playlist-title">{t("playlist.title")}</label>
<input id="playlist-title" type="text" className="w-full bg-theme-1 px-1 py-0.5 rounded-md outline-none h-9" value={title} placeholder={t("playlist.playlist-title")} onChange={e => setTitle(e.target.value)}/>
@@ -48,7 +48,7 @@ export function PlaylistDetailsTemplate({title, imagebase64, imageUrl, author, d
return (
<div className="flex flex-col w-screen max-w-2xl h-screen max-h-[calc(100vh-1.25rem)] translate-y-2.5 bg-theme-1 rounded-t-lg overflow-hidden">
<header className="shrink-0 relative h-36 overflow-hidden flex flex-row p-3">
<button className="absolute top-2 right-2 z-[1]" onClickCapture={onClose}>
<button className="absolute top-2 right-2 z-[2]" onClickCapture={onClose}>
<CrossIcon className="size-3.5 text-gray-200"/>
</button>
<BsmImage className="absolute top-0 left-0 size-full object-cover blur-xl scale-150 brightness-75 saturate-200" base64={imagebase64} image={imageUrl}/>
@@ -16,6 +16,7 @@ import { MODEL_TYPE_FOLDERS } from "shared/models/models/constants";
import { useConstant } from "renderer/hooks/use-constant.hook";
import { MapsManagerService } from "renderer/services/maps-manager.service";
import { BsmLink } from "renderer/components/shared/bsm-link.component";
import { DISCORD_URL } from "shared/constants";
export const UnlinkContentsModal: ModalComponent<boolean, {version: BSVersion, contentType: "maps"|"playlists"|"avatars"|"sabers"|"platforms"|"blocks"}> = ({options: { data: { version, contentType } }, resolver }) => {
const { text: t, element: te } = useTranslationV2();
@@ -86,7 +87,7 @@ export const UnlinkContentsModal: ModalComponent<boolean, {version: BSVersion, c
</p>
<div className="flex justify-center items-center gap-3 *:underline *:text-sm *:text-neutral-200">
<BsmLink href="https://en.qrwp.org/Symbolic_link">{t("modals.link-contents.what-is-a-symbolic-link")}</BsmLink>
<BsmLink href="https://discord.gg/uSqbHVpKdV">{t("modals.link-contents.i-need-help")}</BsmLink>
<BsmLink href={DISCORD_URL}>{t("modals.link-contents.i-need-help")}</BsmLink>
</div>
<Tippy content={t("modals.unlink-contents.do-not-copy-contents-tip", { contentType: t(`misc.${contentType}`).toLowerCase() })} theme="default">
<div className="relative h-5 flex my-3 items-center w-fit">
@@ -16,6 +16,7 @@ import equal from "fast-deep-equal";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
import { ProgressBarService } from "renderer/services/progress-bar.service";
import { noop } from "shared/helpers/function.helpers";
export function BsVersionItem(props: { version: BSVersion }) {
@@ -70,7 +71,7 @@ export function BsVersionItem(props: { version: BSVersion }) {
const wasVerification = bsDownloader.isVerifying;
bsDownloader.stopDownload().then(() => {
if(wasVerification){ return; }
bsUninstallerService.uninstall(versionDownload).then(res => res && verionManagerService.askInstalledVersions());
bsUninstallerService.uninstall(versionDownload).then(() => verionManagerService.askInstalledVersions()).catch(noop);
});
};
@@ -56,14 +56,14 @@ export function NavBar() {
<NavBarSpliter />
<NavBarItem isActive={["/blah","/"].includes(route)}>
<Tippy placement="right" content={t("nav-bar.add-version")} className="!bg-neutral-900" arrow={false}>
<Link className="rounded-md w-9 h-9 flex justify-center items-center hover:bg-light-main-color-3 dark:hover:bg-main-color-3" to="blah">
<Link className="rounded-md w-9 h-9 flex justify-center items-center hover:bg-theme-3" to="blah">
<BsmIcon icon="add" className="text-blue-500 h-[34px]" style={{ color }} />
</Link>
</Tippy>
</NavBarItem>
<NavBarItem isActive={route === "/settings"}>
<Tippy placement="right" content={t("nav-bar.settings")} className="!bg-neutral-900" arrow={false}>
<Link className="rounded-md w-9 h-9 flex justify-center items-center hover:bg-light-main-color-3 dark:hover:bg-main-color-3" to="settings">
<Link className="rounded-md w-9 h-9 flex justify-center items-center hover:bg-theme-3" to="settings">
<BsmIcon icon="settings" className="text-blue-500 h-7" style={{ color }} />
</Link>
</Tippy>
@@ -24,7 +24,7 @@ export function BsmCheckbox({ className, checked, onChange, disabled }: Props) {
<GlowEffect visible={!disabled && hovered} className="!w-[calc(100%+6px)] !h-[calc(100%+6px)] !-top-[3px] !-left-[3px]" />
<span
className={`w-full h-full flex items-center justify-center rounded-md border-2 border-current overflow-hidden bg-light-main-color-3 dark:bg-main-color-1 ${disabled ? "opacity-30 cursor-not-allowed" : "cursor-pointer"}`}
className={`w-full h-full flex items-center justify-center rounded-md border-2 border-main-color-1 dark:border-light-main-color-1 overflow-hidden bg-light-main-color-3 dark:bg-main-color-1 ${disabled ? "opacity-30 cursor-not-allowed" : "cursor-pointer"}`}
onClickCapture={e => {
e.stopPropagation();
handleClick();
@@ -72,9 +72,6 @@ export function VirtualScroll<T = unknown>({ className, classNames, minItemWidth
const { scrollDirection, scrollOffset, scrollUpdateWasRequested } = e;
const { scrollHeight } = listRef.current;
if(!scrollHeight || !listHeight){ return; }
const margin = scrollEnd.margin ?? 0;
if (scrollDirection === "forward" && !scrollUpdateWasRequested && scrollOffset + listHeight + margin >= scrollHeight) {
@@ -12,7 +12,7 @@ import { FranceIcon } from "./flags/france-icon.component";
import { SpainIcon } from "./flags/spain-icon.component";
import { ItalyIcon } from "./flags/italy-icon.component";
import { UsaIcon } from "./flags/usa-icon.component";
import { UkIcon } from "./flags/uk-icon.component";
import { UkraineIcon } from "./flags/ukraine-icon.component";
import { TaskIcon } from "./icons/task-icon.component";
import { CopyIcon } from "./icons/copy-icon.component";
import { SteamIcon } from "./icons/steam-icon.component";
@@ -58,6 +58,7 @@ import { ChineseIcon } from "./flags/chinese-icon.component";
import { ChineseTraditionalIcon } from "./flags/chineseTraditional-icon.component";
import { JapanIcon } from "./flags/japan-icon.component";
import { KoreaIcon } from "./flags/korea-icon.component";
import { BrazilIcon } from "./flags/brasil-icon.component";
import { ChevronTopIcon } from "./icons/chevron-top-icon.component";
import { EyeCrossIcon } from "./icons/eye-cross-icon.component";
import { ShortcutIcon } from "./icons/shortcut-icon.component";
@@ -69,10 +70,12 @@ import { AddFileIcon } from "./icons/add-file-icon.component";
import { CancelIcon } from "./icons/cancel-icon.component";
import { WarningIcon } from "./icons/warning-icon.component";
import { ArrowUpwardIcon } from "./icons/arrow-upward-icon.component";
import { UkIcon } from "./flags/uk-icon.component";
import { PhilippineIcon } from "./flags/philippine-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" | "clean" | "browse" | "add-file" | "arrow-upward" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "pt-BR-flag" | "uk-UA-flag" | "tl-PH-flag" | "null" );
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" | "browse" | "add-file" | "arrow-upward" | "cancel" | "warning" | "fr-FR-flag" | "es-ES-flag" | "it-IT-flag" | "en-US-flag" | "en-EN-flag" | "de-DE-flag" | "ru-RU-flag" | "zh-CN-flag" | "zh-TW-flag" | "ja-JP-flag" | "ko-KR-flag" | "null" );
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
@@ -142,6 +145,16 @@ export const BsmIcon = memo(({ className, icon, style }: { className?: string; i
if (icon === "ko-KR-flag"){
return <KoreaIcon className={className} style={style} />;
}
if (icon === "pt-BR-flag"){
return <BrazilIcon className={className} style={style} />;
}
if (icon === "uk-UA-flag"){
return <UkraineIcon className={className} style={style} />;
}
if (icon === "tl-PH-flag") {
return <PhilippineIcon className={className} style={style} />;
}
if (icon === "task") {
return <TaskIcon className={className} style={style} />;
}
@@ -0,0 +1,14 @@
import { createSvgIcon } from "../svg-icon.type";
export const BrazilIcon = createSvgIcon((props, ref) => {
return (
<svg ref={ref} {...props} width="71" height="48" viewBox="0 0 71 48" xmlns="http://www.w3.org/2000/svg">
<g fill="none"><rect fill="#52C162" x=".15" y=".13" width="70" height="47" rx="6.36"/>
<path fill="#F6D660" d="m7.93 23.63 27.21-21 27.21 21-27.21 21z"/>
<path d="M31 18.55c-2.83 0-5.612.737-8.07 2.14a12.56 12.56 0 0 0 23.44 8.59A16.41 16.41 0 0 0 31 18.55" fill="#4758A9"/>
<path d="M31 18.55a16.41 16.41 0 0 1 15.37 10.73 12.5 12.5 0 0 0 1-2.87C42.964 17.945 32.834 14.23 24 17.84a12.5 12.5 0 0 0-1.06 2.85A16.3 16.3 0 0 1 31 18.55" fill="#FFF"/>
<path d="M47.4 26.42A12.57 12.57 0 0 0 24 17.84c8.848-3.633 19.005.09 23.41 8.58z" fill="#4758A9"/>
</g>
</svg>
);
});
@@ -0,0 +1,14 @@
import { createSvgIcon } from "../svg-icon.type";
export const PhilippineIcon = createSvgIcon((props, ref) => {
return (
<svg ref={ref} {...props} width="71" height="48" viewBox="0 0 71 48" xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path d="M63.89.12H6.61A6.36 6.36 0 0 0 .25 6.48v17.64h70V6.48A6.36 6.36 0 0 0 63.89.12" fill="#4758A9"/>
<path d="M.25 40.75a6.36 6.36 0 0 0 6.36 6.36h57.27a6.36 6.36 0 0 0 6.36-6.36V24.12h-70z" fill="#ED5565"/>
<path d="M2.15 1.95a6.34 6.34 0 0 0-1.9 4.54v34.27a6.34 6.34 0 0 0 1.95 4.57l23.05-21.21z" fill="#FFF"/>
<circle fill="#F6D660" cx="10.25" cy="23.62" r="4"/>
</g>
</svg>
);
});
@@ -0,0 +1,14 @@
import { createSvgIcon } from "../svg-icon.type";
// Uploaded from: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools
export const UkraineIcon = createSvgIcon((props, ref) => {
return (
<svg ref={ref} {...props} width="71" height="48" viewBox="0 0 71 48" xmlns="http://www.w3.org/2000/svg">
<g fill="none">
<path d="M64.27 47.7H7a6.36 6.36 0 0 1-6.36-6.36V23.7h70v17.64a6.36 6.36 0 0 1-6.37 6.36" fill="#F6D660"/>
<path d="M.63 7.06A6.36 6.36 0 0 1 7 .7h57.27a6.36 6.36 0 0 1 6.36 6.36V23.7h-70z" fill="#4A89DC"/>
</g>
</svg>
);
})
@@ -4,8 +4,11 @@ import { PinIcon } from "renderer/components/svgs/icons/pin-icon.component";
import { UnpinIcon } from "renderer/components/svgs/icons/unpin-icon.component";
import { SvgIcon } from "renderer/components/svgs/svg-icon.type";
import { cn } from "renderer/helpers/css-class.helpers";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
import { LaunchMod } from "shared/models/bs-launch/launch-option.interface";
import { AddIcon } from "renderer/components/svgs/icons/add-icon.component";
import { EditIcon } from "renderer/components/svgs/icons/edit-icon.component";
import { TrashIcon } from "renderer/components/svgs/icons/trash-icon.component";
type Props = {
readonly className?: string;
@@ -13,17 +16,26 @@ type Props = {
readonly launchArgs?: string;
readonly launchMods?: LaunchModItemProps[];
readonly onLaunchArgsChange?: (args: string) => void;
readonly onAddLaunchMod?: (command?: string) => void;
};
export function LaunchOptionsPanel({ className, open, launchArgs, launchMods, onLaunchArgsChange }: Props) {
export function LaunchOptionsPanel({ className, open, launchArgs, launchMods, onLaunchArgsChange, onAddLaunchMod }: Props) {
const {text: t} = useTranslationV2();
const color = useThemeColor("first-color");
const { text: t } = useTranslationV2();
return (
<div className={cn("grid grid-rows-[0fr] transition-[grid-template-rows] bg-theme-2 rounded-md shadow-md shadow-black overflow-hidden", open && "!grid-rows-[1fr]", className)}>
<div className="flex flex-col overflow-y-scroll scrollbar-default">
<div className="p-3.5">
<input className="h-10 w-full rounded-md bg-theme-1 text-center mb-2" type="text" placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")} value={launchArgs} onChange={e => onLaunchArgsChange(e.target.value)}/>
<div className="w-full relative flex justify-center items-center mb-2">
<input className="h-10 w-full rounded-md bg-theme-1 text-center" type="text" placeholder={t("pages.version-viewer.launch-mods.advanced-launch.placeholder")} value={launchArgs} onChange={e => onLaunchArgsChange(e.target.value)}/>
<Tippy theme="default" placement="top" content={t("pages.version-viewer.launch-mods.advanced-launch.create-launch-option")}>
<button className="absolute size-8 rounded-md p-0.5 right-1 hover:brightness-110" style={{ backgroundColor: color }} onClick={() => onAddLaunchMod?.(launchArgs)}>
<AddIcon className="size-full"/>
</button>
</Tippy>
</div>
<div className={cn("flex gap-2 flex-wrap")}>
{launchMods?.map((mod) => (
<LaunchModItem key={mod.id} {...mod}/>
@@ -35,36 +47,62 @@ export function LaunchOptionsPanel({ className, open, launchArgs, launchMods, on
)
}
export type CustomLaunchOption = {
readonly id: string;
readonly label: string;
readonly data: {
readonly command?: string;
}
}
export type LaunchModItemProps = {
readonly id: LaunchMod;
readonly id: string;
readonly icon?: SvgIcon;
readonly label: string;
readonly description: string;
readonly description?: string;
readonly active: boolean;
readonly visible?: boolean;
readonly pinned?: boolean;
readonly onChange?: (val: boolean) => void;
readonly onPinChange?: (val: boolean) => void;
readonly onEdit?: () => void;
readonly onDelete?: () => void;
}
export function LaunchModItem({ id, icon: Icon, label, description, active, visible, pinned, onChange, onPinChange }: LaunchModItemProps) {
export function LaunchModItem({ id, icon: Icon, label, description, active, visible, pinned, onChange, onPinChange, onEdit, onDelete }: LaunchModItemProps) {
const { text: t } = useTranslationV2();
return (
<Tippy theme="default" className="break-words" placement="top" content={description}>
<button id={id} className={cn("grow rounded-md bg-theme-1 relative flex justify-center items-center h-10 py-1 px-3", visible === false && "hidden")} onClick={e => { e.preventDefault(); e.stopPropagation(); onChange?.(!active) }}>
<Tippy theme="default" className="break-words" placement="top" content={description ?? null} disabled={!description}>
<div id={id} className={cn("grow rounded-md bg-theme-1 relative flex justify-center items-center h-10 py-1 px-3", visible === false && "hidden")} onClick={e => { e.preventDefault(); e.stopPropagation(); onChange?.(!active) }}>
<BsmCheckbox className="h-4 aspect-square z-[1] relative mr-1.5" checked={active} onChange={onChange}/>
{Icon && <Icon className="h-full w-fit py-0.5 mr-1.5"/>}
<span className="font-bold">{label}</span>
{Icon && <Icon className="h-full w-fit py-0.5 mr-1.5 text-gray-800 dark:text-gray-200"/>}
<span className="font-bold text-gray-800 dark:text-gray-200">{label}</span>
{onPinChange && (
<Tippy theme="default" placement="right" content={pinned ? t("misc.unpin") : t("misc.pin")} hideOnClick>
<button className="h-full py-2 px-1.5" onClick={e => { e.preventDefault(); e.stopPropagation(); onPinChange?.(!pinned) }}>
{pinned ? <UnpinIcon className="size-full"/> : <PinIcon className="size-full"/>}
{pinned
? <UnpinIcon className="size-full text-gray-800 dark:text-gray-200"/>
: <PinIcon className="size-full text-gray-800 dark:text-gray-200"/>}
</button>
</Tippy>
)}
</button>
{onEdit && (
<Tippy theme="default" placement="right" content={t("misc.edit")} hideOnClick>
<button className="h-full py-2 px-1" onClick={e => { e.preventDefault(); e.stopPropagation(); onEdit?.() }}>
<EditIcon className="size-full text-gray-800 dark:text-gray-200"/>
</button>
</Tippy>
)}
{onDelete && (
<Tippy theme="default" placement="right" content={t("misc.delete")} hideOnClick>
<button className="h-full py-2 px-1" onClick={e => { e.preventDefault(); e.stopPropagation(); onDelete?.() }}>
<TrashIcon className="size-full text-gray-800 dark:text-gray-200"/>
</button>
</Tippy>
)}
</div>
</Tippy>
);
@@ -14,18 +14,19 @@ import { lastValueFrom } from "rxjs";
import { BsDownloaderService } from "renderer/services/bs-version-download/bs-downloader.service";
import equal from "fast-deep-equal";
import { GlowEffect } from "renderer/components/shared/glow-effect.component";
import { cn } from "renderer/helpers/css-class.helpers";
import { BSVersionManagerService } from "renderer/services/bs-version-manager.service";
import { safeLt } from "shared/helpers/semver.helpers";
import { WarningIcon } from "renderer/components/svgs/icons/warning-icon.component";
import Tippy from "@tippyjs/react";
import { LaunchModItemProps, LaunchOptionsPanel } from "./launch-options-panel.component";
import { CustomLaunchOption, LaunchModItemProps, LaunchOptionsPanel } from "./launch-options-panel.component";
import { LaunchMod, LaunchMods } from "shared/models/bs-launch/launch-option.interface";
import { OculusIcon } from "renderer/components/svgs/icons/oculus-icon.component";
import { DesktopIcon } from "renderer/components/svgs/icons/desktop-icon.component";
import { TerminalIcon } from "renderer/components/svgs/icons/terminal-icon.component";
import { DefaultConfigKey } from "renderer/config/default-configuration.config";
import { EditIcon } from "renderer/components/svgs/icons/edit-icon.component";
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
import { CreateCustomLaunchOptionModal } from "renderer/components/modal/modal-types/create-custom-launch-option.component";
type Props = { version: BSVersion };
@@ -36,18 +37,21 @@ export function LaunchSlide({ version }: Props) {
const bsLauncherService = useService(BSLauncherService);
const bsDownloader = useService(BsDownloaderService);
const versions = useService(BSVersionManagerService);
const modal = useService(ModalService);
const [advancedLaunch, setAdvancedLaunch] = useState(false);
const [additionalArgsString, setAdditionalArgsString] = useState<string>(configService.get<string>("additionnal-args") || "");
const [command, setCommand] = useState<string>(configService.get<string>("launch-command") || "");
const customLaunchOptions = useObservable<CustomLaunchOption[]>(() => configService.watch<CustomLaunchOption[]>("custom-launch-options"), []);
const [customLaunchModsArgs, setCustomLaunchModsArgs] = useState<string[]>([]);
const versionDownloading = useObservable(() => bsDownloader.downloadingVersion$);
const [activeLaunchMods, setActiveLaunchMods] = useState<LaunchMod[]>(configService.get("launch-mods") ?? []);
const [pinnedLaunchMods, setPinnedLaunchMods] = useState<LaunchMod[]>(configService.get("pinned-launch-mods" as DefaultConfigKey) ?? []);
const [activeLaunchMods, setActiveLaunchMods] = useState<string[]>(configService.get("launch-mods") ?? []);
const [pinnedLaunchMods, setPinnedLaunchMods] = useState<string[]>(configService.get("pinned-launch-mods" as DefaultConfigKey) ?? []);
const versionRunning = useObservable(() => bsLauncherService.versionRunning$);
useEffect(() => {
configService.set("additionnal-args", additionalArgsString);
}, [additionalArgsString]);
configService.set("launch-command", command);
}, [command]);
useEffect(() => {
configService.set("pinned-launch-mods", pinnedLaunchMods);
@@ -61,11 +65,11 @@ export function LaunchSlide({ version }: Props) {
}
}, [activeLaunchMods]);
const toggleActiveLaunchMod = (checked: boolean, launchMod: LaunchMod) => checked
const toggleActiveLaunchMod = (checked: boolean, launchMod: string) => checked
? setActiveLaunchMods(prev => [...prev, launchMod])
: setActiveLaunchMods(prev => prev.filter(mod => mod !== launchMod));
const togglePinnedLaunchMod = (pinned: boolean, launchMod: LaunchMod) => pinned
const togglePinnedLaunchMod = (pinned: boolean, launchMod: string) => pinned
? setPinnedLaunchMods(prev => [...prev, launchMod])
: setPinnedLaunchMods(prev => prev.filter(mod => mod !== launchMod));
@@ -78,6 +82,29 @@ export function LaunchSlide({ version }: Props) {
: ["BSInstances", version.name, "Logs"];
}
const customOptions = customLaunchOptions?.map<LaunchModItemProps>(option => ({
id: option.id,
label: option.label,
active: activeLaunchMods.includes(option.id),
pinned: pinnedLaunchMods.includes(option.id),
onChange: (checked) => {
toggleActiveLaunchMod(checked, option.id)
setCustomLaunchModsArgs(prev => {
if(checked){
return [...prev, option.data.command];
}
return prev.filter(arg => arg !== option.data.command);
});
},
onPinChange: (pinned) => togglePinnedLaunchMod(pinned, option.id),
onEdit: () => {
saveCustomLaunchOption(option);
},
onDelete: () => {
deleteCustomLaunchOption(option.id);
},
})) ?? [];
return [
{
id: LaunchMods.OCULUS,
@@ -142,16 +169,15 @@ export function LaunchSlide({ version }: Props) {
onChange: (checked) => toggleActiveLaunchMod(checked, LaunchMods.PROTON_LOGS),
onPinChange: (pinned) => togglePinnedLaunchMod(pinned, LaunchMods.PROTON_LOGS),
},
...customOptions,
]
}, [activeLaunchMods, pinnedLaunchMods, version]);
}, [activeLaunchMods, pinnedLaunchMods, customLaunchOptions, version]);
const launch = async () => {
const additionalArgs = additionalArgsString?.split(";").map(arg => arg.trim()).filter(arg => arg.length > 0);
const launch$ = bsLauncherService.launch({
version,
launchMods: activeLaunchMods,
additionalArgs: advancedLaunch ? additionalArgs : [],
launchMods: activeLaunchMods.filter(mod => Object.values(LaunchMods).includes(mod as LaunchMod)) as LaunchMod[],
command: [command, ...customLaunchModsArgs].filter(Boolean).join(" ").trim(),
});
return lastValueFrom(launch$).catch(() => {});
@@ -161,6 +187,19 @@ export function LaunchSlide({ version }: Props) {
return safeLt(version?.BSVersion, versions.getRecommendedVersion()?.BSVersion);
}, [version]);
const saveCustomLaunchOption = async (option: Partial<CustomLaunchOption>) => {
const result = await modal.openModal(CreateCustomLaunchOptionModal, { data: option });
if(result.exitCode !== ModalExitCode.COMPLETED) { return; }
const newCustomLaunchOptions = (customLaunchOptions ?? []).filter(mode => mode.id !== result.data.id);
newCustomLaunchOptions.push(result.data);
configService.set("custom-launch-options", newCustomLaunchOptions);
}
const deleteCustomLaunchOption = (id: string) => {
const newCustomLaunchOptions = (customLaunchOptions ?? []).filter(mode => mode.id !== id);
configService.set("custom-launch-options", newCustomLaunchOptions);
}
return (
<div className="w-full shrink-0 items-center relative flex flex-col justify-start overflow-hidden">
<div className="flex flex-col gap-3 justify-center items-center mb-4">
@@ -183,6 +222,7 @@ export function LaunchSlide({ version }: Props) {
if(launchMod?.visible === false || !launchMod?.pinned) { return undefined; }
return (
<LaunchModToogle
key={launchMod.id}
icon={launchMod.icon}
infoText={launchMod.description}
text={launchMod.label}
@@ -194,9 +234,9 @@ export function LaunchSlide({ version }: Props) {
</div>
<div className="mt-4 flex flex-col items-center justify-center gap-3">
<div className="relative">
<GlowEffect className="!rounded-full" visible={!!(activeLaunchMods?.length || additionalArgsString)}/>
<GlowEffect className="!rounded-full" visible={!!((activeLaunchMods?.filter(mod => !pinnedLaunchMods.includes(mod)))?.length || command)}/>
<BsmButton
className={cn("rounded-full w-fit text-lg py-1 px-7 bg-theme-2 text-gray-800 dark:text-white", (advancedLaunch && additionalArgsString) ? "" : "shadow-md shadow-black")}
className="rounded-full w-fit text-lg py-1 px-7 bg-theme-2 text-gray-800 dark:text-white shadow-md shadow-black"
text="pages.version-viewer.launch-mods.advanced-launch.button"
withBar={false}
onClick={e => {
@@ -206,12 +246,12 @@ export function LaunchSlide({ version }: Props) {
/>
</div>
</div>
<LaunchOptionsPanel open={advancedLaunch} launchMods={launchModItems} launchArgs={additionalArgsString} className="w-full max-w-3xl mt-3" onLaunchArgsChange={setAdditionalArgsString}/>
<LaunchOptionsPanel open={advancedLaunch} launchMods={launchModItems} launchArgs={command} className="w-full max-w-3xl mt-3" onLaunchArgsChange={setCommand} onAddLaunchMod={command => saveCustomLaunchOption({ data: { command } })}/>
<div className='grow flex justify-center items-center p-2'>
<BsmButton
onClick={launch}
active={JSON.stringify(version) === JSON.stringify(versionRunning)}
className='relative text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform'
className="relative text-5xl text-gray-800 dark:text-gray-200 font-bold tracking-wide pt-1 pb-3 px-7 rounded-lg shadow-md italic shadow-black active:scale-90 transition-transform"
text="misc.launch"
disabled={equal(version, versionDownloading)}
/>
@@ -1,12 +1,13 @@
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
import { CSSProperties, MouseEvent, useMemo, useRef } from "react";
import { CSSProperties, MouseEvent, useMemo } from "react";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import useDoubleClick from "use-double-click";
import { useOnUpdate } from "renderer/hooks/use-on-update.hook";
import striptags from "striptags";
import { safeGt } from "shared/helpers/semver.helpers";
import Tippy from "@tippyjs/react";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
type Props = {
className?: string;
@@ -21,20 +22,74 @@ type Props = {
onUninstall?: () => void;
};
type FileSizeProps = {
fileSize?: number;
wantInfoStyle: CSSProperties;
};
function FileSizeText({ fileSize, wantInfoStyle }: Readonly<FileSizeProps>) {
const { text: t } = useTranslationV2();
const verifyFileSize = fileSize !== undefined;
const isMediumMod = verifyFileSize && fileSize > 1024 * 1024 * 50; // 50MB
const isLargeMod = verifyFileSize && fileSize > 1024 * 1024 * 100; // 100MB
const getFormattedSize = () : string => {
if (!verifyFileSize || fileSize === 0)
return `-`;
if (fileSize < 1024 * 1024)
return `${(fileSize/1024).toFixed(2)}KB`;
return `${(fileSize/1024/1024).toFixed(2)}MB`;
};
const getWarningText = () : string => {
if (isLargeMod)
return `pages.version-viewer.mods.mods-grid.mod-item.this-is-a-very-large-mod`;
if (isMediumMod)
return `pages.version-viewer.mods.mods-grid.mod-item.this-is-a-large-mod`;
return "";
};
const getTheme = () : string => {
if (isLargeMod)
return 'red';
if (isMediumMod)
return 'yellow';
return "";
};
const getTextColor = () : string => {
if (isLargeMod)
return "text-red-400";
if (isMediumMod)
return "text-yellow-400";
return "";
};
return (
<Tippy
content={t(getWarningText())}
placement="left"
theme={getTheme()}
disabled={!(isLargeMod || isMediumMod)}
delay={[50, 0]} >
<span className={`min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90 ${getTextColor()}`} style={wantInfoStyle}>
{getFormattedSize()}
</span>
</Tippy>
);
}
export function ModItem({ className, mod, installedVersion, isDependency, isSelected, onChange, wantInfo, onWantInfo, disabled, onUninstall }: Props) {
const themeColor = useThemeColor("second-color");
const clickRef = useRef();
const isChecked = useMemo(() => isDependency || isSelected || mod.mod.category === BbmCategories.Core, [isDependency, isSelected, mod.mod.category]);
useDoubleClick({
onSingleClick: e => handleWantInfo(e),
onDoubleClick: e => handleOnChange(e),
ref: clickRef,
latency: 175,
});
useOnUpdate(() => {
onChange(isChecked);
}, [isChecked]);
@@ -46,17 +101,13 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
e.preventDefault();
onWantInfo(mod);
};
const handleOnChange = (e: MouseEvent) => {
e.preventDefault();
onChange(!isChecked);
};
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.mod.category === BbmCategories.Core || isDependency || disabled} checked={isChecked} />
<li className={`${className} group`} onClick={handleWantInfo}>
<div className="h-full flex items-center justify-center p-1.5 px-4 rounded-l-md bg-inherit ml-3 border-2 border-r-0 z-[1] group-hover:brightness-90" style={wantInfoStyle}>
<BsmCheckbox className="size-[18px] aspect-square z-[1] relative bg-inherit" onChange={() => onChange(!isChecked)} disabled={mod.mod.category === BbmCategories.Core || 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}>
<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 flex items-center" style={wantInfoStyle}>
{mod.mod.name}
</span>
<span className={`min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90 ${installedVersion && isOutDated && "text-red-400 line-through"} ${installedVersion && !isOutDated && "text-green-400"}`} style={wantInfoStyle}>
@@ -65,6 +116,7 @@ export function ModItem({ className, mod, installedVersion, isDependency, isSele
<span className="min-w-0 text-center bg-inherit py-2 px-1 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
{mod.version.modVersion}
</span>
<FileSizeText fileSize={mod.version.fileSize} wantInfoStyle={wantInfoStyle} />
<span title={striptags(mod.mod?.description ?? "", { tagReplacementText: " " })} className="px-3 bg-inherit whitespace-nowrap text-ellipsis overflow-hidden py-2 text-sm border-t-2 border-b-2 group-hover:brightness-90" style={wantInfoStyle}>
{striptags(mod.mod?.summary ?? "", { tagReplacementText: " " })}
</span>
@@ -17,10 +17,9 @@ type Props = {
uninstallMod?: (mods: BbmFullMod) => void;
uninstallAllMods?: () => void;
unselectAllMods?: () => void;
openModsDropZone?: () => void;
};
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos, disabled, uninstallMod, uninstallAllMods, unselectAllMods, openModsDropZone }: Props) {
export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreInfoMod, onWantInfos, disabled, uninstallMod, uninstallAllMods, unselectAllMods }: Props) {
const [filter, setFilter] = useState("");
const [filterEnabled, setFilterEnabled] = useState(false);
@@ -59,18 +58,18 @@ export function ModsGrid({ modsMap, installed, modsSelected, onModChange, moreIn
return (
modsMap && (
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_1fr_min-content] bg-light-main-color-2 dark:bg-main-color-2 text-main-color-1 dark:text-light-main-color-1">
<div className="grid gap-y-1 grid-cols-[40px_min-content_min-content_min-content_min-content_1fr_min-content] bg-light-main-color-2 dark:bg-main-color-2 text-main-color-1 dark:text-light-main-color-1">
<span className="absolute z-10 top-0 w-full h-8 bg-inherit rounded-tl-md" />
<span className="z-10 sticky flex items-center justify-end top-0 bg-inherit border-b-2 border-main-color-1 rounded-tl-md">
<BsmButton className="rounded-full h-6 w-6 p-[2px]" withBar={false} icon="search" onClick={handleToogleFilter} />
</span>
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1 whitespace-nowrap">{filterEnabled ? <motion.input autoFocus className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md h-6 px-2" initial={{ width: 0 }} animate={{ width: "250px" }} transition={{ ease: "easeInOut", duration: 0.15 }} onChange={e => handleInput(e.target.value)} /> : <span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>}</span>
<span className="z-10 sticky top-0 flex items-center bg-inherit border-main-color-1 border-b-2 h-8 px-1 whitespace-nowrap">{filterEnabled ? <motion.input autoFocus className="bg-light-main-color-1 dark:bg-main-color-1 rounded-md h-6 px-2" initial={{ width: 0 }} animate={{ width: "200px" }} transition={{ ease: "easeInOut", duration: 0.15 }} onChange={e => handleInput(e.target.value)} /> : <span className="w-full text-center">{t("pages.version-viewer.mods.mods-grid.header-bar.name")}</span>}</span>
<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.installed")}</span>
<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 px-2 whitespace-nowrap">{t("pages.version-viewer.mods.mods-grid.header-bar.size")}</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.import-mods", icon: "download", onClick: () => openModsDropZone?.() },
{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.unselect-all", icon: "cancel", onClick: () => unselectAllMods?.() },
{ text: "pages.version-viewer.mods.mods-grid.header-bar.dropdown.uninstall-all", icon: "trash", onClick: () => uninstallAllMods?.() }
]} />
@@ -1,47 +1,53 @@
import { ReactNode, useEffect, useLayoutEffect, useRef, useState } from "react";
import { forwardRef, ReactNode, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
import { BsModsManagerService } from "renderer/services/bs-mods-manager.service";
import { BSVersion } from "shared/bs-version.interface";
import { BbmCategories, BbmFullMod, BbmModVersion } from "shared/models/mods/mod.interface";
import { BbmCategories, BbmFullMod } from "shared/models/mods/mod.interface";
import { ModsGrid } from "./mods-grid.component";
import { ConfigurationService } from "renderer/services/configuration.service";
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 { lastValueFrom } from "rxjs";
import { useTranslation, useTranslationV2 } from "renderer/hooks/use-translation.hook";
import { useTranslationV2 } from "renderer/hooks/use-translation.hook";
import { LinkOpenerService } from "renderer/services/link-opener.service";
import { useInView } from "framer-motion";
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
import { ModsDisclaimerModal } from "renderer/components/modal/modal-types/mods-disclaimer-modal.component";
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";
import Tippy from "@tippyjs/react";
import { ProgressBarService } from "renderer/services/progress-bar.service";
import { Dropzone } from "renderer/components/shared/dropzone.component";
import { ModsGridStatus } from "shared/models/mods/mod-ipc.model";
import { BsmLink } from "renderer/components/shared/bsm-link.component";
import { DISCORD_URL, GITHUB_URL } from "shared/constants";
export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion; onDisclamerDecline: () => void }) {
export type ModsSlideRef = {
loadMods: () => Promise<void>;
}
type Props = { version: BSVersion; isActive?: boolean, onDisclamerDecline: () => void };
export const ModsSlide = forwardRef<ModsSlideRef, Props>(({ version, isActive, onDisclamerDecline }, forwaredRef) => {
const ACCEPTED_DISCLAIMER_KEY = "accepted-mods-disclaimer";
const { text: t } = useTranslationV2();
const { text: t, element: te } = useTranslationV2();
const modsManager = useService(BsModsManagerService);
const configService = useService(ConfigurationService);
const notification = useService(NotificationService);
const linkOpener = useService(LinkOpenerService);
const modals = useService(ModalService);
const os = useService(OsDiagnosticService);
const progress = useService(ProgressBarService);
const ref = useRef(null);
const isVisible = useInView(ref, { amount: 0.5 });
const [gridStatus, setGridStatus] = useState(ModsGridStatus.OK);
const [modsAvailable, setModsAvailable] = useState(null as Map<BbmCategories, BbmFullMod[]>);
const [modsInstalled, setModsInstalled] = useState(null as Map<BbmCategories, BbmFullMod[]>);
const [modsSelected, setModsSelected] = useState([] as BbmFullMod[]);
const [moreInfoMod, setMoreInfoMod] = useState(null as BbmFullMod);
const [reinstallAllMods, setReinstallAllMods] = useState(false);
const isOnline = useObservable(() => os.isOnline$);
const [installing, setInstalling] = useState(false);
const [uninstalling, setUninstalling] = useState(false);
const [modsDropZoneOpen, setModsDropZoneOpen] = useState(false);
@@ -100,14 +106,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
return Array.from(collectedDependencies);
};
const installMods = (reinstallAll: boolean): void => {
setReinstallAllMods(false);
if (installing) {
return;
}
const getModsToInstall = (reinstallAll?: boolean): BbmFullMod[] => {
let modsToInstall = [
...modsSelected,
...getAllDependencies(modsSelected, Array.from(modsAvailable.values()).flat())
@@ -127,18 +126,30 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
set.delete(null);
set.delete(undefined);
modsToInstall = Array.from(set); // Remove duplicates
return Array.from(set); // Remove duplicates
}
const installMods = (reinstallAll: boolean): Promise<void> => {
setReinstallAllMods(false);
if (installing) {
return Promise.resolve();
}
const modsToInstall = getModsToInstall(reinstallAll);
if (!modsToInstall.length) {
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;
return loadMods();
}
setInstalling(() => true)
lastValueFrom(modsManager.installMods(modsToInstall, version)).then(() => {
loadMods();
}).catch(noop).finally(() => setInstalling(() => false));
return lastValueFrom(modsManager.installMods(modsToInstall, version)).then(() => (
loadMods()
)).catch(noop).finally(() => (
setInstalling(() => false)
));
};
const importMods = (files: string[]): void => {
@@ -174,55 +185,53 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
setModsSelected(() => []);
}
const ensureDisclaimerAccepted = async (): Promise<boolean> => {
if (configService.get<boolean>(ACCEPTED_DISCLAIMER_KEY)) {
return true;
}
const res = await modals.openModal(ModsDisclaimerModal);
const haveAccepted = res.exitCode === ModalExitCode.COMPLETED;
if (haveAccepted) {
configService.set(ACCEPTED_DISCLAIMER_KEY, true);
}
return haveAccepted;
}
const loadMods = async (): Promise<void> => {
if (os.isOffline) {
if (gridStatus !== ModsGridStatus.OK) {
return Promise.resolve();
}
const promise = async (): Promise<[BbmFullMod[], BbmModVersion[]]> => {
const available = await lastValueFrom(modsManager.getAvailableMods(version));
const installed = await lastValueFrom(modsManager.getInstalledMods(version));
return [available, installed];
}
return promise().then(([available, installed]) => {
return modsManager.getVersionModsState(version).then(({ available, installed }) => {
const defaultMods = installed?.length ? [] : available.filter(m => m.mod.category === BbmCategories.Core || m.mod.category === BbmCategories.Essential);
setModsAvailable(() => modsToCategoryMap(available));
const installedMods: BbmFullMod[] = installed.map(version => {
const mod = available.find(m => m.mod.id === version.modId);
return mod ? { ...mod, version } : null;
}).filter(mod => mod);
setModsSelected(available.filter(m => m.mod.category === BbmCategories.Core || defaultMods.some(d => m.mod.name.toLowerCase() === d.mod.name.toLowerCase()) || installedMods.some(i => m.mod.id === i.mod.id)));
setModsInstalled(modsToCategoryMap(installedMods));
setModsSelected(available.filter(m => m.mod.category === BbmCategories.Core || defaultMods.some(d => m.mod.name.toLowerCase() === d.mod.name.toLowerCase()) || installed.some(i => m.mod.id === i.mod.id)));
setModsInstalled(modsToCategoryMap(installed));
});
};
useImperativeHandle(forwaredRef, () => ({
loadMods
}), [version]);
useEffect(() => {
if(!isVisible || !isOnline){
if(!isActive){
return noop();
}
(async () => {
if (configService.get<boolean>(ACCEPTED_DISCLAIMER_KEY)) {
return true;
}
const res = await modals.openModal(ModsDisclaimerModal);
const haveAccepted = res.exitCode === ModalExitCode.COMPLETED;
if (haveAccepted) {
configService.set(ACCEPTED_DISCLAIMER_KEY, true);
}
return haveAccepted;
})().then(canLoad => {
ensureDisclaimerAccepted().then(async canLoad => {
if (!canLoad) {
return onDisclamerDecline?.();
}
const status = await modsManager.getModsGridStatus();
setGridStatus(() => status);
loadMods();
});
@@ -230,8 +239,24 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
setMoreInfoMod(null);
setModsAvailable(null);
setModsInstalled(null);
setGridStatus(ModsGridStatus.OK);
};
}, [isVisible, isOnline, version]);
}, [isActive, version]);
useEffect(() => {
// Center the progress bar between buttons
if(isActive){
progress.setStyle({ paddingLeft: "150px", paddingRight: "190px", bottom: "24px" });
} else {
progress.setStyle(null);
}
return () => {
progress.setStyle(null);
}
}, [isActive])
useLayoutEffect(() => {
if (modsAvailable) {
@@ -239,9 +264,29 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
}
}, [modsAvailable]);
const renderStatus = () => {
if (gridStatus === ModsGridStatus.BEATMODS_DOWN) {
return <ModStatus image={BeatConflictImg}>
<span className="text-xl text-center px-2 mt-3 italic">
{te("pages.version-viewer.mods.status.beatmods-down", {links: (<>
<BsmLink className="text-blue-500 underline" href={DISCORD_URL}>
Discord
</BsmLink>
/
<BsmLink className="text-blue-500 underline" href={GITHUB_URL}>
GitHub
</BsmLink>
</>)})}
</span>
</ModStatus>
}
return <ModStatus text={`pages.version-viewer.mods.status.${gridStatus}`} image={BeatConflictImg} />;
}
const renderContent = () => {
if (!isOnline) {
return <ModStatus text="pages.version-viewer.mods.no-internet" image={BeatConflictImg} />;
if (gridStatus !== ModsGridStatus.OK) {
return renderStatus();
}
if (!modsAvailable) {
return <ModStatus text="pages.version-viewer.mods.loading-mods" image={BeatWaitingImg} spin />;
@@ -268,17 +313,21 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
uninstallMod={uninstallMod}
uninstallAllMods={uninstallAllMods}
unselectAllMods={unselectAllMods}
openModsDropZone={() => setModsDropZoneOpen(true)}
/>
</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 }}/>
<div ref={downloadRef} className="flex h-8 justify-center items-center gap-px overflow-hidden rounded-md">
<div className="grow h-full relative">
<BsmButton className="relative left-0 flex items-center justify-center px-2 size-full transition-[top] duration-200 ease-in-out" text="pages.version-viewer.mods.buttons.install-or-update" typeColor="primary" withBar={false} onClick={() => installMods(false)} style={{ top: reinstallAllMods ? "-100%" : "0" }} />
<BsmButton className="relative left-0 flex items-center justify-center px-2 size-full transition-[top] duration-200 ease-in-out " text="pages.version-viewer.mods.buttons.reinstall-all" typeColor="primary" withBar={false} onClick={() => installMods(true)} style={{ top: reinstallAllMods ? "-100%" : "0" }}/>
<div className="flex gap-2">
<div ref={downloadRef} className="flex h-8 justify-center items-center gap-px overflow-hidden rounded-md">
<div className="grow h-full relative">
<BsmButton className="relative left-0 flex items-center justify-center px-2 size-full transition-[top] duration-200 ease-in-out" text="pages.version-viewer.mods.buttons.install-or-update" typeColor="primary" withBar={false} onClick={() => installMods(false)} style={{ top: reinstallAllMods ? "-100%" : "0" }} />
<BsmButton className="relative left-0 flex items-center justify-center px-2 size-full transition-[top] duration-200 ease-in-out " text="pages.version-viewer.mods.buttons.reinstall-all" typeColor="primary" withBar={false} onClick={() => installMods(true)} style={{ top: reinstallAllMods ? "-100%" : "0" }}/>
</div>
<BsmButton className="flex items-center justify-center shrink-0 h-full" iconClassName="transition-transform size-full ease-in-out duration-200" iconStyle={{ transform: reinstallAllMods ? "rotate(360deg)" : "rotate(180deg)" }} icon="chevron-top" typeColor="primary" withBar={false} onClick={() => setReinstallAllMods(prev => !prev)}/>
</div>
<BsmButton className="flex items-center justify-center shrink-0 h-full" iconClassName="transition-transform size-full ease-in-out duration-200" iconStyle={{ transform: reinstallAllMods ? "rotate(360deg)" : "rotate(180deg)" }} icon="chevron-top" typeColor="primary" withBar={false} onClick={() => setReinstallAllMods(prev => !prev)}/>
<Tippy content={t("pages.version-viewer.mods.mods-grid.header-bar.dropdown.import-mods")} theme="default">
<BsmButton className="flex items-center justify-center rounded-md px-1 h-8 aspect-square" icon="download" typeColor="cancel" withBar={false} onClick={() => setModsDropZoneOpen(true)}/>
</Tippy>
</div>
</div>
</>
@@ -286,7 +335,7 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
};
return (
<div ref={ref} className="shrink-0 w-full h-full px-8 pb-7 flex justify-center">
<div className="shrink-0 w-full h-full px-8 pb-7 flex justify-center">
<Dropzone
className="w-full h-full shrink-0"
onFiles={importMods}
@@ -306,15 +355,15 @@ export function ModsSlide({ version, onDisclamerDecline }: { version: BSVersion;
</Dropzone>
</div>
);
}
});
function ModStatus({ text, image, spin = false, children }: { text: string; image: string; spin?: boolean, children?: ReactNode}) {
const t = useTranslation();
function ModStatus({ text, image, spin = false, children }: { text?: string; image: string; spin?: boolean, children?: ReactNode}) {
const { text: t } = useTranslationV2();
return (
<div className="w-full h-full flex flex-col items-center justify-center text-gray-800 dark:text-gray-200">
<img className={`w-32 h-32 ${spin ? "spin-loading" : ""}`} src={image} alt=" " />
<span className="text-xl mt-3 italic">{t(text)}</span>
{text && <span className="text-xl text-center px-2 mt-3 italic">{t(text)}</span>}
{children}
</div>
);

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