Compare commits

...

215 Commits

Author SHA1 Message Date
MathieuG-P 0f0c114710 Merge pull request #55 from Zagrios/feature/Moving-config-file/27
Feature/moving config file/27
2023-01-01 10:06:30 +01:00
MathieuG-P b922c47fc2 config rework 2022-12-31 22:52:45 +01:00
MathieuG-P f0110dcbb8 remove config changes AND fix bug on clone version with linked maps 2022-12-31 22:51:55 +01:00
MathieuG-P ff0d5f535b Merge branch 'master' into feature/Moving-config-file/27 2022-12-31 20:40:41 +01:00
MathieuG-P 0f5ca9647d Merge pull request #54 from Zagrios/feature/dotnet-alert/53
[Feature] Alert if dotnet is not installed
2022-12-30 23:39:43 +01:00
MathieuG-P 1e26c366f8 too much warnings when connection is unstable 2022-12-30 23:36:02 +01:00
MathieuG-P 74aa8defae little tweak 2022-12-30 23:01:52 +01:00
MathieuG-P d013641e53 alert if dotnet not present 2022-12-30 23:01:32 +01:00
MathieuG-P 9ca9207e02 Merge pull request #51 from Zagrios/hotfix/remove-uwu-stuff
hotfix/remove useless uwu stuff
2022-12-29 23:53:49 +01:00
MathieuG-P 0ae11ddc8c remove useless uwu stuff 2022-12-29 23:52:59 +01:00
MathieuG-P a13c7313d6 Merge branch 'master' of https://github.com/Zagrios/bs-manager 2022-12-29 23:38:40 +01:00
MathieuG-P 804c2d7c19 add cert sha 2022-12-29 23:38:35 +01:00
MathieuG-P 1a58243141 Merge pull request #50 from Zagrios/feature/mods-disclaimer/44
feature/add-mods-disclaimer/44
2022-12-29 16:53:55 +01:00
MathieuG-P 574b1fe414 add mods disclaimer 2022-12-29 16:53:15 +01:00
MathieuG-P d127f3b6e7 Merge pull request #49 from Zagrios/hotfix/link-maps-by-default
hotfix/link-maps-by-default
2022-12-29 15:13:09 +01:00
MathieuG-P 2b14f52db9 just try to link maps after download a BS version 2022-12-29 15:10:48 +01:00
MathieuG-P 4143912ef2 Merge pull request #48 from Zagrios/feature/notification-enable-oneclick/47
feature/add-notification-to-enable-oneclick-if-not/47
2022-12-29 13:32:50 +01:00
MathieuG-P 45cbecb21e add notification to enable OneClicks if not 2022-12-29 12:22:35 +01:00
MathieuG-P a8189f28b5 Merge pull request #46 from Zagrios/feature/add-discord-button/43
feature/add-dicord-button/43
2022-12-29 11:10:38 +01:00
MathieuG-P d626ed67f9 add discord button in settings 2022-12-29 11:07:10 +01:00
MathieuG-P b1347b063d Merge pull request #42 from Zagrios/feature/improve-custom-version-design/9
feature/improve-custom-version-nav-bar-ui/9
2022-12-28 23:37:38 +01:00
MathieuG-P caaacf868a improve text sizing AND better tooltip 2022-12-28 23:32:33 +01:00
MathieuG-P 99cf6b8945 Merge pull request #41 from Zagrios/hotfix/light-theme-maps-management
hotfix/light theme maps management
2022-12-28 22:31:19 +01:00
MathieuG-P 2aa9d8afb5 implement light theme for maps-management 2022-12-28 22:30:28 +01:00
MathieuG-P 37dce9769e fix translation 2022-12-28 22:30:11 +01:00
MathieuG-P cdd6d77fc4 deep link enabler light theme 2022-12-28 21:48:27 +01:00
MathieuG-P 0ce21036a7 Merge pull request #40 from Zagrios/feature/swipe-notification-to-close/29
feature/swipe-notification-to-close/29
2022-12-28 21:43:50 +01:00
MathieuG-P f7fd662fef handle swipe to right to close notification 2022-12-28 21:40:55 +01:00
MathieuG-P 46687e5068 Merge pull request #39 from Zagrios/feature/add-splitter-under-versions-list/32
feature/add-splitter-under-versions-list/32
2022-12-28 20:52:42 +01:00
MathieuG-P 9c5cf5160a add spliter under versions and little css styles change 2022-12-28 20:51:12 +01:00
MathieuG-P d391e13f66 Merge pull request #38 from Zagrios/hotfix/install-version-process-was-not-killed
hotfix/fix get main window from windows service
2022-12-28 20:42:49 +01:00
MathieuG-P df166f3dd0 fix get main window from windows service 2022-12-28 20:39:11 +01:00
MathieuG-P 0cc5f8cbb1 Merge pull request #37 from Zagrios/feature/lunch-steam-from-bsm/20
feature/launch-steam-from-bsm/20
2022-12-28 20:31:51 +01:00
MathieuG-P 06622c4088 add button to lunch steam when steam is not running 2022-12-28 20:30:10 +01:00
MathieuG-P a8ca0b26b3 Merge pull request #36 from Zagrios/bugfix/outdated-mod-not-detected-as-outdated/28
bugfix/outdated mod not detected as outdate/28
2022-12-28 19:10:07 +01:00
MathieuG-P c15696a5b8 mod version properly comparated now 2022-12-28 19:05:27 +01:00
MathieuG-P 249caa1a82 Merge pull request #35 from Zagrios/feature/maps-management
feature/maps management
2022-12-28 18:46:37 +01:00
MathieuG-P fab45876bf nothing 2022-12-28 15:35:31 +01:00
MathieuG-P fdb381ca6c Merge branch 'master' into feature/maps-management 2022-12-28 15:34:23 +01:00
MathieuG-P 392d2656a3 fix on play map was selected 2022-12-28 15:06:32 +01:00
MathieuG-P 9ba8af438d fix styles 2022-12-28 14:54:34 +01:00
MathieuG-P 65b2a5c385 download maps button when no maps 2022-12-28 01:38:58 +01:00
MathieuG-P 61a3685d6c sanitize models filename 2022-12-27 00:50:15 +01:00
MathieuG-P a1aeab1d90 remove console logs and useless stuff 2022-12-27 00:11:29 +01:00
MathieuG-P 4cb9482e27 fix fake items in dropdown 2022-12-26 21:50:41 +01:00
MathieuG-P 5d62c6c0e9 download on double click PLUS little fixes 2022-12-26 21:23:10 +01:00
MathieuG-P 6bfe735b60 remove translate todos 2022-12-26 21:04:28 +01:00
MathieuG-P 7d919e9dcf remove force deeplink 2022-12-26 21:03:05 +01:00
MathieuG-P 4293fd9d1b missing return 2022-12-26 21:02:21 +01:00
MathieuG-P af69c821a7 add oneClick system notifications translations 2022-12-26 20:48:36 +01:00
MathieuG-P 36eca807ad add deep link notifications translations 2022-12-26 14:44:04 +01:00
MathieuG-P b217fcf9c0 better handle deep link error 2022-12-26 14:17:27 +01:00
MathieuG-P ef202e33ec add notification and system notification 2022-12-26 00:09:52 +01:00
MathieuG-P 89ac6077be update framer-motion 2022-12-25 22:43:41 +01:00
MathieuG-P 80412e536e no need types for uuid 2022-12-25 22:38:27 +01:00
MathieuG-P c8e4812967 use crypto api for uuid 2022-12-25 22:33:32 +01:00
MathieuG-P b9850effca fix duplicated key 2022-12-25 22:29:58 +01:00
MathieuG-P e9286822ab fix bad indentation 2022-12-25 22:25:06 +01:00
MathieuG-P 9d7cd708c7 deep-links models fully work AND add translations 2022-12-25 22:18:28 +01:00
MathieuG-P c674fce0c0 advancement on onclick install model AND fix circual dependency 2022-12-25 01:04:36 +01:00
MathieuG-P 2aed523480 advancement on oneClick model AND lazy loading react windows 2022-12-24 18:19:59 +01:00
MathieuG-P 7f5a297b87 fix link 2022-12-24 12:59:14 +01:00
MathieuG-P 6c5190eacb initiate model-saber support 2022-12-24 01:18:02 +01:00
MathieuG-P 6f6da723e7 fix delete maps not delete when version is not linked 2022-12-23 22:03:12 +01:00
MathieuG-P b9fda11d34 clean unsubscribe 2022-12-23 21:54:54 +01:00
MathieuG-P 2931b58276 clean 2022-12-23 21:53:07 +01:00
MathieuG-P 68fd4111ad window not closing at the end 2022-12-23 21:52:54 +01:00
MathieuG-P 698eab012f we can now enable deep links for playlists 2022-12-23 21:52:34 +01:00
MathieuG-P e18caca70f one click install playlist working 2022-12-23 00:29:15 +01:00
MathieuG-P d556350eef advencement on oneclick playlists 2022-12-22 00:57:06 +01:00
MathieuG-P 16aa65afed create one click install playlist window 2022-12-20 17:38:45 +01:00
MathieuG-P 7e10d28d28 advence on oneclic playlist 2022-12-20 15:57:26 +01:00
MathieuG-P 58faea04a3 start playlists management 2022-12-20 03:00:51 +01:00
MathieuG-P ee95df202d move maps service 2022-12-20 00:20:37 +01:00
MathieuG-P bd2038cc79 fix warning 2022-12-19 23:58:29 +01:00
MathieuG-P 31183473ab add todo 2022-12-19 23:57:58 +01:00
MathieuG-P 605a383963 fix object desctory when oneClick window close 2022-12-19 03:38:06 +01:00
MathieuG-P 802d0533c8 disable force deep-link 2022-12-19 02:46:23 +01:00
MathieuG-P a886ccb981 tweak log 2022-12-19 02:44:47 +01:00
MathieuG-P b8abe30e39 fix white border AND little tweak 2022-12-19 02:41:12 +01:00
MathieuG-P b8e2bb73f5 increase speed fake progress bar 2022-12-19 02:40:11 +01:00
MathieuG-P 714837286f handle deepLink from score saber AND try fix white border 2022-12-19 01:55:20 +01:00
MathieuG-P a786be653c remove console log 2022-12-19 01:54:50 +01:00
MathieuG-P bdfb7a156f oneclick install map from bsv now work 2022-12-18 03:41:53 +01:00
MathieuG-P c626b850c9 handle enable and disable map deep links 2022-12-17 01:24:37 +01:00
Gaëtan 5a5d31f9ac corretif timestamp 2022-12-13 22:25:23 +01:00
Gaëtan 16db775379 github is broken 2022-12-13 22:24:32 +01:00
Gaëtan b2f76d7aaa change and separe data config.json 2022-12-13 22:21:35 +01:00
Gaëtan Gerard 383f065d1b corretif 1.27 2022-12-13 22:19:18 +01:00
Gaëtan Gerard 3216eddde6 add 1.27 2022-12-13 22:18:30 +01:00
Gaëtan 77ab238863 Config congiguration.service.ts 2022-12-13 11:05:12 +01:00
MathieuG-P ca5c86dbf1 create sevice to manage deep links 2022-12-12 23:35:57 +01:00
Gaëtan 9fbf81f8f2 init change 2022-12-12 22:14:06 +01:00
MathieuG-P 0372f7a1d5 remove console log 2022-12-12 22:01:00 +01:00
MathieuG-P 79ecaf3d15 remove dead code 2022-12-12 21:55:26 +01:00
MathieuG-P 31d8925615 move beat-saver service in main 2022-12-12 21:53:43 +01:00
MathieuG-P 9050d84b03 add site icons for OneClick installs 2022-12-12 21:53:03 +01:00
MathieuG-P 4d0cc4a6fe start of oneclick installs 2022-12-12 02:43:23 +01:00
MathieuG-P 91f7179d5e fix github button style 2022-12-12 00:28:35 +01:00
MathieuG-P d22fb0dcf4 fix translation 2022-12-12 00:03:01 +01:00
MathieuG-P 3f0b6c2db7 add little cross on all modals 2022-12-11 23:37:59 +01:00
MathieuG-P 66f113ae45 add proper filter icon 2022-12-11 23:37:41 +01:00
MathieuG-P efe03f41b9 fix translation 2022-12-11 23:37:05 +01:00
MathieuG-P 055d8b2bcc add all translations 2022-12-11 23:11:23 +01:00
MathieuG-P 6b724d8040 uninstall react tooltip 2022-12-11 23:11:05 +01:00
MathieuG-P b983467cfc change add and link button styles 2022-12-11 13:48:45 +01:00
MathieuG-P d378312dbd add some french translations 2022-12-11 02:20:25 +01:00
MathieuG-P 55e7145ba2 lazy load map images 2022-12-10 17:19:46 +01:00
MathieuG-P 77f6db9b6c change add map button style 2022-12-10 17:19:32 +01:00
MathieuG-P 47474f5c11 add comms 2022-12-10 15:50:10 +01:00
MathieuG-P 3db5cb70b7 optimise map filter check 2022-12-10 15:49:26 +01:00
MathieuG-P 87c37b18d3 fix observable never complete if error 2022-12-10 15:39:26 +01:00
MathieuG-P 160efbfafd no add maps button when offline 2022-12-10 15:38:54 +01:00
MathieuG-P 3be2100160 playlist tab wip 2022-12-10 15:29:33 +01:00
MathieuG-P c4d942eb2e search on sort change AND add search text 2022-12-10 01:29:46 +01:00
MathieuG-P 1ca62ba2f5 handle translation AND onChange reture string 2022-12-10 01:29:23 +01:00
MathieuG-P c5f871d810 remove useless import 2022-12-10 01:28:49 +01:00
MathieuG-P e0d70bd882 change import export MapsPage 2022-12-10 00:15:09 +01:00
MathieuG-P 7423df43c5 fix error for shared maps 2022-12-09 23:56:43 +01:00
MathieuG-P 8cb78e8550 fix styles 2022-12-09 23:56:34 +01:00
MathieuG-P 4308ba5125 fix text style 2022-12-09 23:46:20 +01:00
MathieuG-P 02753999f1 fix add map and link buttons AND fix tag filters set undefined 2022-12-09 23:29:09 +01:00
MathieuG-P 702486020f wrong map style and type 2022-12-09 23:25:05 +01:00
MathieuG-P 6b12b7bc74 fix wrong map style and type 2022-12-09 23:23:20 +01:00
MathieuG-P 5bf674d782 we can export all or specific maps 2022-12-08 00:09:59 +01:00
MathieuG-P 6df2c98955 we can now delete mutliple maps at the same time 2022-12-07 23:10:59 +01:00
MathieuG-P 77bc6236f8 create iframe view and fix preview map 2022-12-07 20:58:46 +01:00
MathieuG-P 57e0c73a31 fix rerendering AND fix downloader map folder name 2022-12-07 01:33:24 +01:00
MathieuG-P f038ae79fb maps and playlists panel are always in one block 2022-12-06 23:32:25 +01:00
MathieuG-P 63775a5e99 fix bug 2022-12-06 23:31:50 +01:00
MathieuG-P 8930009398 we can now download maps from bsaver 2022-12-06 00:52:37 +01:00
MathieuG-P 9dce72250e close on escape 2022-12-06 00:52:06 +01:00
MathieuG-P 0ea41da41c we can now see maps from bsaver 2022-12-05 03:19:05 +01:00
MathieuG-P 36c1eb20fc memoize map item 2022-12-05 03:18:33 +01:00
MathieuG-P 341e8aed99 create download maps modal 2022-12-04 01:08:18 +01:00
MathieuG-P abdc05298b prepare for maps downloading 2022-11-30 23:35:02 +01:00
MathieuG-P b7db145517 add cache for maps details 2022-11-30 00:48:55 +01:00
MathieuG-P 57ab0b9ff2 translate map actions modals 2022-11-28 22:40:45 +01:00
Gaëtan Gerard 398494c320 Merge pull request #25 from Zagrios/update-link-feature-and-report-bug
Updated link "report a bug" and "feature request"
2022-11-28 22:21:48 +01:00
Gaëtan Gerard 43e4a0866c Updated link "report a bug" and "feature request" 2022-11-28 22:19:02 +01:00
MathieuG-P 4e64ae93c2 add progress bar for link unlink and delete maps 2022-11-28 21:14:24 +01:00
MathieuG-P eec011d9f0 single maps deletion finished 2022-11-28 21:13:57 +01:00
Gaëtan Gerard 08b0a5c689 Merge pull request #21 from Zagrios/Issue-Templates-Update
Update issue templates
2022-11-28 11:19:56 +01:00
Gaëtan Gerard 21dee2ec57 Update issue templates 2022-11-28 10:48:52 +01:00
MathieuG-P c172b1ed81 add link unlink maps modals AND start maps deletion 2022-11-28 02:30:02 +01:00
MathieuG-P 69918d0f3f npm i 2022-11-27 23:49:53 +01:00
MathieuG-P d390050183 can now link and unlink version maps 2022-11-26 23:29:50 +01:00
MathieuG-P 31f38820d2 delete useless log 2022-11-24 01:18:18 +01:00
MathieuG-P 8372d3bed9 add linked unlinked maps indication 2022-11-24 01:18:08 +01:00
MathieuG-P 46f5f9486c map filter and search works 2022-11-22 00:21:22 +01:00
MathieuG-P 1e6351aaf9 fix label 2022-11-21 22:31:24 +01:00
MathieuG-P f9edf33097 progress on filter panel 2022-11-21 00:05:20 +01:00
MathieuG-P f815695298 add filters panel (partial) 2022-11-20 00:54:30 +01:00
MathieuG-P bbbbf50551 update tailwindcss 2022-11-19 15:31:17 +01:00
MathieuG-P 0c74e9683b npm i 2022-11-19 15:21:51 +01:00
MathieuG-P b1b4d13dfc Merge branch 'master' into feature/maps-management 2022-11-19 15:18:51 +01:00
MathieuG-P 5860ab8eb9 . 2022-11-19 15:12:17 +01:00
MathieuG-P a4af268720 add registry in npm config file 2022-11-19 15:12:02 +01:00
MathieuG-P 092b7fbd77 add 1.26.0 2022-11-18 17:09:56 +01:00
MathieuG-P 58c8c32a58 we are no longer blocked if error occur durring update downloading 2022-11-17 23:50:13 +01:00
MathieuG-P 69c4202899 Merge branch 'master' of https://github.com/Zagrios/bs-manager 2022-11-17 23:42:52 +01:00
MathieuG-P 43cc008011 add version number on log file 2022-11-17 23:42:41 +01:00
MathieuG-P 821e487c93 prototype of maps searchbar 2022-11-17 00:07:46 +01:00
MathieuG-P 42e9c2ed58 remove saturation 2022-11-16 23:32:12 +01:00
MathieuG-P 6ea7f45d40 remove saturation 2022-11-16 23:28:10 +01:00
MathieuG-P 5c2edcf558 no preview available when no mapId 2022-11-16 23:27:41 +01:00
MathieuG-P 3333924511 delete comment 2022-11-16 23:27:24 +01:00
Gaëtan d200444d75 Merge branch 'master' of https://github.com/Zagrios/bs-manager 2022-11-15 22:13:06 +01:00
MathieuG-P 82b33fbf95 maps navbar in panel 2022-11-15 22:11:48 +01:00
MathieuG-P c985fd09bd css fix 2022-11-14 23:04:52 +01:00
MathieuG-P c312b6ede8 only maps and not maps and playlists 2022-11-14 23:04:44 +01:00
MathieuG-P e76adf7a5f stop propagation on click 2022-11-14 23:04:27 +01:00
MathieuG-P 7dae0bf77f create empty maps toolbar 2022-11-14 23:04:14 +01:00
MathieuG-P ed9bc0c3a8 can select maps and fix blur issue on translate 2022-11-14 23:03:19 +01:00
Gaëtan 9d6638c35e add services 2022-11-14 15:27:41 +01:00
MathieuG-P 5ba670505b add info and optimize rendering 2022-11-14 00:24:43 +01:00
MathieuG-P ef000b7eda add maps and playlists list to versions 2022-11-14 00:23:07 +01:00
MathieuG-P cfda3830db fix css issue 2022-11-14 00:22:43 +01:00
MathieuG-P f0cf76a5fe VisibilitySensor was causing unwanted destroy 2022-11-14 00:18:46 +01:00
MathieuG-P ce7438825b Update feature_request.md 2022-11-13 19:43:37 +01:00
MathieuG-P 0d0ed47455 Update bug_report.md 2022-11-13 19:41:13 +01:00
MathieuG-P 8217a577a0 missing icons and add difficulties 2022-11-13 02:14:53 +01:00
MathieuG-P 4c36f2321a npm run audit fix 2022-11-12 14:48:03 +01:00
MathieuG-P 4223a17341 play song and add data to map item 2022-11-11 02:43:30 +01:00
MathieuG-P 6bb99ace5c add bsm icon png 2022-11-10 13:25:15 +01:00
MathieuG-P de86519282 add map data 2022-11-10 00:52:59 +01:00
MathieuG-P 3a30e5fbc9 right type 2022-11-10 00:52:47 +01:00
MathieuG-P 68ee96b8e3 change map item style 2022-11-09 22:55:15 +01:00
MathieuG-P cf78b2b21d add song author 2022-11-09 22:55:07 +01:00
MathieuG-P 0a5c158657 get maps details from bsaver api 2022-11-09 00:46:01 +01:00
MathieuG-P 46766ec0ff Merge pull request #18 from Zagrios/Update-Tailwind
Update tailwind setup
2022-11-08 22:49:45 +01:00
MathieuG-P 300fad0010 add type tailwind 2022-11-08 22:41:44 +01:00
MathieuG-P 84450419c1 scroll by mouse position 2022-11-08 22:39:00 +01:00
Gaëtan Gerard fd5c14a1bd Update Tailwind 2022-11-08 16:39:14 +01:00
MathieuG-P cd8dae5976 maps managment advancement 2022-11-07 17:09:19 +01:00
MathieuG-P 5e2dd10035 fully load local maps, and prepare bsaver map infos 2022-11-06 01:38:31 +01:00
MathieuG-P df4df8add1 for system file link protocol 2022-11-06 01:35:58 +01:00
MathieuG-P 4544ee5622 maps nav bar active when on maps page 2022-11-06 01:35:29 +01:00
MathieuG-P dd7bc3556d maps models 2022-11-06 01:33:29 +01:00
MathieuG-P 788b7598b0 add protocol to support system file link 2022-11-06 01:33:03 +01:00
MathieuG-P a4f4eb5558 off some rules 2022-11-06 01:32:35 +01:00
MathieuG-P 43cdcbf765 add maps and playlist slide 2022-11-01 23:04:39 +01:00
MathieuG-P 2a9d7f3a15 maps manager service prototype 2022-11-01 23:04:28 +01:00
MathieuG-P 5c5e6c7439 hover effect more generic 2022-11-01 23:04:13 +01:00
MathieuG-P 322faa6a92 create page of shared maps and playlists 2022-11-01 18:42:15 +01:00
MathieuG-P 701e91462d fix fill-rule 2022-10-31 23:19:29 +01:00
MathieuG-P 2b34d643a9 add missing translations 2022-10-31 23:17:38 +01:00
MathieuG-P 4a7949fd2a modal system lot impoved and simpler to use 2022-10-31 23:04:42 +01:00
MathieuG-P 47fce8a6dd Merge pull request #17 from Zagrios/feature/oculus-installation-detection
Feature/oculus installation detection
2022-10-31 20:46:02 +01:00
MathieuG-P 4d6d3cf4f8 remove useless imports 2022-10-31 20:28:44 +01:00
MathieuG-P 26236ba024 add transltations 2022-10-31 20:26:08 +01:00
MathieuG-P dcb932859b fix steam running check even if it's oculus version 2022-10-31 20:22:28 +01:00
MathieuG-P a9646209e1 oculus detection complete 2022-10-30 22:40:18 +01:00
MathieuG-P 6ce7bc94df update changelog 2022-10-28 14:54:26 +02:00
MathieuG-P 5b82f4c162 detection of oculus beat saber installation 2022-10-26 01:13:12 +02:00
192 changed files with 11336 additions and 4313 deletions
+1
View File
@@ -4,6 +4,7 @@
"global-require": "off",
"import/no-dynamic-require": "off",
"prettier/prettier": 0,
"no-await-in-loop": "off",
"import/prefer-default-export": "off",
"no-empty-function": "off",
"@typescript-eslint/no-empty-function": "off",
+28 -36
View File
@@ -10,6 +10,7 @@ import ReactRefreshWebpackPlugin from '@pmmmwh/react-refresh-webpack-plugin';
import baseConfig from './webpack.config.base';
import webpackPaths from './webpack.paths';
import checkNodeEnv from '../scripts/check-node-env';
import { AppWindow } from "../../src/shared/models/window-manager/app-window.model"
// When an ESLint server is running, we can't set the NODE_ENV so we'll check if it's
// at the dev webpack config is not accidentally run in a production environment
@@ -39,6 +40,22 @@ if (
execSync('npm run postinstall');
}
const getHtmlPageOptions = (page: AppWindow): HtmlWebpackPlugin.Options => {
return {
filename: path.join(page),
template: path.join(webpackPaths.srcRendererPath, page.replace(".html", ".ejs")),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
}
}
const configuration: webpack.Configuration = {
devtool: 'inline-source-map',
@@ -76,14 +93,6 @@ const configuration: webpack.Configuration = {
},
},
'sass-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [require('tailwindcss'), require('autoprefixer')],
},
},
},
],
include: /\.module\.s?(c|a)ss$/,
},
@@ -96,8 +105,12 @@ const configuration: webpack.Configuration = {
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [require('tailwindcss'), require('autoprefixer')],
postcssOptions: {
plugins:
[
require('tailwindcss'),
require('autoprefixer'),
]
},
},
},
@@ -151,33 +164,12 @@ const configuration: webpack.Configuration = {
new ReactRefreshWebpackPlugin(),
new HtmlWebpackPlugin({
filename: path.join('index.html'),
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
}),
new HtmlWebpackPlugin(getHtmlPageOptions("index.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("launcher.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-map.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-playlist.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-model.html")),
new HtmlWebpackPlugin({
filename: path.join('launcher.html'),
template: path.join(webpackPaths.srcRendererPath, 'launcher.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
})
],
node: {
+29 -32
View File
@@ -14,6 +14,7 @@ import baseConfig from './webpack.config.base';
import webpackPaths from './webpack.paths';
import checkNodeEnv from '../scripts/check-node-env';
import deleteSourceMaps from '../scripts/delete-source-maps';
import { AppWindow } from "../../src/shared/models/window-manager/app-window.model"
checkNodeEnv('production');
deleteSourceMaps();
@@ -25,6 +26,23 @@ const devtoolsConfig =
}
: {};
const getHtmlPageOptions = (page: AppWindow): HtmlWebpackPlugin.Options => {
return {
filename: path.join(page),
template: path.join(webpackPaths.srcRendererPath, page.replace(".html", ".ejs")),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
env: process.env.NODE_ENV,
isDevelopment: process.env.NODE_ENV !== 'production',
nodeModules: webpackPaths.appNodeModulesPath,
}
}
const configuration: webpack.Configuration = {
...devtoolsConfig,
@@ -58,14 +76,6 @@ const configuration: webpack.Configuration = {
},
},
'sass-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [require('tailwindcss'), require('autoprefixer')],
},
},
},
],
include: /\.module\.s?(c|a)ss$/,
},
@@ -78,8 +88,12 @@ const configuration: webpack.Configuration = {
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [require('tailwindcss'), require('autoprefixer')],
postcssOptions: {
plugins:
[
require('tailwindcss'),
require('autoprefixer'),
]
},
},
},
@@ -132,29 +146,12 @@ const configuration: webpack.Configuration = {
analyzerMode: process.env.ANALYZE === 'true' ? 'server' : 'disabled',
}),
new HtmlWebpackPlugin({
filename: 'index.html',
template: path.join(webpackPaths.srcRendererPath, 'index.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
isDevelopment: process.env.NODE_ENV !== 'production',
}),
new HtmlWebpackPlugin(getHtmlPageOptions("index.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("launcher.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-map.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-playlist.html")),
new HtmlWebpackPlugin(getHtmlPageOptions("oneclick-download-model.html")),
new HtmlWebpackPlugin({
filename: 'launcher.html',
template: path.join(webpackPaths.srcRendererPath, 'launcher.ejs'),
minify: {
collapseWhitespace: true,
removeAttributeQuotes: true,
removeComments: true,
},
isBrowser: false,
isDevelopment: process.env.NODE_ENV !== 'production',
})
],
};
+6 -1
View File
@@ -10,6 +10,8 @@ module.exports = {
"global-require": 0,
"import/prefer-default-export": "off",
"no-empty-function": "off",
"no-await-in-loop": "off",
"no-continue": "off",
"@typescript-eslint/no-empty-function": "off",
'@typescript-eslint/lines-between-class-members': "off",
"class-methods-use-this": "off",
@@ -30,7 +32,10 @@ module.exports = {
"@typescript-eslint/no-throw-literal": "off",
"@typescript-eslint/no-unused-expressions": "off",
"no-param-reassign": "off",
"no-useless-escape": "off"
"no-useless-escape": "off",
"jsx-a11y/no-noninteractive-element-interactions": "off",
"no-async-promise-executor": "off",
"new-cap": "off"
},
parserOptions: {
ecmaVersion: 2020,
@@ -1,7 +1,7 @@
---
name: Bug report
name: "[BUG] Bug report"
about: Create a report to help us improve
title: "[BUG]"
title: "[BUG] : "
labels: bug
assignees: Zagrios
@@ -1,7 +1,7 @@
---
name: Feature request
name: "[FEAT.] Feature request"
about: Suggest an idea for this project
title: "[AME]"
title: "[FEAT.] : "
labels: enhancement
assignees: Zagrios
+1
View File
@@ -0,0 +1 @@
registry=https://registry.npmjs.org/
+14 -1
View File
@@ -1,3 +1,16 @@
## [0.2.0](https://github.com/Zagrios/bs-manager/releases/tag/v0.2.0) (October 23, 2022)
### Features
- Mods management fully implementd [5c47897](https://github.com/Zagrios/bs-manager/commit/5c478979e96b14dd54a3889e551c163ece701b4a)
- Add close button in settings page #5
- Add GitHub and contribution actions in settings page #3
### Fixes
- Lunch Beat Saber multiple times #1
- Placeholders and some texts not with light theme #6
- Wrong background color when version is downloading #7
- Add missing translations [5817260](https://github.com/Zagrios/bs-manager/commit/58172606f3f4731c90c9aa838fb89ac95458e032)
- Error when loading downloaded versions when Steam is not installed [6e07adb](https://github.com/Zagrios/bs-manager/commit/6e07adb990f78073dd68c2b9cf6209562bb4bf2b)
## [0.1.0](https://github.com/Zagrios/bs-manager/releases/tag/v0.1.0) (September 19, 2022)
* Initial public release
- Initial public release
+1 -1
View File
@@ -1,5 +1,5 @@
<p align="center">
<a href="https://github.com/RiskiVR/BSLegacyLauncher">
<a href="https://github.com/Zagrios/bs-manager">
<img src="https://raw.githubusercontent.com/Zagrios/bs-manager/master/resources/readme/icon.svg" alt="logo" width="250px"/>
</a>
<h3 align="center">BSManager</h3>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" version="1.1" viewBox="0 0 717.2 717.2" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
</style>
<linearGradient id="SVGID_1_" x1="61.604" x2="647.04" y1="373.65" y2="373.06" gradientTransform="matrix(1 .0010142 -.0010142 1 4.6349 -15.227)" gradientUnits="userSpaceOnUse">
<stop style="stop-color:#DC2DE2" offset=".010541"/>
<stop style="stop-color:#972DE2" offset=".3269"/>
<stop style="stop-color:#8F28E2" offset=".4648"/>
<stop style="stop-color:#7A1BE1" offset=".6756"/>
<stop style="stop-color:#5706E0" offset=".9318"/>
<stop style="stop-color:#4D00E0" offset="1"/>
</linearGradient>
<path class="st0" d="m565.85 141.83c-19.8 11.3-37 22.3-55.2 31.1-14.1 6.8-28.8 4.5-42.4-3.6-29.4-17.4-58.9-34.5-88.2-52-12.9-7.7-25.7-9.5-39-1.9-50.3 28.9-100.6 58-150.8 87.1-8.8 5.1-9.2 10 0 15.3 52 30.2 103.9 60.4 155.7 91 10.9 6.4 20.4 4.2 30.2-1.5 68.7-39.9 137.4-79.8 206.1-119.7 5.3-3.1 10.5-6.2 16-9 19.3-9.8 25.3-10.1 36.2 8.3 8.5 14.2 15.2 31.8 15.7 48.1 1.8 64.6 1.1 129.3 1 193.9 0 22.4-0.1 44.8-1.5 67.1-1 20.4-12.2 38.9-29.8 49.2-55.9 33.5-112.6 65.7-169 98.3-2.8 0.9-5.8 1.6-8.7 2 1-24.6 0.4-47.9 4.3-70.5 1.6-9.2 12-18.8 20.8-24.3 31.5-19.8 63.8-38.3 96.3-56.3 11.3-6.3 15-13.9 14.9-26.8-0.7-58-0.2-116-0.2-174 0-12.7-5.1-16.3-16.2-10-52.1 29.9-104.2 59.9-155.7 90.9-5.5 3.3-9.9 13.4-9.9 20.3-0.6 65.7 0.2 131.4 0.4 197.2 0.1 22.9-0.1 45.8-0.4 68.7-0.1 11.5-4.9 19.4-17 21.9-20.7 4.3-40.3 3-59-8-67.7-39.5-134.6-79.5-202.9-117.2-35.7-19.7-52.4-47.2-51.7-87.8 0.9-58 0.4-116.1 0.5-174.2-0.2-1.9 0.5-3.8 1.4-10.2 25.3 21 61.8 24.5 70.5 60.3 3.8 15.4 1.4 32.4 1.6 48.7 0.3 26 0 52 1.3 77.9 0.3 6.1 4 14.8 8.8 17.7 52.9 31.5 106.6 62.1 159.9 92.9 3.5 1.3 7.1 2.3 10.8 2.9 0.6-17.9 1.4-34.4 1.5-50.9 0.1-47.4 0.4-94.7-0.7-142.1-1-7.8-5.3-14.8-11.8-19.1-61.5-36.7-123.7-72.6-185.8-108.7-16.2-9.5-33-18.3-48.5-28.9-5-4.2-7.6-10.5-7.2-17 3.7-21.4 16.8-40 35.6-50.8 71.6-41.3 143-82.6 214.4-123.9 27.1-15.7 54.2-16 81.3-0.4 52.5 30.4 104.9 60.9 157.3 91.4 2.1 1.2 4.1 2.9 9.1 6.6z" style="fill:url(#SVGID_1_)"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

+16
View File
@@ -493,5 +493,21 @@
"ReleaseImg": "https://cdn.cloudflare.steamstatic.com/steamcommunity/public/images/clans//32055887/52cbd15b5e226f509e051565f6e4935eb0e44e2c.png",
"ReleaseDate": "1665673269",
"year": "2022"
},
{
"BSVersion": "1.26.0",
"BSManifest": "3634450655207470002",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/3414318646334948892",
"ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans//32055887/d18ad9d7f45a5cc2a467d75f0bdf9d32b9141b14.png",
"ReleaseDate": "1667923560",
"year": "2022"
},
{
"BSVersion": "1.27.0",
"BSManifest": "3485516174915301618",
"ReleaseURL": "https://steamcommunity.com/games/620980/announcements/detail/3629368753553708497",
"ReleaseImg": "https://cdn.akamai.steamstatic.com/steamcommunity/public/images/clans//32055887/d9ed8771498be7fc83721aaaaa5b15a08fbeadb1.png",
"ReleaseDate": "1670943003",
"year": "2022"
}
]
+597 -304
View File
@@ -1,360 +1,653 @@
{
"misc":{
"download": "Download",
"verify": "Verify",
"launch": "Launch",
"mods": "Mods",
"maps": "Maps",
"cancel": "Cancel"
},
"pages":{
"version-viewer":{
"launch-mods":{
"oculus": "Oculus Mod",
"desktop": "Desktop Mod",
"debug": "Debug Mod"
},
"mods":{
"loading-mods": "Loading mods...",
"buttons":{
"more-infos": "More info",
"install-or-update": "Install or update"
"misc": {
"download": "Download",
"verify": "Verify",
"launch": "Launch",
"mods": "Mods",
"maps": "Maps",
"playlists": "Playlists",
"models": "Models",
"cancel": "Cancel",
"delete": "Delete",
"accept": "Accept",
"refuse": "Refuse"
},
"pages": {
"version-viewer": {
"launch-mods": {
"oculus": "Oculus Mod",
"desktop": "Desktop Mod",
"debug": "Debug Mod"
},
"mods-grid":{
"header-bar":{
"name": "Name",
"installed": "Installed",
"latest": "Latest",
"description":"Description",
"dropdown":{
"uninstall-all":"Uninstall all"
"maps": {
"search-bar": {
"search-placeholder": "Search a map",
"filters-btn": "Filters",
"dropdown": {
"export-maps": "Export maps",
"delete-maps": "Delete maps"
}
},
"tabs": {
"maps": {
"actions": {
"add-maps": {
"text": "Add"
},
"link-maps": {
"tooltips": {
"link": "Link maps",
"unlink": "Unling maps"
}
}
},
"empty-maps":{
"text":"No maps",
"button":"Download maps"
}
}
}
}
},
"dropdown":{
"open-folder": "Open folder",
"verify-files": "Verify files",
"clone": "Clone",
"edit": "Edit",
"uninstall": "Uninstall"
}
},
"available-versions":{
"title": "Select a version",
"steam-release":"Release page"
},
"settings":{
"steam":{
"title":"Steam",
"description":"If you log out of Steam, you will have to log back in to download a new version of Beat Saber.",
"logout":"Log out"
},
"appearance":{
"title":"Appearance",
"description":"Choose the two main colours of BSManager.",
"reset":"Reset",
"sub-title":"Theme",
"themes":{
"dark":"Dark",
"light": "Light",
"os": "Sync with computer"
}
},
"installation-folder":{
"title":"Installation folder",
"description":"Change the default folder for Beat Saber versions and other upcoming features.",
"choose-folder":"Choose folder"
},
"language":{
"title":"Language",
"description":"Select a language.",
"languages":{
"en-EN": "English, UK",
"en-US": "English, US",
"fr-FR": "Français",
"es-ES": "Español",
"translated":{
"en-EN": "English, United Kingdom",
"en-US": "English, USA",
"fr-FR": "French",
"es-ES": "Spanish"
}
}
},
"patreon":{
"title":"Support BSManager 💖",
"description": "Support the project and help us to provide continuous improvements to BSManager.",
"buttons":{
"support": "Support BSManager",
"supporters":"Supporters 👀"
},
"view":{
"no-supporters": "No supporter yet",
"sponsors":"Sponsors",
"supporters":"Supporters"
"mods": {
"loading-mods": "Loading mods...",
"buttons": {
"more-infos": "More info",
"install-or-update": "Install or update"
},
"mods-grid": {
"header-bar": {
"name": "Name",
"installed": "Installed",
"latest": "Latest",
"description": "Description",
"dropdown": {
"uninstall-all": "Uninstall all"
}
}
}
},
"dropdown": {
"open-folder": "Open folder",
"verify-files": "Verify files",
"clone": "Clone",
"edit": "Edit",
"uninstall": "Uninstall"
}
},
"contribution":{
"description":"Suggest new features or report a bug to help improve BSManager!",
"buttons":{
"request-features":"Request a feature",
"report-bug":"Report a bug",
"open-logs": "Open logs"
"available-versions": {
"title": "Select a version",
"steam-release": "Release page"
},
"settings": {
"steam": {
"title": "Steam",
"description": "If you log out of Steam, you will have to log back in to download a new version of Beat Saber.",
"logout": "Log out"
},
"appearance": {
"title": "Appearance",
"description": "Choose the two main colours of BSManager.",
"reset": "Reset",
"sub-title": "Theme",
"themes": {
"dark": "Dark",
"light": "Light",
"os": "Sync with computer"
}
},
"installation-folder": {
"title": "Installation folder",
"description": "Change the default folder for Beat Saber versions and other upcoming features.",
"choose-folder": "Choose folder"
},
"additional-content": {
"title": "Additional content",
"description": "Additional content allows you to customize BeatSaber!",
"deep-links": {
"sub-title": "OneClick installations"
}
},
"language": {
"title": "Language",
"description": "Select a language.",
"languages": {
"en-EN": "English, UK",
"en-US": "English, US",
"fr-FR": "Français",
"es-ES": "Español",
"translated": {
"en-EN": "English, United Kingdom",
"en-US": "English, USA",
"fr-FR": "French",
"es-ES": "Spanish"
}
}
},
"patreon": {
"title": "Support BSManager 💖",
"description": "Support the project and help us to provide continuous improvements to BSManager.",
"buttons": {
"support": "Support BSManager",
"supporters": "Supporters 👀"
},
"view": {
"no-supporters": "No supporter yet",
"sponsors": "Sponsors",
"supporters": "Supporters"
}
},
"discord": {
"description": "Join the BSManager community by joining our Discord!"
},
"contribution": {
"description": "Suggest new features or report a bug to help improve BSManager!",
"buttons": {
"request-features": "Request a feature",
"report-bug": "Report a bug",
"open-logs": "Open logs"
}
}
}
}
},
"notifications":{
"types":{
"error": "🚨 Error",
"warning": "⚠️ Warning",
"success": "🎉 Success"
},
"common":{
"msg":{
},
"notifications": {
"types": {
"error": "🚨 Error",
"warning": "⚠️ Warning",
"success": "🎉 Success"
},
"common": {
"msg": {
"error-occurred": "An error has occurred"
}
},
"shared":{
"errors":{
"titles":{
"operation-running": "Operation running"
"shared": {
"errors": {
"titles": {
"operation-running": "Operation running"
},
"msg": {
"operation-running": "Wait for the end of the current operation then start again."
}
}
},
"bs-download": {
"success": {
"titles": {
"download-success": "Download complete",
"verification-finished": "Verification complete"
}
},
"msg":{
"operation-running": "Wait for the end of the current operation then start again."
}
}
},
"bs-download":{
"success":{
"titles":{
"download-success": "Download complete",
"verification-finished":"Verification complete"
}
},
"warnings":{
"msg":{
"ManifestChecksum": "The previously downloaded manifest does not match the new 🤔",
"ConnectionTimeout": "Your internet connection seems unstable 🥶",
"ConnectionLost": "The connection has been lost, try again...",
"ConnectionError": "Unable to connect to Steam, retry...",
"Unknown": "Something strange has happened 🤔 Your connection is probably unstable."
}
},
"errors":{
"msg":{
"Password": "Password is invalid.",
"InvalidCredentials": "Your e-mail or password is invalid 😴",
"InvalidPassword": "Your password is invalid 😕",
"NoManifest": "No manifest was found",
"DirectoryCreate": "Unable to install the necessary folders.",
"NotAvailableApp": "Are you trying to download BeatSaber when you don't have it ? 🤣",
"DepotNotFound": "Unable to download BeatSaber 😥 try again later 😕",
"NotCompleted": "The download could not complete ¯\\_(ツ)_/¯",
"InvalidManifest": "Unable to download BeatSaber 😥 try again later 😕",
"NoValidKey": "Unable to download BeatSaber 😥 try again later 😕",
"NoManifestCode": "Unable to download BeatSaber 😥 try again later 😕",
"401": "Steam doesn't seem to want to let us download BeatSaber 😢",
"404": "Unable to contact Steam servers.",
"Unknown": "An unknown error occurred ¯\\_(ツ)_/¯",
"NoServer": "Unable to contact Steam servers.",
"NotAllowed": "Apparently you are not allowed to download BeatSaber 🥱",
"ConnectionTimeout": "Cannot connect to Steam 😕",
"SteamLib": "If you have this error, report the bug on GitHub with the logs pls.",
"ConnectionError": "Unable to connect to Steam after 10 tries 🤯",
"LicenceError": "Unable to get the list of licenses.",
"verification-failed": "Verification failed, try again later.",
"RateLimitExceeded": "You've tried too many times, wait a while and try again later.",
"AlreadyDownloading": "A download is already in progress",
"no-internet": "No internet connection."
}
}
},
"settings":{
"move-folder":{
"success":{
"titles":{
"transfer-finished":"Transfer complete"
}
"warnings": {
"msg": {
"ManifestChecksum": "The previously downloaded manifest does not match the new 🤔",
"ConnectionTimeout": "Your internet connection seems unstable 🥶",
"ConnectionLost": "The connection has been lost, try again...",
"ConnectionError": "Unable to connect to Steam, retry...",
"Unknown": "Something strange has happened 🤔 Your connection is probably unstable."
}
},
"errors":{
"titles":{
"transfer-failed":"Transfer failed 😕"
}
}
},
"steam":{
"success":{
"titles":{
"logout": "Disconnected from Steam."
}
}
}
},
"bs-launch":{
"success":{
"titles":{
"launching": "Launching...🚀"
}
},
"errors":{
"titles":{
"UNABLE_TO_LAUNCH": "Unable to launch",
"STEAM_NOT_RUNNING": "Steam is not running",
"BS_ALREADY_RUNNING": "BeatSaber already running",
"EXE_NOT_FINDED": "Missing files",
"EXIT": "Abrupt stop"
},
"msg":{
"STEAM_NOT_RUNNING": "Steam must be running to launch BeatSaber.",
"BS_ALREADY_RUNNING": "Close BeatSaber before launching it again.",
"EXE_NOT_FINDED": "Some files seem to be missing, try to verify the files.",
"EXIT": "BeatSaber stopped abruptly, tries to check the files."
}
}
},
"custom-version":{
"errors":{
"titles":{
"CantEditSteam": "Unable to edit",
"CantRename": "Renaming impossible",
"VersionAlreadExist": "This version already exists",
"CantClone": "Cloning impossible"
},
"msg":{
"CantEditSteam": "You can't edit the Steam version. You can clone it though."
}
},
"success":{
"titles":{
"CloningFinished": "Cloning complete 🎉"
}
}
},
"mods":{
"install-mods":{
"errors": {
"titles":{
"dotnet-required": ".NET 6 Required"
},
"msg": {
"401": "Steam doesn't seem to want to let us download BeatSaber 😢",
"404": "Unable to contact Steam servers.",
"Password": "Password is invalid.",
"InvalidCredentials": "Your e-mail or password is invalid 😴",
"InvalidPassword": "Your password is invalid 😕",
"NoManifest": "No manifest was found",
"DirectoryCreate": "Unable to install the necessary folders.",
"NotAvailableApp": "Are you trying to download BeatSaber when you don't have it ? 🤣",
"DepotNotFound": "Unable to download BeatSaber 😥 try again later 😕",
"NotCompleted": "The download could not complete ¯\\_(ツ)_/¯",
"InvalidManifest": "Unable to download BeatSaber 😥 try again later 😕",
"NoValidKey": "Unable to download BeatSaber 😥 try again later 😕",
"NoManifestCode": "Unable to download BeatSaber 😥 try again later 😕",
"Unknown": "An unknown error occurred ¯\\_(ツ)_/¯",
"NoServer": "Unable to contact Steam servers.",
"NotAllowed": "Apparently you are not allowed to download BeatSaber 🥱",
"ConnectionTimeout": "Cannot connect to Steam 😕",
"SteamLib": "If you have this error, report the bug on GitHub with the logs pls.",
"ConnectionError": "Unable to connect to Steam after 10 tries 🤯",
"LicenceError": "Unable to get the list of licenses.",
"verification-failed": "Verification failed, try again later.",
"RateLimitExceeded": "You've tried too many times, wait a while and try again later.",
"AlreadyDownloading": "A download is already in progress",
"no-internet": "No internet connection.",
"dotnet-required": ".NET 6 Runtime must be installed in order to download a version of BeatSaber. Download it by clicking the button below."
},
"actions":{
"download-dotnet": "Download .NET 6"
}
}
},
"settings": {
"move-folder": {
"success": {
"titles": {
"transfer-finished": "Transfer complete"
}
},
"errors": {
"titles": {
"transfer-failed": "Transfer failed 😕"
}
}
},
"steam": {
"success": {
"titles": {
"logout": "Disconnected from Steam."
}
}
},
"additional-content":{
"deep-link": {
"activation":{
"success":{
"title":"OneClick activated!",
"description":"OneClick installations have been activated."
},
"error":{
"description":"Unable to activate OneClick installations."
}
},
"deactivation":{
"success":{
"title":"OneClick deactivated!",
"description":"OneClick installations have been deactivated."
},
"error":{
"description":"An unknown error occurred."
}
},
"check-all-enabled":{
"title": "OneClick Disabled",
"description": "One or more OneClick installations are disabled. Go to settings to enable them.",
"actions":{
"settings": "Settings",
"not-remind": "Do not remind me"
}
}
}
}
},
"bs-launch": {
"success": {
"titles": {
"launching": "Launching...🚀"
}
},
"errors": {
"titles": {
"UNABLE_TO_LAUNCH": "Unable to launch",
"STEAM_NOT_RUNNING": "Steam is not running",
"OCULUS_NOT_RUNNING": "Oculus is not running",
"BS_ALREADY_RUNNING": "BeatSaber already running",
"EXE_NOT_FINDED": "Missing files",
"EXIT": "Abrupt stop"
},
"msg": {
"STEAM_NOT_RUNNING": "Steam must be running to launch BeatSaber.",
"OCULUS_NOT_RUNNING": "Oculus must be running to launch BeatSaber.",
"BS_ALREADY_RUNNING": "Close BeatSaber before launching it again.",
"EXE_NOT_FINDED": "Some files seem to be missing, try to verify the files.",
"EXIT": "BeatSaber stopped abruptly, tries to check the files."
},
"actions":{
"STEAM_NOT_RUNNING": "Launch Steam"
}
}
},
"steam":{
"steam-launching":{
"title": "Steam is launching!",
"description": "Steam may take some time to launch depending on your configuration."
}
},
"custom-version": {
"errors": {
"titles": {
"CantEditSteam": "Unable to edit",
"CantRename": "Renaming impossible",
"VersionAlreadExist": "This version already exists",
"CantClone": "Cloning impossible"
},
"msg": {
"CantEditSteam": "You can't edit the Steam version. You can clone it though."
}
},
"success": {
"titles": {
"CloningFinished": "Cloning complete 🎉"
}
}
},
"mods": {
"install-mods": {
"titles": {
"success": "Mods installed 🎉",
"warning": "Mods installed 🤔"
},
"msg":{
"msg": {
"success": "All mods have been installed.",
"warning": "One or more mods could not be installed.",
"errors":{
"errors": {
"no-mods": "No mods to install.",
"cannot-install-bsipa": "BSIPA installation failed 😨"
}
}
},
"uninstall-mod":{
"titles":{
"uninstall-mod": {
"titles": {
"success": "Mod uninstalled 🎉"
},
"msg":{
"errors":{
"msg": {
"errors": {
"no-mods": "This mod isn't installed 😑"
}
}
},
"uninstall-all-mods":{
"titles":{
"uninstall-all-mods": {
"titles": {
"success": "Mods uninstalled 🎉"
},
"msg":{
"msg": {
"success": "All mods have been uninstalled.",
"errors": {
"no-mods": "No mod is installed in this version 😑"
}
}
}
},
"maps":{
"one-click-install":{
"success":"Map installation complete",
"error":"An error occurred while installing the map"
}
},
"playlists":{
"one-click-install":{
"success":"Playlist installation complete",
"error":"An error occurred while installing the playlist"
}
},
"models":{
"one-click-install":{
"success":"Model installation complete",
"error":"An error occurred while installing the model"
}
}
},
"modals":{
"guard":{
"title": "Steam Guard",
"inputs":{
"guard-code":{
"label": "Guard Code",
"placeholder": "Enter your Guard code"
}
},
"buttons":{
"submit": "Login"
}
},
"steam-login":{
"title": "Steam Login",
"inputs":{
"username":{
"label": "Username",
"placeholder": "Enter your username"
},
"modals": {
"guard": {
"title": "Steam Guard",
"inputs": {
"guard-code": {
"label": "Guard Code",
"placeholder": "Enter your Guard code"
}
},
"password":{
"label": "Password",
"placeholder": "Enter your password"
},
"stay":{
"label": "Stay connected"
"buttons": {
"submit": "Login"
}
},
"buttons":{
"submit": "Login"
}
},
"bs-uninstall":{
"title": "Uninstall",
"description": "Are you sure you want to uninstall BeatSaber {version} ? You will have to download it again to play it.",
"buttons":{
"submit": "Uninstall"
}
},
"install-folder":{
"title": "Installation folder",
"description": "Changing the default installation folder will result in moving all installed data to the new folder.",
"buttons":{
"submit": "Choose folder"
}
},
"edit-version":{
"title": "Edit the version",
"buttons":{
"submit": "Edit"
}
},
"clone-version":{
"title": "Clone version",
"description": "Cloning the version allows you to separate additional BeatSaber content between two versions.",
"buttons":{
"submit": "Clone"
}
},
"uninstall-mod":{
},
"steam-login": {
"title": "Steam Login",
"inputs": {
"username": {
"label": "Username",
"placeholder": "Enter your username"
},
"password": {
"label": "Password",
"placeholder": "Enter your password"
},
"stay": {
"label": "Stay connected"
}
},
"buttons": {
"submit": "Login"
}
},
"bs-uninstall": {
"title": "Uninstall",
"description": "Are you sure you want to uninstall BeatSaber {version} ? You will have to download it again to play it.",
"buttons": {
"submit": "Uninstall"
}
},
"install-folder": {
"title": "Installation folder",
"description": "Changing the default installation folder will result in moving all installed data to the new folder.",
"buttons": {
"submit": "Choose folder"
}
},
"edit-version": {
"title": "Edit the version",
"buttons": {
"submit": "Edit"
}
},
"clone-version": {
"title": "Clone version",
"description": "Cloning the version allows you to separate additional BeatSaber content between two versions.",
"inputs": {
"name": {
"label": "Name",
"placeholder": "Version name"
},
"color": {
"label": "Color"
}
},
"buttons": {
"submit": "Clone"
}
},
"uninstall-mod": {
"title": "Uninstall",
"description": "Are you sure you want to uninstall {mod}? It could cause other installed mods to malfunction.",
"description-bsipa": "Are you sure you want to uninstall BSIPA? After that all installed mods will not work anymore."
},
"uninstall-all-mods":{
"uninstall-all-mods": {
"title": "Uninstalling mods",
"description": "Are you sure you want to uninstall all the mods from version {version}? This operation cannot be undone."
},
"maps-actions": {
"delete-maps": {
"title": {
"single": "Delete the map?",
"multiple": "Delete maps ?"
},
"desc": {
"single": "Are you sure you want to delete the map {name}?",
"multiple": "Are you sure you want to delete all {nb} maps?"
},
"info": {
"desc": {
"single": "This map is part of the shared maps",
"multiple": "These maps are part of the shared maps"
},
"title": {
"single": "This map will also be removed from versions using shared maps",
"multiple": "These maps will also be removed from versions using shared maps"
}
}
},
"link-maps": {
"title": "Link maps",
"desc": "Linking maps allows to share maps between all versions. Once linked, this version will benefit from the shared maps",
"info": "Adding and deleting maps will also be shared",
"keep-maps": {
"label": "Keep maps",
"title": "Keep maps will move the maps of the current version to the shared maps folder. Otherwise they will be lost"
},
"valid-btn": "Link maps"
},
"unlink-maps": {
"title": "Unlink maps",
"desc": "Please note that unlinking maps will not allow the use of shared maps for this version.",
"keep-maps": {
"label": "Keep maps",
"title": "Keeping the maps will create a copy of the shared maps for the current version. Otherwise, no map will be kept for this version."
},
"valid-btn": "Unlink maps"
}
},
"download-maps": {
"search-btn": "Search",
"loading-maps": "Loading maps..."
},
"mods-disclaimer":{
"title": "disclaimer",
"p-1": "By choosing to use mods, you understand that:",
"li-1": "You may experience problems that don't exist in the vanilla game. 99.9% of bugs, crashes, and lag are due to mods.",
"li-2": "Mods are subject to being broken by updates and that's normal - be patient and respectful when this happens, as modders are volunteers with real lives.",
"li-3": "Beat Games aren't purposefully trying to break mods. They wish to work on the codebase and sometimes this breaks mods, but they are not out to kill mods.",
"p-2": "Do not attack the game developers for issues related to mods, and vice versa - modders and game developers are two separate groups. Just don't be a jerk ok."
}
},
"auto-update":{
"checking": "Checking for updates",
"downloading": "Downloading updates"
},
"dateformat":{
},
"maps": {
"map-filter-panel": {
"duration": "Duration",
"tags": "tags",
"specificities": "general",
"requirements": "requirements"
},
"map-types": {
"accuracy": "accuracy",
"balanced": "balanced",
"challenge": "challenge",
"dancestyle": "dance",
"fitness": "fitness",
"speed": "speed",
"tech": "tech"
},
"map-styles": {
"dance": "dance",
"swing": "swing",
"nightcore": "nightcore",
"folk": "folk",
"family": "family",
"ambient": "ambient",
"funk": "funk",
"jazz": "jazz",
"soul": "soul",
"speedcore": "speedcore",
"punk": "punk",
"rb": "r&b",
"holiday": "holiday",
"vocaloid": "vocaloid",
"jrock": "j-rock",
"trance": "trance",
"drumbass": "drum & bass",
"comedy": "comedy",
"instrumental": "instrumental",
"hardcore": "hardcore",
"kpop": "k-pop",
"indie": "indie",
"techno": "techno",
"house": "house",
"game": "video game",
"film": "film",
"alt": "alternative",
"dubstep": "dubstep",
"metal": "metal",
"anime": "anime",
"hiphop": "hiphop",
"jpop": "j-pop",
"rock": "rock",
"pop": "pop",
"electronic": "electronic",
"classical-orchestral": "Classical & Orchestral"
},
"map-specificities": {
"automapper": "AI",
"ranked": "ranked",
"curated": "curated",
"verified": "verified",
"fullSpread": "full spread"
},
"difficulties": {
"Easy": "easy",
"Normal": "normal",
"Hard": "hard",
"Expert": "expert",
"ExpertPlus": "expert+"
},
"map-item": {
"by": "By {songAutor}",
"mapped-by": "mapped by"
}
},
"beat-saver": {
"maps-sorts": {
"Latest": "Latest",
"Relevance": "Relevance",
"Rating": "Rating",
"Curated": "Curated"
}
},
"auto-update": {
"checking": "Checking for updates",
"downloading": "Downloading updates"
},
"dateformat": {
"dayNames": [
"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat",
"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"
"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"
"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"
"a",
"p",
"am",
"pm",
"A",
"P",
"AM",
"PM"
]
}
}
}
+598 -306
View File
@@ -1,361 +1,653 @@
{
"misc":{
"download": "Descargar",
"verify": "Verificar",
"launch": "Lanzar",
"mods": "Mods",
"maps": "Maps",
"cancel": "Cancelar"
},
"pages":{
"version-viewer":{
"launch-mods":{
"oculus": "Oculus Mod",
"desktop": "Desktop Mod",
"debug": "Debug Mod"
},
"mods":{
"loading-mods": "Cargando mods...",
"buttons":{
"more-infos": "Más información",
"install-or-update": "Instalar o actualizar"
"misc": {
"download": "Descargar",
"verify": "Verificar",
"launch": "Lanzar",
"mods": "Mods",
"maps": "Mapas",
"playlists": "Playlists",
"models": "Modelos",
"cancel": "Cancelar",
"delete": "Borrar",
"accept": "Aceptar",
"refuse": "Rechazar"
},
"pages": {
"version-viewer": {
"launch-mods": {
"oculus": "Oculus Mod",
"desktop": "Desktop Mod",
"debug": "Debug Mod"
},
"mods-grid":{
"header-bar":{
"name": "Nombre",
"installed": "Instalado",
"latest": "Último",
"description":"Descripción",
"dropdown":{
"uninstall-all":"Desinstalar todos"
"maps": {
"search-bar": {
"search-placeholder": "Buscar en un mapa",
"filters-btn": "Filtros",
"dropdown": {
"export-maps": "Exportar mapas",
"delete-maps": "Borrar mapas"
}
},
"tabs": {
"maps": {
"actions": {
"add-maps": {
"text": "Añadir"
},
"link-maps": {
"tooltips": {
"link": "Vincular los mapas",
"unlink": "Desatar los mapas"
}
}
},
"empty-maps":{
"text":"No hay mapas",
"button":"Descargar mapas"
}
}
}
}
},
"dropdown":{
"open-folder": "Abrir la carpeta",
"verify-files": "Verificar los archivos",
"clone": "Clonar",
"edit": "Editar",
"uninstall": "Desinstalar"
}
},
"available-versions":{
"title": "Seleccione una versión",
"steam-release":"Publicación Página"
},
"settings":{
"steam":{
"title":"Steam",
"description":"Si sales de Steam, tendrás que volver a entrar para descargar una nueva versión de Beat Saber.",
"logout":"Desconectar"
},
"appearance":{
"title":"Apariencia",
"description":"Elija los dos colores principales de BSManager.",
"reset":"Reiniciar",
"sub-title":"Tema",
"themes":{
"dark":"Oscuro",
"light": "Claro",
"os": "Sincronizar con ordenador"
}
},
"installation-folder":{
"title":"Carpeta de instalación",
"description":"Cambia la carpeta por defecto para las versiones de Beat Saber y otras características próximas.",
"choose-folder":"Elija la carpeta"
},
"language":{
"title":"Idioma",
"description":"Selecciona un idioma.",
"languages":{
"en-EN": "English, UK",
"en-US": "English, US",
"fr-FR": "Français",
"es-ES": "Español",
"translated":{
"en-EN": "Inglés, Reino Unido",
"en-US": "Inglés, Estados Unidos",
"fr-FR": "Francés",
"es-ES": "Español"
}
}
},
"patreon":{
"title":"Apoyo BSManager 💖",
"description": "Apoye el proyecto y ayúdenos a proporcionar mejoras continuas a BSManager.",
"buttons":{
"support": "Apoyo BSManager",
"supporters":"Apoyos 👀"
},
"view":{
"no-supporters": "Todavía no hay apoyos",
"sponsors":"Patrocinadores",
"supporters":"Apoyos"
"mods": {
"loading-mods": "Cargando mods...",
"buttons": {
"more-infos": "Más información",
"install-or-update": "Instalar o actualizar"
},
"mods-grid": {
"header-bar": {
"name": "Nombre",
"installed": "Instalado",
"latest": "Último",
"description": "Descripción",
"dropdown": {
"uninstall-all": "Desinstalar todos"
}
}
}
},
"dropdown": {
"open-folder": "Abrir la carpeta",
"verify-files": "Verificar los archivos",
"clone": "Clonar",
"edit": "Editar",
"uninstall": "Desinstalar"
}
},
"contribution":{
"description":"¡Sugiera nuevas características o reporte un error para ayudar a mejorar BSManager!",
"buttons":{
"request-features":"Proponer una funcionalidad",
"report-bug":"Informar de un error",
"open-logs": "Abrir los registros"
"available-versions": {
"title": "Seleccione una versión",
"steam-release": "Publicación Página"
},
"settings": {
"steam": {
"title": "Steam",
"description": "Si sales de Steam, tendrás que volver a entrar para descargar una nueva versión de Beat Saber.",
"logout": "Desconectar"
},
"appearance": {
"title": "Apariencia",
"description": "Elija los dos colores principales de BSManager.",
"reset": "Reiniciar",
"sub-title": "Tema",
"themes": {
"dark": "Oscuro",
"light": "Claro",
"os": "Sincronizar con ordenador"
}
},
"installation-folder": {
"title": "Carpeta de instalación",
"description": "Cambia la carpeta por defecto para las versiones de Beat Saber y otras características próximas.",
"choose-folder": "Elija la carpeta"
},
"additional-content": {
"title": "Contenido adicional",
"description": "¡El contenido adicional te permite personalizar BeatSaber!",
"deep-links": {
"sub-title": "Instalaciones OneClick"
}
},
"language": {
"title": "Idioma",
"description": "Selecciona un idioma.",
"languages": {
"en-EN": "English, UK",
"en-US": "English, US",
"fr-FR": "Français",
"es-ES": "Español",
"translated": {
"en-EN": "Inglés, Reino Unido",
"en-US": "Inglés, Estados Unidos",
"fr-FR": "Francés",
"es-ES": "Español"
}
}
},
"patreon": {
"title": "Apoyo BSManager 💖",
"description": "Apoye el proyecto y ayúdenos a proporcionar mejoras continuas a BSManager.",
"buttons": {
"support": "Apoyo BSManager",
"supporters": "Apoyos 👀"
},
"view": {
"no-supporters": "Todavía no hay apoyos",
"sponsors": "Patrocinadores",
"supporters": "Apoyos"
}
},
"discord": {
"description": "¡Únete a la comunidad de BSManager uniéndote a nuestro Discord!"
},
"contribution": {
"description": "¡Sugiera nuevas características o reporte un error para ayudar a mejorar BSManager!",
"buttons": {
"request-features": "Proponer una funcionalidad",
"report-bug": "Informar de un error",
"open-logs": "Abrir los registros"
}
}
}
}
},
"notifications":{
"types":{
"error": "🚨 Error",
"warning": "⚠️ Advertencia",
"success": "🎉 Éxito"
},
"common":{
"msg":{
},
"notifications": {
"types": {
"error": "🚨 Error",
"warning": "⚠️ Advertencia",
"success": "🎉 Éxito"
},
"common": {
"msg": {
"error-occurred": "Se ha producido un error"
}
},
"shared":{
"errors":{
"titles":{
"operation-running": "Operación en curso"
"shared": {
"errors": {
"titles": {
"operation-running": "Operación en curso"
},
"msg": {
"operation-running": "Espere a que termine la operación actual y vuelva a empezar."
}
}
},
"bs-download": {
"success": {
"titles": {
"download-success": "Descargar completo",
"verification-finished": "Verificación completa"
}
},
"msg":{
"operation-running": "Espere a que termine la operación actual y vuelva a empezar."
}
}
},
"bs-download":{
"success":{
"titles":{
"download-success": "Descargar completo",
"verification-finished":"Verificación completa"
}
},
"warnings":{
"msg":{
"ManifestChecksum": "El manifiesto descargado anteriormente no coincide con el nuevo 🤔",
"ConnectionTimeout": "Su conexión a Internet parece inestable 🥶",
"ConnectionLost": "Se ha perdido la conexión, inténtelo de nuevo...",
"ConnectionError": "No se puede conectar a Steam, reintentar...",
"Unknown": "Algo extraño ha ocurrido 🤔 Tu conexión es probablemente inestable."
}
},
"errors":{
"msg":{
"Password": "La contraseña es inválida.",
"InvalidCredentials": "Su correo electrónico o contraseña no son válidos 😴",
"InvalidPassword": "Su contraseña no es válida 😕",
"NoManifest": "No se ha encontrado el manifiesto",
"DirectoryCreate": "No se pueden instalar las carpetas necesarias.",
"NotAvailableApp": "¿Intentas descargar BeatSaber cuando no lo tienes? 🤣",
"DepotNotFound": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕",
"NotCompleted": "La descarga no pudo completarse ¯\\_(ツ)_/¯",
"InvalidManifest": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕",
"NoValidKey": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕",
"NoManifestCode": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕",
"401": "Parece que Steam no quiere dejarnos descargar BeatSaber 😢",
"404": "No se puede contactar con los servidores de Steam",
"Unknown": "Se ha producido un error desconocido ¯\\_(ツ)_/¯",
"NoServer": "No se puede contactar con los servidores de Steam.",
"NotAllowed": "Aparentemente no tienes permiso para descargar BeatSaber 🥱",
"ConnectionTimeout": "No se puede conectar a Steam 😕",
"SteamLib": "Si tienes este error, reporta el bug en GitHub con los logs por favor.",
"ConnectionError": "No se puede conectar a Steam después de 10 intentos 🤯",
"LicenceError": "No se puede obtener la lista de licencias.",
"verification-failed": "Verificación fallida, inténtelo más tarde.",
"RateLimitExceeded": "Lo has intentado demasiadas veces, espera un poco y vuelve a intentarlo más tarde.",
"AlreadyDownloading": "La descarga ya está en curso",
"no-internet": "Sin conexión a Internet."
}
}
},
"settings":{
"move-folder":{
"success":{
"titles":{
"transfer-finished":"Transferencia completa"
}
"warnings": {
"msg": {
"ManifestChecksum": "El manifiesto descargado anteriormente no coincide con el nuevo 🤔",
"ConnectionTimeout": "Su conexión a Internet parece inestable 🥶",
"ConnectionLost": "Se ha perdido la conexión, inténtelo de nuevo...",
"ConnectionError": "No se puede conectar a Steam, reintentar...",
"Unknown": "Algo extraño ha ocurrido 🤔 Tu conexión es probablemente inestable."
}
},
"errors":{
"titles":{
"transfer-failed":"Transferencia fallida 😕"
}
}
},
"steam":{
"success":{
"titles":{
"logout": "Desconectado de Steam."
}
}
}
},
"bs-launch":{
"success":{
"titles":{
"launching": "Lanzamiento...🚀"
}
},
"errors":{
"titles":{
"UNABLE_TO_LAUNCH": "No se puede lanzar",
"STEAM_NOT_RUNNING": "Steam no funciona",
"BS_ALREADY_RUNNING": "BeatSaber ya está en marcha",
"EXE_NOT_FINDED": "Archivos perdidos",
"EXIT": "Parada abrupta"
},
"msg":{
"STEAM_NOT_RUNNING": "Para iniciar BeatSaber es necesario que Steam esté en funcionamiento.",
"BS_ALREADY_RUNNING": "Cierra BeatSaber antes de volver a lanzarlo.",
"EXE_NOT_FINDED": "Parece que faltan algunos archivos, intente verificar los archivos.",
"EXIT": "BeatSaber se detuvo abruptamente, trata de revisar los archivos."
}
}
},
"custom-version":{
"errors":{
"titles":{
"CantEditSteam": "Imposible editar",
"CantRename": "El renombramiento es imposible",
"VersionAlreadExist": "Esta versión ya existe",
"CantClone": "Clonación imposible"
},
"msg":{
"CantEditSteam": "No puedes editar la versión de Steam. Sin embargo, puedes clonarla."
}
},
"success":{
"titles":{
"CloningFinished": "Clonación completa 🎉"
}
}
},
"mods":{
"install-mods":{
"errors": {
"titles":{
"dotnet-required": ".NET 6 Requerido"
},
"msg": {
"401": "Parece que Steam no quiere dejarnos descargar BeatSaber 😢",
"404": "No se puede contactar con los servidores de Steam",
"Password": "La contraseña es inválida.",
"InvalidCredentials": "Su correo electrónico o contraseña no son válidos 😴",
"InvalidPassword": "Su contraseña no es válida 😕",
"NoManifest": "No se ha encontrado el manifiesto",
"DirectoryCreate": "No se pueden instalar las carpetas necesarias.",
"NotAvailableApp": "¿Intentas descargar BeatSaber cuando no lo tienes? 🤣",
"DepotNotFound": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕",
"NotCompleted": "La descarga no pudo completarse ¯\\_(ツ)_/¯",
"InvalidManifest": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕",
"NoValidKey": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕",
"NoManifestCode": "No se puede descargar BeatSaber 😥 inténtalo más tarde 😕",
"Unknown": "Se ha producido un error desconocido ¯\\_(ツ)_/¯",
"NoServer": "No se puede contactar con los servidores de Steam.",
"NotAllowed": "Aparentemente no tienes permiso para descargar BeatSaber 🥱",
"ConnectionTimeout": "No se puede conectar a Steam 😕",
"SteamLib": "Si tienes este error, reporta el bug en GitHub con los logs por favor.",
"ConnectionError": "No se puede conectar a Steam después de 10 intentos 🤯",
"LicenceError": "No se puede obtener la lista de licencias.",
"verification-failed": "Verificación fallida, inténtelo más tarde.",
"RateLimitExceeded": "Lo has intentado demasiadas veces, espera un poco y vuelve a intentarlo más tarde.",
"AlreadyDownloading": "La descarga ya está en curso",
"no-internet": "Sin conexión a Internet.",
"dotnet-required": "Se debe instalar el tiempo de ejecución de .NET 6 para descargar una versión de BeatSaber. Descárguelo haciendo clic en el botón de abajo."
},
"actions":{
"download-dotnet": "Descargar .NET 6"
}
}
},
"settings": {
"move-folder": {
"success": {
"titles": {
"transfer-finished": "Transferencia completa"
}
},
"errors": {
"titles": {
"transfer-failed": "Transferencia fallida 😕"
}
}
},
"steam": {
"success": {
"titles": {
"logout": "Desconectado de Steam."
}
}
},
"additional-content":{
"deep-link": {
"activation":{
"success":{
"title":"¡OneClick activado!",
"description":"Las instalaciones OneClick han sido activadas."
},
"error":{
"description":"No se pueden activar las instalaciones OneClick."
}
},
"deactivation":{
"success":{
"title":"¡OneClick desactivado!",
"description":"Las instalaciones OneClick han sido desactivadas."
},
"error":{
"description":"Ocurrió un error desconocido."
}
},
"check-all-enabled":{
"title": "OneClick deshabilitado",
"description": "Una o más instalaciones de OneClick están deshabilitadas. Vaya a la configuración para habilitarlas.",
"actions":{
"settings": "Ajustes",
"not-remind": "No volver a recordarme"
}
}
}
}
},
"bs-launch": {
"success": {
"titles": {
"launching": "Lanzamiento...🚀"
}
},
"errors": {
"titles": {
"UNABLE_TO_LAUNCH": "No se puede lanzar",
"STEAM_NOT_RUNNING": "Steam no funciona",
"OCULUS_NOT_RUNNING": "Oculus no funciona",
"BS_ALREADY_RUNNING": "BeatSaber ya está en marcha",
"EXE_NOT_FINDED": "Archivos perdidos",
"EXIT": "Parada abrupta"
},
"msg": {
"STEAM_NOT_RUNNING": "Para iniciar BeatSaber es necesario que Steam esté en funcionamiento.",
"OCULUS_NOT_RUNNING": "Oculus debe estar funcionando para lanzar BeatSaber.",
"BS_ALREADY_RUNNING": "Cierra BeatSaber antes de volver a lanzarlo.",
"EXE_NOT_FINDED": "Parece que faltan algunos archivos, intente verificar los archivos.",
"EXIT": "BeatSaber se detuvo abruptamente, trata de revisar los archivos."
},
"actions":{
"STEAM_NOT_RUNNING": "Iniciar Steam"
}
}
},
"steam":{
"steam-launching":{
"title": "¡Steam se está iniciando!",
"description": "Steam puede tardar un poco en iniciarse dependiendo de tu configuración."
}
},
"custom-version": {
"errors": {
"titles": {
"CantEditSteam": "Imposible editar",
"CantRename": "El renombramiento es imposible",
"VersionAlreadExist": "Esta versión ya existe",
"CantClone": "Clonación imposible"
},
"msg": {
"CantEditSteam": "No puedes editar la versión de Steam. Sin embargo, puedes clonarla."
}
},
"success": {
"titles": {
"CloningFinished": "Clonación completa 🎉"
}
}
},
"mods": {
"install-mods": {
"titles": {
"success": "Mods instalados 🎉",
"warning": "Mods instalados 🤔"
},
"msg":{
"msg": {
"success": "Todos los mods han sido instalados.",
"warning": "No se han podido instalar uno o más mods.",
"errors":{
"no-mods": "No hay que instalar mods.",
"errors": {
"no-mods": "No hay que instalar mods.",
"cannot-install-bsipa": "La instalación de BSIPA falló 😨"
}
}
},
"uninstall-mod":{
"titles":{
"uninstall-mod": {
"titles": {
"success": "Mod desinstalado 🎉"
},
"msg":{
"errors":{
"msg": {
"errors": {
"no-mods": "Este mod no está instalado 😑"
}
}
},
"uninstall-all-mods":{
"titles":{
"uninstall-all-mods": {
"titles": {
"success": "Mods desinstalados 🎉"
},
"msg":{
"msg": {
"success": "Todos los mods han sido desinstalados.",
"errors": {
"no-mods": "No se instala ningún mod en esta versión 😑"
}
}
}
},
"maps":{
"one-click-install":{
"success":"Instalación del mapa completada",
"error":"Se produjo un error durante la instalación del mapa"
}
},
"playlists":{
"one-click-install":{
"success":"Instalación de la lista de reproducción completada",
"error":"Se produjo un error durante la instalación de la lista de reproducción"
}
},
"models":{
"one-click-install":{
"success":"Instalación del modelo completada",
"error":"Se produjo un error durante la instalación del modelo"
}
}
},
"modals":{
"guard":{
"title": "Steam Guard",
"inputs":{
"guard-code":{
"label": "Guard Código",
"placeholder": "Introduzca su código Guard"
}
},
"buttons":{
"submit": "Conéctate"
}
},
"steam-login":{
"title": "Steam Conectar",
"inputs":{
"username":{
"label": "Nombre de usuario",
"placeholder": "Introduzca su nombre de usuario"
},
"modals": {
"guard": {
"title": "Steam Guard",
"inputs": {
"guard-code": {
"label": "Guard Código",
"placeholder": "Introduzca su código Guard"
}
},
"password":{
"label": "Contraseña",
"placeholder": "Introduzca su contraseña"
},
"stay":{
"label": "Manténgase conectado"
"buttons": {
"submit": "Conéctate"
}
},
"buttons":{
"submit": "Inicio de sesión"
}
},
"bs-uninstall":{
"title": "Desinstalar",
"description": "¿Estás seguro de que quieres desinstalar BeatSaber {version}? Tendrás que descargarlo de nuevo para poder jugar.",
"buttons":{
"submit": "Desinstalar"
}
},
"install-folder":{
"title": "Instalación Carpeta",
"description": "Al cambiar la carpeta de instalación por defecto, se moverán todos los datos instalados a la nueva carpeta.",
"buttons":{
"submit": "Elija la carpeta"
}
},
"edit-version":{
"title": "Editar la versión",
"buttons":{
"submit": "Editar"
}
},
"clone-version":{
"title": "Versión clonada",
"description": "La clonación de la versión le permite separar el contenido adicional de BeatSaber entre dos versiones.",
"buttons":{
"submit": "Clonar"
}
},
"uninstall-mod":{
},
"steam-login": {
"title": "Steam Conectar",
"inputs": {
"username": {
"label": "Nombre de usuario",
"placeholder": "Introduzca su nombre de usuario"
},
"password": {
"label": "Contraseña",
"placeholder": "Introduzca su contraseña"
},
"stay": {
"label": "Manténgase conectado"
}
},
"buttons": {
"submit": "Inicio de sesión"
}
},
"bs-uninstall": {
"title": "Desinstalar",
"description": "¿Estás seguro de que quieres desinstalar BeatSaber {version}? Tendrás que descargarlo de nuevo para poder jugar.",
"buttons": {
"submit": "Desinstalar"
}
},
"install-folder": {
"title": "Instalación Carpeta",
"description": "Al cambiar la carpeta de instalación por defecto, se moverán todos los datos instalados a la nueva carpeta.",
"buttons": {
"submit": "Elija la carpeta"
}
},
"edit-version": {
"title": "Editar la versión",
"buttons": {
"submit": "Editar"
}
},
"clone-version": {
"title": "Versión clonada",
"description": "La clonación de la versión le permite separar el contenido adicional de BeatSaber entre dos versiones.",
"inputs": {
"name": {
"label": "Nombre",
"placeholder": "Nombre de la versión"
},
"color": {
"label": "Color"
}
},
"buttons": {
"submit": "Clonar"
}
},
"uninstall-mod": {
"title": "Desinstalar",
"description": "¿Estás seguro de que quieres desinstalar {mod}? Podría provocar el mal funcionamiento de otros mods instalados.",
"description-bsipa": "¿Estás seguro de que quieres desinstalar BSIPA? Después de eso, todos los mods instalados dejarán de funcionar."
},
"uninstall-all-mods":{
"uninstall-all-mods": {
"title": "Desinstalación de mods",
"description": "¿Estás seguro de que quieres desinstalar todos los mods de la versión {version}? Esta operación no se puede deshacer."
},
"maps-actions": {
"delete-maps": {
"title": {
"single": "¿Borrar el mapa?",
"multiple": "¿Eliminar mapas?"
},
"desc": {
"single": "¿Estás seguro de que quieres eliminar el mapa {name}?",
"multiple": "¿Seguro que quieres eliminar los {nb} mapas?"
},
"info": {
"desc": {
"single": "Este mapa es parte de los mapas compartidos",
"multiple": "Estos mapas son parte de los mapas compartidos"
},
"title": {
"single": "Este mapa también se eliminará de las versiones que usan mapas compartidos.",
"multiple": "Estos mapas también se eliminarán de las versiones que usan mapas compartidos."
}
}
},
"link-maps": {
"title": "Vincular mapas",
"desc": "La vinculación de mapas permite compartir mapas entre todas las versiones. Una vez vinculada, esta versión se beneficiará de los mapas compartidos",
"info": "También se compartirá la adición y eliminación de mapas.",
"keep-maps": {
"label": "Mantener mapas",
"title": "Mantener mapas moverá los mapas de la versión actual a la carpeta de mapas compartidos. De lo contrario, se perderán"
},
"valid-btn": "Vincular mapas"
},
"unlink-maps": {
"title": "Desvincular mapas",
"desc": "Tenga en cuenta que desvincular mapas no permitirá el uso de mapas compartidos para esta versión.",
"keep-maps": {
"label": "Mantener mapas",
"title": "Mantener los mapas creará una copia de los mapas compartidos para la versión actual. De lo contrario, no se mantendrá ningún mapa para esta versión."
},
"valid-btn": "Desvincular mapas"
}
},
"download-maps": {
"search-btn": "buscar",
"loading-maps": "Cargando mapas..."
},
"mods-disclaimer":{
"title": "descargo de responsabilidad",
"p-1": "Al elegir usar mods, entiendes y aceptas que:",
"li-1": "Es posible que encuentres problemas que no existen en el juego base. El 99,9% de los errores, bloqueos y ralentizaciones son debidos a los mods.",
"li-2": "Es probable que los mods dejen de funcionar debido a las actualizaciones y eso es normal. Sé paciente y respetuoso cuando suceda esto, ya que los modders son voluntarios y tienen una vida.",
"li-3": "Beat Games no está intencionalmente tratando de romper los mods. Quieren mejorar el juego base y a veces esto rompe los mods, pero no es su objetivo matar a los mods.",
"p-2": "No ataques a los desarrolladores por problemas relacionados con los mods y viceversa. Los modders y los desarrolladores son dos grupos distintos. No seas tonto, ¿de acuerdo?"
}
},
"auto-update":{
"checking": "Comprobar las actualizaciones",
"downloading": "Descargar actualizaciones"
},
"dateformat":{
},
"maps": {
"map-filter-panel": {
"duration": "Duración",
"tags": "tags",
"specificities": "general",
"requirements": "requisitos"
},
"map-types": {
"accuracy": "precisión",
"balanced": "equilibrado",
"challenge": "desafío",
"dancestyle": "baile",
"fitness": "fitness",
"speed": "speed",
"tech": "tec"
},
"map-styles": {
"dance": "baile",
"swing": "swing",
"nightcore": "nightcore",
"folk": "folk",
"family": "familia",
"ambient": "ambiente",
"funk": "funk",
"jazz": "jazz",
"soul": "soul",
"speedcore": "speedcore",
"punk": "punk",
"rb": "r&b",
"holiday": "vacaciones",
"vocaloid": "vocaloid",
"jrock": "j-rock",
"trance": "trance",
"drumbass": "drum & bass",
"comedy": "comedia",
"instrumental": "instrumental",
"hardcore": "hardcore",
"kpop": "k-pop",
"indie": "indie",
"techno": "tecno",
"house": "house",
"game": "videojuego",
"film": "film",
"alt": "alternativa",
"dubstep": "dubstep",
"metal": "metal",
"anime": "anime",
"hiphop": "hiphop",
"jpop": "j-pop",
"rock": "rock",
"pop": "pop",
"electronic": "electrónico",
"classical-orchestral": "Clásico y orquestal"
},
"map-specificities": {
"automapper": "IA",
"ranked": "rankado",
"curated": "recomendado",
"verified": "verificado",
"fullSpread": "panel completo"
},
"difficulties": {
"Easy": "fácil",
"Normal": "normal",
"Hard": "difícil",
"Expert": "experto",
"ExpertPlus": "experto+"
},
"map-item": {
"by": "Por {songAutor}",
"mapped-by": "mapeado por"
}
},
"beat-saver": {
"maps-sorts": {
"Latest": "Última",
"Relevance": "Relevancia",
"Rating": "Notas",
"Curated": "Recomendado"
}
},
"auto-update": {
"checking": "Comprobar las actualizaciones",
"downloading": "Descargar actualizaciones"
},
"dateformat": {
"dayNames": [
"Dom", "Lun", "Mar", "Mié", "Jue", "Vie", "Sáb",
"Domingo", "Lunes", "Martes", "Miércoles", "Jueves", "Viernes", "Sábado"
"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"
"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"
"a",
"p",
"am",
"pm",
"A",
"P",
"AM",
"PM"
]
}
}
}
+598 -305
View File
@@ -1,360 +1,653 @@
{
"misc":{
"download": "Télécharger",
"verify": "Vérifier",
"launch": "Lancer",
"mods": "Mods",
"maps": "Maps",
"cancel": "Annuler"
},
"pages":{
"version-viewer":{
"launch-mods":{
"oculus": "Mode Oculus",
"desktop": "Mode Bureau",
"debug": "Mode Debug"
},
"mods":{
"loading-mods": "Chargement des mods...",
"buttons":{
"more-infos": "Plus d'infos",
"install-or-update": "Installer ou mettre à jour"
"misc": {
"download": "Télécharger",
"verify": "Vérifier",
"launch": "Lancer",
"mods": "Mods",
"maps": "Maps",
"playlists": "Playlists",
"models": "Modèles",
"cancel": "Annuler",
"delete": "Supprimer",
"accept": "Accepter",
"refuse": "Refuser"
},
"pages": {
"version-viewer": {
"launch-mods": {
"oculus": "Mode Oculus",
"desktop": "Mode Bureau",
"debug": "Mode Debug"
},
"mods-grid":{
"header-bar":{
"name": "Nom",
"installed": "Installé",
"latest": "Récent",
"description":"Description",
"dropdown":{
"uninstall-all":"Tout désinstaller"
"maps": {
"search-bar": {
"search-placeholder": "Rechercher une map",
"filters-btn": "Filtres",
"dropdown": {
"export-maps": "Exporter les maps",
"delete-maps": "Supprimer les maps"
}
},
"tabs": {
"maps": {
"actions": {
"add-maps": {
"text": "Ajouter"
},
"link-maps": {
"tooltips": {
"link": "Lier les maps",
"unlink": "Délier les maps"
}
}
},
"empty-maps":{
"text":"Aucune map",
"button":"Télécharger des maps"
}
}
}
}
},
"dropdown":{
"open-folder": "Ouvrir le dossier",
"verify-files": "Vérifier les fichiers",
"clone": "Cloner",
"edit": "Editer",
"uninstall": "Désinstaller"
}
},
"available-versions":{
"title": "Choisi une version",
"steam-release":"Notes de version"
},
"settings":{
"steam":{
"title":"Steam",
"description":"Si tu te déconnectes de Steam, tu devras te reconnecter pour télécharger une nouvelle version de Beat Saber.",
"logout":"Déconnexion"
},
"appearance":{
"title":"Apparence",
"description":"Choisi les deux couleurs principales de BSManager.",
"reset":"Réinitialiser",
"sub-title":"Thème",
"themes":{
"dark":"Sombre",
"light": "Clair",
"os": "Synchroniser avec l'ordinateur"
}
},
"installation-folder":{
"title":"Dossier d'installation",
"description":"Change le dossier par défaut pour les versions de Beat Saber et d'autres fonctionnalités à venir.",
"choose-folder":"Choisir un dossier"
},
"language":{
"title":"Langage",
"description":"Sélectionne un langage.",
"languages":{
"en-EN": "English, UK",
"en-US": "English, US",
"fr-FR": "Français",
"es-ES": "Español",
"translated":{
"en-EN": "Anglais, Royaume-Uni",
"en-US": "Anglais, États-Unis",
"fr-FR": "Français",
"es-ES": "Espagnol"
}
}
},
"patreon":{
"title":"Soutenir BSManager 💖",
"description": "Soutien le projet et aide-nous à fournir des améliorations continues à BSManager.",
"buttons":{
"support": "Soutenir BSManager",
"supporters":"Supporteurs 👀"
},
"view":{
"no-supporters": "Pas encore de supporteurs",
"sponsors":"Sponsors",
"supporters":"Supporteurs"
"mods": {
"loading-mods": "Chargement des mods...",
"buttons": {
"more-infos": "Plus d'infos",
"install-or-update": "Installer ou mettre à jour"
},
"mods-grid": {
"header-bar": {
"name": "Nom",
"installed": "Installé",
"latest": "Récent",
"description": "Description",
"dropdown": {
"uninstall-all": "Tout désinstaller"
}
}
}
},
"dropdown": {
"open-folder": "Ouvrir le dossier",
"verify-files": "Vérifier les fichiers",
"clone": "Cloner",
"edit": "Editer",
"uninstall": "Désinstaller"
}
},
"contribution":{
"description":"Propose de nouvelles fonctionnalités ou signale un bug pour contribuer à l'amélioration de BSManager !",
"buttons":{
"request-features":"Proposer une fonctionnalité",
"report-bug":"Signaler un bug",
"open-logs": "Ouvrir les logs"
"available-versions": {
"title": "Choisi une version",
"steam-release": "Notes de version"
},
"settings": {
"steam": {
"title": "Steam",
"description": "Si tu te déconnectes de Steam, tu devras te reconnecter pour télécharger une nouvelle version de Beat Saber.",
"logout": "Déconnexion"
},
"appearance": {
"title": "Apparence",
"description": "Choisi les deux couleurs principales de BSManager.",
"reset": "Réinitialiser",
"sub-title": "Thème",
"themes": {
"dark": "Sombre",
"light": "Clair",
"os": "Synchroniser avec l'ordinateur"
}
},
"installation-folder": {
"title": "Dossier d'installation",
"description": "Change le dossier par défaut pour les versions de Beat Saber et d'autres fonctionnalités à venir.",
"choose-folder": "Choisir un dossier"
},
"additional-content": {
"title": "Contenus additionnels",
"description": "Les contenus additionnels te permet de personaliser BeatSaber !",
"deep-links": {
"sub-title": "Installations OneClick"
}
},
"language": {
"title": "Langage",
"description": "Sélectionne un langage.",
"languages": {
"en-EN": "English, UK",
"en-US": "English, US",
"fr-FR": "Français",
"es-ES": "Español",
"translated": {
"en-EN": "Anglais, Royaume-Uni",
"en-US": "Anglais, États-Unis",
"fr-FR": "Français",
"es-ES": "Espagnol"
}
}
},
"patreon": {
"title": "Soutenir BSManager 💖",
"description": "Soutien le projet et aide-nous à fournir des améliorations continues à BSManager.",
"buttons": {
"support": "Soutenir BSManager",
"supporters": "Supporteurs 👀"
},
"view": {
"no-supporters": "Pas encore de supporteurs",
"sponsors": "Sponsors",
"supporters": "Supporteurs"
}
},
"discord":{
"description": "Prends part à la communauté de BSManager en rejoignant notre Discord !"
},
"contribution": {
"description": "Propose de nouvelles fonctionnalités ou signale un bug pour contribuer à l'amélioration de BSManager !",
"buttons": {
"request-features": "Proposer une fonctionnalité",
"report-bug": "Signaler un bug",
"open-logs": "Ouvrir les logs"
}
}
}
}
},
"notifications":{
"types":{
"error": "🚨 Erreur",
"warning": "⚠️ Attention",
"success": "🎉 Succès"
},
"common":{
"msg":{
},
"notifications": {
"types": {
"error": "🚨 Erreur",
"warning": "⚠️ Attention",
"success": "🎉 Succès"
},
"common": {
"msg": {
"error-occurred": "Une erreur c'est produite"
}
},
"shared":{
"errors":{
"titles":{
"operation-running": "Opération en cours"
"shared": {
"errors": {
"titles": {
"operation-running": "Opération en cours"
},
"msg": {
"operation-running": "Attend la fin de l'opération en cours puis recommences."
}
}
},
"bs-download": {
"success": {
"titles": {
"download-success": "Téléchargement terminé",
"verification-finished": "Vérification terminée"
}
},
"msg":{
"operation-running": "Attend la fin de l'opération en cours puis recommences."
}
}
},
"bs-download":{
"success":{
"titles":{
"download-success": "Téléchargement terminé",
"verification-finished":"Vérification terminée"
}
},
"warnings":{
"msg":{
"ManifestChecksum": "Le manifest télécharger précédemment ne correspond pas au nouveau 🤔",
"ConnectionTimeout": "Ta connexion internet semble instable 🥶",
"ConnectionLost": "La connexion a été perdue, retentative...",
"ConnectionError": "Impossible de se connecter à Steam, retentative...",
"Unknown": "Quelque chose d'étrange c'est produit 🤔 Votre connexion est surement instable."
}
},
"errors":{
"msg":{
"Password": "Le mot de passe est invalide.",
"InvalidCredentials": "Ton e-mail ou mot de passe est invalide 😴",
"InvalidPassword": "Ton mot de passe est invalide 😕",
"NoManifest": "Aucun manifest n'a été trouvé.",
"DirectoryCreate": "Impossible d'installer les dossiers nécessaires.",
"NotAvailableApp": "Tu essayes de télécharger BeatSaber alors que tu ne l'as pas ? 🤣",
"DepotNotFound": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕",
"NotCompleted": "Le téléchargement n'a pas pu se terminer ¯\\_(ツ)_/¯",
"InvalidManifest": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕",
"NoValidKey": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕",
"NoManifestCode": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕",
"401": "Steam semble pas vouloir nous laisser télécharger BeatSaber 😢",
"404": "Impossible de contacter les serveurs de Steam.",
"Unknown": "Une erreur inconnue c'est produite ¯\\_(ツ)_/¯",
"NoServer": "Impossible de contacter les serveurs de Steam.",
"NotAllowed": "Apparemment tu n'es pas autorisé à télécharger BeatSaber 🥱",
"ConnectionTimeout": "Impossible de se connecter à Steam 😕",
"SteamLib": "Si tu à cette erreur, report le bug sur GitHub avec les logs stp.",
"ConnectionError": "Impossible de se connecter à Steam après 10 essais 🤯",
"LicenceError": "Impossible d'obtenir la liste des licences.",
"verification-failed": "La vérification a échoué, réessaye plus tard.",
"RateLimitExceeded": "Tu as essayé trop de fois attend un peu et recommence plus tard.",
"AlreadyDownloading": "Un téléchargement est déjà en cours",
"no-internet": "Pas de connexion internet."
}
}
},
"settings":{
"move-folder":{
"success":{
"titles":{
"transfer-finished":"Transfert terminé 👌"
}
"warnings": {
"msg": {
"ManifestChecksum": "Le manifest télécharger précédemment ne correspond pas au nouveau 🤔",
"ConnectionTimeout": "Ta connexion internet semble instable 🥶",
"ConnectionLost": "La connexion a été perdue, retentative...",
"ConnectionError": "Impossible de se connecter à Steam, retentative...",
"Unknown": "Quelque chose d'étrange c'est produit 🤔 Votre connexion est surement instable."
}
},
"errors":{
"titles":{
"transfer-failed":"Le transfert a échoué 😕"
}
}
},
"steam":{
"success":{
"titles":{
"logout": "Déconnecté de Steam."
}
}
}
},
"bs-launch":{
"success":{
"titles":{
"launching": "Lancement...🚀"
}
},
"errors":{
"titles":{
"UNABLE_TO_LAUNCH": "Lancement impossible",
"STEAM_NOT_RUNNING": "Steam n'est pas lancé",
"BS_ALREADY_RUNNING": "BeatSaber est déjà lancé",
"EXE_NOT_FINDED": "Fichiers manquants",
"EXIT": "Arrêt brutal"
},
"msg":{
"STEAM_NOT_RUNNING": "Steam doit être en cours d'exécution pour lancer BeatSaber.",
"BS_ALREADY_RUNNING": "Ferme BeatSaber avant de le lancer à nouveau.",
"EXE_NOT_FINDED": "Quelques fichiers semblent manquants. Essaye de vérifier les fichiers.",
"EXIT": "BeatSaber s'est arrêté brusquement, essaye de vérifier les fichiers."
}
}
},
"custom-version":{
"errors":{
"titles":{
"CantEditSteam": "Edition impossible",
"CantRename": "Renommage impossible",
"VersionAlreadExist": "Cette version existe déjà",
"CantClone": "Clonage impossible"
},
"msg":{
"CantEditSteam": "Tu ne peut pas éditer la version Steam. Tu peut la cloner par contre."
}
},
"success":{
"titles":{
"CloningFinished": "Clonage terminer 🎉"
}
}
},
"mods":{
"install-mods":{
"errors": {
"titles":{
"dotnet-required": ".NET 6 Requis"
},
"msg": {
"401": "Steam semble pas vouloir nous laisser télécharger BeatSaber 😢",
"404": "Impossible de contacter les serveurs de Steam.",
"Password": "Le mot de passe est invalide.",
"InvalidCredentials": "Ton e-mail ou mot de passe est invalide 😴",
"InvalidPassword": "Ton mot de passe est invalide 😕",
"NoManifest": "Aucun manifest n'a été trouvé.",
"DirectoryCreate": "Impossible d'installer les dossiers nécessaires.",
"NotAvailableApp": "Tu essayes de télécharger BeatSaber alors que tu ne l'as pas ? 🤣",
"DepotNotFound": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕",
"NotCompleted": "Le téléchargement n'a pas pu se terminer ¯\\_(ツ)_/¯",
"InvalidManifest": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕",
"NoValidKey": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕",
"NoManifestCode": "Impossible de télécharger BeatSaber 😥 réessaye plus tard 😕",
"Unknown": "Une erreur inconnue c'est produite ¯\\_(ツ)_/¯",
"NoServer": "Impossible de contacter les serveurs de Steam.",
"NotAllowed": "Apparemment tu n'es pas autorisé à télécharger BeatSaber 🥱",
"ConnectionTimeout": "Impossible de se connecter à Steam 😕",
"SteamLib": "Si tu à cette erreur, report le bug sur GitHub avec les logs stp.",
"ConnectionError": "Impossible de se connecter à Steam après 10 essais 🤯",
"LicenceError": "Impossible d'obtenir la liste des licences.",
"verification-failed": "La vérification a échoué, réessaye plus tard.",
"RateLimitExceeded": "Tu as essayé trop de fois attend un peu et recommence plus tard.",
"AlreadyDownloading": "Un téléchargement est déjà en cours",
"no-internet": "Pas de connexion internet.",
"dotnet-required": ".NET 6 Runtime doit être installé pour pouvoir télécharger une version de BeatSaber. Télécharge-le en cliquant sur le bouton ci-dessous."
},
"actions":{
"download-dotnet": "Télécharger .NET 6"
}
}
},
"settings": {
"move-folder": {
"success": {
"titles": {
"transfer-finished": "Transfert terminé 👌"
}
},
"errors": {
"titles": {
"transfer-failed": "Le transfert a échoué 😕"
}
}
},
"steam": {
"success": {
"titles": {
"logout": "Déconnecté de Steam."
}
}
},
"additional-content":{
"deep-link": {
"activation":{
"success":{
"title":"OneClick activé !",
"description":"Les installations OneClick ont été activées."
},
"error":{
"description":"Impossible d'activer les installations OneClick."
}
},
"deactivation":{
"success":{
"title":"OneClick désactivé !",
"description":"Les installations OneClick ont été désactivées."
},
"error":{
"description":"Une erreur inconnue s'est produite."
}
},
"check-all-enabled":{
"title": "OneClick désactivée(s)",
"description": "Une ou plusieurs installations OneClick sont désactivées. Rendez-vous dans les paramètres pour les activer.",
"actions":{
"settings": "Paramètres",
"not-remind": "Ne plus me rappeler"
}
}
}
}
},
"bs-launch": {
"success": {
"titles": {
"launching": "Lancement...🚀"
}
},
"errors": {
"titles": {
"UNABLE_TO_LAUNCH": "Lancement impossible",
"STEAM_NOT_RUNNING": "Steam n'est pas lancé",
"OCULUS_NOT_RUNNING": "Oculus n'est pas lancé",
"BS_ALREADY_RUNNING": "BeatSaber est déjà lancé",
"EXE_NOT_FINDED": "Fichiers manquants",
"EXIT": "Arrêt brutal"
},
"msg": {
"STEAM_NOT_RUNNING": "Steam doit être en cours d'exécution pour lancer BeatSaber.",
"OCULUS_NOT_RUNNING": "Oculus doit être en cours d'exécution pour lancer BeatSaber.",
"BS_ALREADY_RUNNING": "Ferme BeatSaber avant de le lancer à nouveau.",
"EXE_NOT_FINDED": "Quelques fichiers semblent manquants. Essaye de vérifier les fichiers.",
"EXIT": "BeatSaber s'est arrêté brusquement, essaye de vérifier les fichiers."
},
"actions":{
"STEAM_NOT_RUNNING": "Lancer Steam"
}
}
},
"steam":{
"steam-launching":{
"title": "Steam se lance !",
"description": "Steam peut mettre quelque temps à se lancer selon votre configuration."
}
},
"custom-version": {
"errors": {
"titles": {
"CantEditSteam": "Edition impossible",
"CantRename": "Renommage impossible",
"VersionAlreadExist": "Cette version existe déjà",
"CantClone": "Clonage impossible"
},
"msg": {
"CantEditSteam": "Tu ne peut pas éditer la version Steam. Tu peut la cloner par contre."
}
},
"success": {
"titles": {
"CloningFinished": "Clonage terminer 🎉"
}
}
},
"mods": {
"install-mods": {
"titles": {
"success": "Mods installés 🎉",
"warning": "Mods installés 🤔"
},
"msg":{
"msg": {
"success": "Tous les mods on été installés.",
"warning": "Un ou plusieurs mods n'ont pas pu être installés.",
"errors":{
"errors": {
"no-mods": "Aucun mods à installer.",
"cannot-install-bsipa": "L'installation de BSIPA à échoué 😨"
}
}
},
"uninstall-mod":{
"titles":{
"uninstall-mod": {
"titles": {
"success": "Mod désintallé 🎉"
},
"msg":{
"errors":{
"msg": {
"errors": {
"no-mods": "Ce mod n'est pas installé 😑"
}
}
},
"uninstall-all-mods":{
"titles":{
"uninstall-all-mods": {
"titles": {
"success": "Mods désintallés 🎉"
},
"msg":{
"msg": {
"success": "Tous les mods on été désintallés.",
"errors": {
"no-mods": "Aucun mod n'est installé dans dans cette version 😑"
}
}
}
},
"maps":{
"one-click-install":{
"success":"Installation de la map terminée",
"error":"Une erreur s'est produite lors de l'installation de la map"
}
},
"playlists":{
"one-click-install":{
"success":"Installation de la playlist terminée",
"error":"Une erreur s'est produite lors de l'installation de la playlist"
}
},
"models":{
"one-click-install":{
"success":"Installation du modèle terminée",
"error":"Une erreur s'est produite lors de l'installation du modèle"
}
}
},
"modals":{
"guard":{
"title": "Steam Guard",
"inputs":{
"guard-code":{
"label": "Code Guard",
"placeholder": "Entre ton code Guard"
}
},
"buttons":{
"submit": "Se connecter"
}
},
"steam-login":{
"title": "Connexion Steam",
"inputs":{
"username":{
"label": "Nom d'utilisateur",
"placeholder": "Entre ton nom d'utilisateur"
},
"modals": {
"guard": {
"title": "Steam Guard",
"inputs": {
"guard-code": {
"label": "Code Guard",
"placeholder": "Entre ton code Guard"
}
},
"password":{
"label": "Mot de passe",
"placeholder": "Entre ton mot de passe"
},
"stay":{
"label": "Rester connecté"
"buttons": {
"submit": "Se connecter"
}
},
"buttons":{
"submit": "Se connecter"
}
},
"bs-uninstall":{
"title": "Désinstaller",
"description": "Est-tu sûr de vouloir désinstaller BeatSaber {version} ? Tu vas devoir la retélécharger pour y jouer.",
"buttons":{
"submit": "Désinstaller"
}
},
"install-folder":{
"title": "Dossier d'installation",
"description": "Changer le dossier d'installation par défaut, va engendrer le déplacement de toutes les données installer vers le nouveau dossier.",
"buttons":{
"submit": "Choisir un dossier"
}
},
"edit-version":{
"title": "Editer la version",
"buttons":{
"submit": "Editer"
}
},
"clone-version":{
"title": "Cloner la version",
"description": "Cloner la version te permet de séparer les contenus additionnels de BeatSaber entre deux mêmes versions.",
"buttons":{
"submit": "Cloner"
}
},
"uninstall-mod":{
},
"steam-login": {
"title": "Connexion Steam",
"inputs": {
"username": {
"label": "Nom d'utilisateur",
"placeholder": "Entre ton nom d'utilisateur"
},
"password": {
"label": "Mot de passe",
"placeholder": "Entre ton mot de passe"
},
"stay": {
"label": "Rester connecté"
}
},
"buttons": {
"submit": "Se connecter"
}
},
"bs-uninstall": {
"title": "Désinstaller",
"description": "Est-tu sûr de vouloir désinstaller BeatSaber {version} ? Tu vas devoir la retélécharger pour y jouer.",
"buttons": {
"submit": "Désinstaller"
}
},
"install-folder": {
"title": "Dossier d'installation",
"description": "Changer le dossier d'installation par défaut, va engendrer le déplacement de toutes les données installer vers le nouveau dossier.",
"buttons": {
"submit": "Choisir un dossier"
}
},
"edit-version": {
"title": "Editer la version",
"buttons": {
"submit": "Editer"
}
},
"clone-version": {
"title": "Cloner la version",
"description": "Cloner la version te permet de séparer les contenus additionnels de BeatSaber entre deux mêmes versions.",
"inputs": {
"name": {
"label": "Nom",
"placeholder": "Nom de la version"
},
"color": {
"label": "Couleur"
}
},
"buttons": {
"submit": "Cloner"
}
},
"uninstall-mod": {
"title": "Désinstaller",
"description": "Es-tu sûr de vouloir désinstaller {mod} ? Cela pourrait faire dysfonctionner d'autres mods installés.",
"description-bsipa": "Es-tu sûr de vouloir désinstaller BSIPA ? Après cela, tous les mods installés ne fonctionneront plus."
},
"uninstall-all-mods":{
"uninstall-all-mods": {
"title": "Désinstaller les mods",
"description": "Es-tu sûr de vouloir désinstaller tous les mods de la version {version} ? Cette opération ne pourra pas être annulée."
},
"maps-actions": {
"delete-maps": {
"title": {
"single": "Supprimer la map ?",
"multiple": "Supprimer les maps ?"
},
"desc": {
"single": "Est-tu sur de vouloir supprimer la map {name} ?",
"multiple": "Est-tu sur de vouloir supprimer les {nb} maps ?"
},
"info": {
"desc": {
"single": "Cette map fait partie des maps paratagées",
"multiple": "Ces maps font parties des maps partagées"
},
"title": {
"single": "Cette map sera également supprimée des versions utilisant les maps partagées",
"multiple": "Ces maps seront égalements supprimées des versions utilisant les maps partagées"
}
}
},
"link-maps": {
"title": "Lier les maps",
"desc": "La liaison des maps permet de partager les maps entre toute les version. Une fois liée, cette version profitera des maps partagées",
"info": "L'ajout et la suppression de maps sera également partagé",
"keep-maps": {
"label": "Conserver les maps",
"title": "Conserver les maps déplacera les maps de la version actuelle dans le dossier des maps partagées. Dans le cas contraire elles seront perdues"
},
"valid-btn": "Lier les maps"
},
"unlink-maps": {
"title": "Délier les maps",
"desc": "Attention, délier les maps ne permettra plus l'utilisation des maps paratagées pour cette version.",
"keep-maps": {
"label": "Conserver les maps",
"title": "Conserver les maps créera une copie des maps partagées pour la version actuelle. Dans le cas contraire, aucune map ne sera conservée pour cette version."
},
"valid-btn": "Délier les maps"
}
},
"download-maps": {
"search-btn": "Rechercher",
"loading-maps": "Chargement des maps..."
},
"mods-disclaimer":{
"title": "avis de non-responsabilité",
"p-1": "En choisissant d'utiliser des mods, vous comprenez et acceptez que :",
"li-1": "Vous pouvez rencontrer des problèmes qui n'existent pas dans le jeu de base. 99.9% des bugs, plantages et ralentissements sont dus aux mods.",
"li-2": "Les mods sont susceptibles de ne plus fonctionner à cause des mises à jour et c'est normal - soyez patient et respectueux lorsque cela se produit, parce que les moddeurs sont bénévoles et ont une vie.",
"li-3": "Beat Games ne cherche pas volontairement à casser les mods. Ils souhaitent améliorer la base du jeu et parfois cela casse les mods, mais ce n'est pas dans leur objectif de tuer les mods.",
"p-2": "N'attaquez pas les développeurs pour des problèmes relatifs aux mods, et inversement - les moddeurs et les développeurs sont deux groupes distincts. Ne sois pas idiot, d'accord ?"
}
},
"auto-update":{
"checking": "Vérification des mises à jour",
"downloading": "Téléchargement des mises à jour"
},
"dateformat":{
},
"maps": {
"map-filter-panel": {
"duration": "Durée",
"tags": "tags",
"specificities": "général",
"requirements": "requis"
},
"map-types": {
"accuracy": "précision",
"balanced": "équilibrée",
"challenge": "challenge",
"dancestyle": "dance",
"fitness": "fitness",
"speed": "vitesse",
"tech": "tech"
},
"map-styles": {
"dance": "dance",
"swing": "swing",
"nightcore": "nightcore",
"folk": "folk",
"family": "famille",
"ambient": "ambiante",
"funk": "funk",
"jazz": "jazz",
"soul": "soul",
"speedcore": "speedcore",
"punk": "punk",
"rb": "r&b",
"holiday": "vacance",
"vocaloid": "vocaloid",
"jrock": "j-rock",
"trance": "trance",
"drumbass": "drum & bass",
"comedy": "comédie",
"instrumental": "instrumental",
"hardcore": "hardcore",
"kpop": "k-pop",
"indie": "indé",
"techno": "techno",
"house": "house",
"game": "jeu vidéo",
"film": "film",
"alt": "alternative",
"dubstep": "dubstep",
"metal": "metal",
"anime": "anime",
"hiphop": "hiphop",
"jpop": "j-pop",
"rock": "rock",
"pop": "pop",
"electronic": "éléctronique",
"classical-orchestral": "Classique & Orchestral"
},
"map-specificities": {
"automapper": "IA",
"ranked": "classée",
"curated": "recommandée",
"verified": "vérifiée",
"fullSpread": "panel complet"
},
"difficulties": {
"Easy": "facile",
"Normal": "normal",
"Hard": "difficile",
"Expert": "expert",
"ExpertPlus": "expert+"
},
"map-item": {
"by": "Par {songAutor}",
"mapped-by": "mappée par"
}
},
"beat-saver": {
"maps-sorts": {
"Latest": "Dernière",
"Relevance": "Pertinence",
"Rating": "Notes",
"Curated": "Recommandée"
}
},
"auto-update": {
"checking": "Vérification des mises à jour",
"downloading": "Téléchargement des mises à jour"
},
"dateformat": {
"dayNames": [
"Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam",
"Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi"
"Dim",
"Lun",
"Mar",
"Mer",
"Jeu",
"Ven",
"Sam",
"Dimanche",
"Lundi",
"Mardi",
"Mercredi",
"Jeudi",
"Vendredi",
"Samedi"
],
"monthNames": [
"Jan", "Fév", "Mar", "Avr", "Mai", "Jun", "Juil", "Aou", "Sept", "Oct", "Nov", "Déc",
"Janvier", "Février", "Mars", "Avril", "Mai", "Juin",
"Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"
"Jan",
"Fév",
"Mar",
"Avr",
"Mai",
"Jun",
"Juil",
"Aou",
"Sept",
"Oct",
"Nov",
"Déc",
"Janvier",
"Février",
"Mars",
"Avril",
"Mai",
"Juin",
"Juillet",
"Août",
"Septembre",
"Octobre",
"Novembre",
"Décembre"
],
"timeNames": [
"a", "p", "am", "pm", "A", "P", "AM", "PM"
"a",
"p",
"am",
"pm",
"A",
"P",
"AM",
"PM"
]
}
}
}
}
+3545 -2640
View File
File diff suppressed because it is too large Load Diff
+15 -10
View File
@@ -76,7 +76,8 @@
"target": [
"nsis"
],
"icon": "assets/favicon.ico"
"icon": "assets/favicon.ico",
"certificateSha1": "f3363a89b084f8f6b616babea3c91f9d79e15000"
},
"linux": {
"target": [
@@ -153,12 +154,11 @@
"@types/jest": "^27.5.2",
"@types/node": "17.0.23",
"@types/react": "^17.0.48",
"@types/react-dom": "^17.0.17",
"@types/react-dom": "^18.0.10",
"@types/react-outside-click-handler": "^1.3.1",
"@types/react-test-renderer": "^17.0.2",
"@types/terser-webpack-plugin": "^5.0.4",
"@types/use-double-click": "^1.0.1",
"@types/uuid": "^8.3.4",
"@types/webpack-bundle-analyzer": "^4.4.2",
"@types/webpack-env": "^1.18.0",
"@typescript-eslint/eslint-plugin": "^5.34.0",
@@ -173,7 +173,7 @@
"css-minimizer-webpack-plugin": "^4.1.0",
"detect-port": "^1.3.0",
"electron": "^20.0.3",
"electron-builder": "^23.3.3",
"electron-builder": "^23.6.0",
"electron-devtools-installer": "^3.2.0",
"electron-notarize": "^1.2.1",
"electron-rebuild": "^3.2.9",
@@ -200,6 +200,7 @@
"postcss": "^8.4.16",
"postcss-loader": "^6.2.1",
"prettier": "^2.7.1",
"ps-scrollbar-tailwind": "0.0.1",
"react-refresh": "^0.12.0",
"react-refresh-typescript": "^2.0.7",
"react-test-renderer": "^18.2.0",
@@ -208,7 +209,7 @@
"sass-loader": "^12.6.0",
"style-loader": "^3.3.1",
"tailwind-scrollbar": "^2.0.1",
"tailwindcss": "^3.1.8",
"tailwindcss": "^3.2.4",
"terser-webpack-plugin": "^5.3.5",
"ts-jest": "^27.1.5",
"ts-loader": "^9.3.0",
@@ -223,6 +224,7 @@
},
"dependencies": {
"@node-steam/vdf": "^2.2.0",
"@tippyjs/react": "^4.2.6",
"archiver": "^5.3.1",
"dateformat": "^5.0.3",
"dot-prop": "^7.2.0",
@@ -230,25 +232,28 @@
"electron-log": "^4.4.8",
"electron-store": "^8.1.0",
"electron-updater": "^5.2.4",
"framer-motion": "^7.2.1",
"fast-deep-equal": "^3.1.3",
"framer-motion": "^8.0.2",
"fs-extra": "^10.1.0",
"history": "^5.3.0",
"is-online": "^9.0.1",
"md5-file": "^5.0.0",
"node-fetch": "^3.3.0",
"node-stream-zip": "^1.15.0",
"react": "^18.2.0",
"react-colorful": "^5.6.1",
"react-dom": "^18.2.0",
"react-fitty": "^1.0.1",
"react-outside-click-handler": "^1.3.0",
"react-range": "^1.8.14",
"react-router-dom": "^6.3.0",
"react-visibility-sensor": "^5.1.1",
"regedit": "^5.1.1",
"rxjs": "^7.5.6",
"sanitize-filename": "^1.6.3",
"semver": "^7.3.8",
"tailwind-scrollbar-hide": "^1.1.7",
"tailwindcss-scoped-groups": "^2.0.0",
"use-delayed-state": "^1.2.0",
"use-double-click": "^1.0.5",
"uuid": "^9.0.0"
"use-fit-text": "^2.4.0"
},
"devEngines": {
"node": ">=14.x",
+6
View File
@@ -0,0 +1,6 @@
const tailwindcss = require("tailwindcss");
const autoprefixer = require("autoprefixer");
module.exports = {
plugins: [tailwindcss, autoprefixer],
};
+1 -1
View File
@@ -1,5 +1,5 @@
{
"name": "BSManager",
"name": "bs-manager",
"version": "0.2.0",
"lockfileVersion": 1,
"requires": true,
Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
import '@testing-library/jest-dom';
import { render } from '@testing-library/react';
import App from '../renderer/App';
import App from '../renderer/windows/App';
describe('App', () => {
it('should render', () => {
+2
View File
@@ -1,3 +1,5 @@
export const BS_EXECUTABLE = "Beat Saber.exe";
export const OCULUS_BS_DIR = "hyperbolic-magnetism-beat-saber"
export const BS_APP_ID = "620980";
export const BS_DEPOT = "620981";
export const APP_NAME = "BSManager";
+7
View File
@@ -0,0 +1,7 @@
export function splitIntoChunk<T = unknown>(arr: T[], chunkSize: number): T[][] {
const resArr = [];
for(let i = 0; i < arr.length; i += chunkSize){
resArr.push(arr.slice(i, i + chunkSize));
}
return resArr;
}
+9
View File
@@ -0,0 +1,9 @@
export function isValidUrl(url: string): boolean{
try{
new URL(url);
return true;
}
catch(e){
return false;
}
}
+49
View File
@@ -0,0 +1,49 @@
import { ipcMain } from "electron";
import { UtilsService } from "../services/utils.service";
import { IpcRequest } from "shared/models/ipc";
import { SearchParams } from "shared/models/maps/beat-saver.model";
import { BeatSaverService } from "../services/thrid-party/beat-saver/beat-saver.service";
ipcMain.on("bsv-search-map", async (event, request: IpcRequest<SearchParams>) => {
const utlis = UtilsService.getInstance();
const bsvService = BeatSaverService.getInstance();
bsvService.searchMaps(request.args).then(maps => {
utlis.ipcSend(request.responceChannel, {success: true, data: maps});
}).catch(e => {
utlis.ipcSend(request.responceChannel, {success: false, error: e});
})
});
ipcMain.on("bsv-get-map-details-from-hashs", async (event, request: IpcRequest<string[]>) => {
const utlis = UtilsService.getInstance();
const bsvService = BeatSaverService.getInstance();
bsvService.getMapDetailsFromHashs(request.args).then(maps => {
utlis.ipcSend(request.responceChannel, {success: true, data: maps});
}).catch(e => {
utlis.ipcSend(request.responceChannel, {success: false, error: e});
})
});
ipcMain.on("bsv-get-map-details-by-id", async (event, request: IpcRequest<string>) => {
const utlis = UtilsService.getInstance();
const bsvService = BeatSaverService.getInstance();
bsvService.getMapDetailsById(request.args).then(maps => {
utlis.ipcSend(request.responceChannel, {success: true, data: maps});
}).catch(e => {
utlis.ipcSend(request.responceChannel, {success: false, error: e});
})
});
ipcMain.on("bsv-get-playlist-details-by-id", async (event, request: IpcRequest<string>) => {
const utlis = UtilsService.getInstance();
const bsvService = BeatSaverService.getInstance();
bsvService.getPlaylistPage(request.args).then(maps => {
utlis.ipcSend(request.responceChannel, {success: true, data: maps});
}).catch(e => {
utlis.ipcSend(request.responceChannel, {success: false, error: e});
})
});
+11 -1
View File
@@ -5,6 +5,7 @@ import { IpcRequest } from 'shared/models/ipc';
import { InstallationLocationService } from '../services/installation-location.service';
import { UtilsService } from '../services/utils.service';
import { BsmException } from 'shared/models/bsm-exception.model';
import { LocalMapsManagerService } from '../services/additional-content/local-maps-manager.service';
export interface InitDownloadInfoInterface {
@@ -24,8 +25,17 @@ export interface DownloadInfo {
stay?: boolean
}
ipcMain.on('is-dotnet-6-installed', async (event, request: IpcRequest<void>) => {
const installer = BSInstallerService.getInstance();
const utils = UtilsService.getInstance();
installer.isDotNet6Installed().then(installed => {
utils.ipcSend(request.responceChannel, {success: true, data: installed});
});
});
ipcMain.on('bs-download.start', async (event, request: IpcRequest<DownloadInfo>) => {
BSInstallerService.getInstance().downloadBsVersion(request.args).then(res => {
BSInstallerService.getInstance().downloadBsVersion(request.args).then(async res => {
await LocalMapsManagerService.getInstance().linkVersionMaps(request.args.bsVersion, true).catch(e => {});
UtilsService.getInstance().ipcSend(request.responceChannel, {success: true, data: res});
}).catch(e => {
UtilsService.getInstance().ipcSend(request.responceChannel, {success: false, data: e});
+140
View File
@@ -0,0 +1,140 @@
import { ipcMain } from "electron";
import { LocalMapsManagerService } from "../services/additional-content/local-maps-manager.service";
import { UtilsService } from "../services/utils.service";
import { BSVersion } from "shared/bs-version.interface";
import { IpcRequest } from "shared/models/ipc";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { BsvMapDetail } from "shared/models/maps";
ipcMain.on('get-version-maps', (event, request: IpcRequest<BSVersion>) => {
const utilsService = UtilsService.getInstance();
const localMaps = LocalMapsManagerService.getInstance();
localMaps.getMaps(request.args).then(maps => {
utilsService.ipcSend<BsmLocalMap[]>(request.responceChannel, {success: true, data: maps});
}).catch(() => {
utilsService.ipcSend(request.responceChannel, {success: false});
})
});
ipcMain.on("verion-have-maps-linked", async (event, request: IpcRequest<BSVersion>) => {
const utils = UtilsService.getInstance();
const maps = LocalMapsManagerService.getInstance();
utils.ipcSend<boolean>(request.responceChannel, {success: true, data: await maps.versionIsLinked(request.args)});
});
ipcMain.on("link-version-maps", async (event, request: IpcRequest<{version: BSVersion, keepMaps: boolean}>) => {
const utils = UtilsService.getInstance();
const maps = LocalMapsManagerService.getInstance();
maps.linkVersionMaps(request.args.version, request.args.keepMaps).then(() => {
utils.ipcSend<void>(request.responceChannel, {success: true});
}).catch(err => {
utils.ipcSend<void>(request.responceChannel, {success: true, error: err});
});
});
ipcMain.on("unlink-version-maps", async (event, request: IpcRequest<{version: BSVersion, keepMaps: boolean}>) => {
const utils = UtilsService.getInstance();
const maps = LocalMapsManagerService.getInstance();
maps.unlinkVersionMaps(request.args.version, request.args.keepMaps).then(() => {
utils.ipcSend<void>(request.responceChannel, {success: true});
}).catch(err => {
utils.ipcSend<void>(request.responceChannel, {success: true, error: err});
});
});
ipcMain.on("delete-maps", async (event, request: IpcRequest<{version: BSVersion, maps: BsmLocalMap[]}>) => {
const utils = UtilsService.getInstance();
const maps = LocalMapsManagerService.getInstance();
maps.deleteMaps(request.args.maps, request.args.version).then(() => {
utils.ipcSend<void>(request.responceChannel, {success: true});
}).catch(err => {
utils.ipcSend<void>(request.responceChannel, {success: false, error: err});
});
});
ipcMain.on("export-maps", async (event, request: IpcRequest<{version: BSVersion, maps: BsmLocalMap[], outPath: string}>) => {
const utils = UtilsService.getInstance();
const maps = LocalMapsManagerService.getInstance();
maps.exportMaps(request.args.version, request.args.maps, request.args.outPath).then(() => {
utils.ipcSend<void>(request.responceChannel, {success: true});
}).catch(err => {
utils.ipcSend<void>(request.responceChannel, {success: false, error: err});
});
});
ipcMain.on("download-map", async (event, request: IpcRequest<{map: BsvMapDetail, version: BSVersion}>) => {
const utils = UtilsService.getInstance();
const maps = LocalMapsManagerService.getInstance();
maps.downloadMap(request.args.map, request.args.version).then(() => {
utils.ipcSend(request.responceChannel, {success: true});
}).catch(err => {
utils.ipcSend(request.responceChannel, {success: false, error: err});
});
});
ipcMain.on("one-click-install-map", async (event, request: IpcRequest<BsvMapDetail>) => {
const utils = UtilsService.getInstance();
const maps = LocalMapsManagerService.getInstance();
maps.oneClickDownloadMap(request.args).then(() => {
utils.ipcSend(request.responceChannel, {success: true});
}).catch(err => {
utils.ipcSend(request.responceChannel, {success: false, error: err});
});
});
ipcMain.on("register-maps-deep-link", async (event, request: IpcRequest<void>) => {
const maps = LocalMapsManagerService.getInstance();
const utils = UtilsService.getInstance();
try{
const res = maps.enableDeepLinks();
utils.ipcSend(request.responceChannel, {success: true, data: res});
}
catch(e){
utils.ipcSend(request.responceChannel, {success: false});
}
});
ipcMain.on("unregister-maps-deep-link", async (event, request: IpcRequest<void>) => {
const maps = LocalMapsManagerService.getInstance();
const utils = UtilsService.getInstance();
try{
const res = maps.disableDeepLinks();
utils.ipcSend(request.responceChannel, {success: true, data: res});
}
catch(e){
utils.ipcSend(request.responceChannel, {success: false});
}
});
ipcMain.on("is-map-deep-links-enabled", async (event, request: IpcRequest<void>) => {
const maps = LocalMapsManagerService.getInstance();
const utils = UtilsService.getInstance();
try{
const res = maps.isDeepLinksEnabled();
utils.ipcSend(request.responceChannel, {success: true, data: res});
}
catch(e){
utils.ipcSend(request.responceChannel, {success: false});
}
});
+61
View File
@@ -0,0 +1,61 @@
import { ipcMain } from "electron";
import { UtilsService } from "../services/utils.service";
import { IpcRequest } from "shared/models/ipc";
import { MSModel } from "shared/models/model-saber/model-saber.model";
import { LocalModelsManagerService } from "../services/additional-content/local-models-manager.service";
ipcMain.on("one-click-install-model", async (event, request: IpcRequest<MSModel>) => {
const utils = UtilsService.getInstance();
const models = LocalModelsManagerService.getInstance();
models.oneClickDownloadModel(request.args).then(() => {
utils.ipcSend(request.responceChannel, {success: true});
}).catch(e => {
utils.ipcSend(request.responceChannel, {success: false, error: e});
})
});
ipcMain.on("register-models-deep-link", async (event, request: IpcRequest<void>) => {
const maps = LocalModelsManagerService.getInstance();
const utils = UtilsService.getInstance();
try{
const res = maps.enableDeepLinks();
utils.ipcSend(request.responceChannel, {success: true, data: res});
}
catch(e){
utils.ipcSend(request.responceChannel, {success: false});
}
});
ipcMain.on("unregister-models-deep-link", async (event, request: IpcRequest<void>) => {
const maps = LocalModelsManagerService.getInstance();
const utils = UtilsService.getInstance();
try{
const res = maps.disableDeepLinks();
utils.ipcSend(request.responceChannel, {success: true, data: res});
}
catch(e){
utils.ipcSend(request.responceChannel, {success: false});
}
});
ipcMain.on("is-models-deep-links-enabled", async (event, request: IpcRequest<void>) => {
const maps = LocalModelsManagerService.getInstance();
const utils = UtilsService.getInstance();
try{
const res = maps.isDeepLinksEnabled();
utils.ipcSend(request.responceChannel, {success: true, data: res});
}
catch(e){
utils.ipcSend(request.responceChannel, {success: false});
}
});
+61
View File
@@ -0,0 +1,61 @@
import { ipcMain } from "electron";
import { IpcRequest } from "shared/models/ipc";
import { LocalPlaylistsManagerService } from "../services/additional-content/local-playlists-manager.service";
import { UtilsService } from "../services/utils.service";
ipcMain.on("one-click-install-playlist", async (event, request: IpcRequest<string>) => {
const utils = UtilsService.getInstance();
const playlists = LocalPlaylistsManagerService.getInstance();
playlists.oneClickInstallPlaylist(request.args).then(() => {
utils.ipcSend(request.responceChannel, {success: true});
}).catch(e => {
utils.ipcSend(request.responceChannel, {success: false, error: e});
});
});
ipcMain.on("register-playlists-deep-link", async (event, request: IpcRequest<void>) => {
const maps = LocalPlaylistsManagerService.getInstance();
const utils = UtilsService.getInstance();
try{
const res = maps.enableDeepLinks();
utils.ipcSend(request.responceChannel, {success: true, data: res});
}
catch(e){
utils.ipcSend(request.responceChannel, {success: false});
}
});
ipcMain.on("unregister-playlists-deep-link", async (event, request: IpcRequest<void>) => {
const maps = LocalPlaylistsManagerService.getInstance();
const utils = UtilsService.getInstance();
try{
const res = maps.disableDeepLinks();
utils.ipcSend(request.responceChannel, {success: true, data: res});
}
catch(e){
utils.ipcSend(request.responceChannel, {success: false});
}
});
ipcMain.on("is-playlists-deep-links-enabled", async (event, request: IpcRequest<void>) => {
const maps = LocalPlaylistsManagerService.getInstance();
const utils = UtilsService.getInstance();
try{
const res = maps.isDeepLinksEnabled();
utils.ipcSend(request.responceChannel, {success: true, data: res});
}
catch(e){
utils.ipcSend(request.responceChannel, {success: false});
}
});
-4
View File
@@ -2,13 +2,9 @@ import { ipcMain } from 'electron';
import { UtilsService } from '../services/utils.service';
import { BSVersionLibService } from '../services/bs-version-lib.service'
import { BSVersion } from 'shared/bs-version.interface';
import path from 'path';
import { exec } from 'child_process';
import { SteamService } from '../services/steam.service';
import { BS_APP_ID } from '../constants';
import { IpcRequest } from 'shared/models/ipc';
import { BSLocalVersionService } from '../services/bs-local-version.service';
import { InstallationLocationService } from '../services/installation-location.service';
import { BsmException } from 'shared/models/bsm-exception.model';
ipcMain.on('bs-version.get-version-dict', (event, req: IpcRequest<void>) => {
+5 -1
View File
@@ -3,8 +3,12 @@ import './os-controls-ipcs';
import './bs-launcher-ipcs';
import './bs-version-ipcs';
import './bs-uninstall-ipcs';
import './map-ipcs';
import './supporters-ipcs';
import './launcher-ipcs';
import './window-manager-ipcs';
import './bs-mods-ipcs';
import './bs-maps-ipcs';
import './beat-saver-ipcs';
import './bs-playlist-ipcs';
import './model-saber.ipcs';
import './bs-model-ipcs';
-13
View File
@@ -1,13 +0,0 @@
import { ipcMain} from 'electron';
import { MapService } from '../services/map.service';
import { UtilsService } from '../services/utils.service';
import { IpcRequest } from 'shared/models/ipc';
import { ExportVersionMapsOption } from 'shared/models/maps/export-version-maps.model';
ipcMain.on("map.export-version", (event, request: IpcRequest<ExportVersionMapsOption>) => {
const utils = UtilsService.getInstance();
const mapService = MapService.getInstance();
mapService.exportVersionMaps(request.args.version, request.args.path).then(() => {
utils.ipcSend(request.responceChannel, {success: true});
}).catch(() => utils.ipcSend(request.responceChannel, {success: false, error: {title: "", msg: ""}}));
});
+15
View File
@@ -0,0 +1,15 @@
import { ipcMain } from "electron";
import { IpcRequest } from "shared/models/ipc";
import { ModelSaberService } from "../services/thrid-party/model-saber/model-saber.service";
import { UtilsService } from "../services/utils.service";
ipcMain.on("ms-get-model-by-id", async (event, request: IpcRequest<string|number>) => {
const utils = UtilsService.getInstance();
const ms = ModelSaberService.getInstance();
ms.getModelById(request.args).then(model => {
utils.ipcSend(request.responceChannel, {success: true, data: model});
}).catch(e => {
utils.ipcSend(request.responceChannel, {success: false, error: e});
})
});
+25 -5
View File
@@ -1,25 +1,31 @@
import { ipcMain, shell, dialog, app } from 'electron';
import { UtilsService } from '../services/utils.service';
import { IpcRequest } from 'shared/models/ipc';
import { SystemNotificationOptions } from 'shared/models/notification/system-notification.model';
import { NotificationService } from '../services/notification.service';
import { SteamService } from '../services/steam.service';
// TODO IMPROVE WINDOW CONTROL BY USING WINDOW SERVICE
ipcMain.on('window.close', async () => {
const utils = UtilsService.getInstance();
utils.getMainWindow()?.close();
utils.getMainWindows("index.html")?.close();
});
ipcMain.on('window.maximize', async () => {
const utils = UtilsService.getInstance();
utils.getMainWindow()?.maximize();
utils.getMainWindows("index.html")?.maximize();
});
ipcMain.on('window.minimize', async () => {
const utils = UtilsService.getInstance();
utils.getMainWindow()?.minimize();
utils.getMainWindows("index.html")?.minimize();
});
ipcMain.on('window.reset', async () => {
const utils = UtilsService.getInstance();
utils.getMainWindow()?.restore();
utils.getMainWindows("index.html")?.restore();
});
ipcMain.on('new-window', async (event, request: IpcRequest<string>) => {
@@ -34,7 +40,7 @@ ipcMain.on('choose-folder', async (event, request: IpcRequest<void>) => {
ipcMain.on("window.progression", async (event, request: IpcRequest<number>) => {
const utils = UtilsService.getInstance();
utils.getMainWindow().setProgressBar(request.args / 100);
utils.getMainWindows("index.html")?.setProgressBar(request.args / 100);
});
ipcMain.on('save-file', async (event, request: IpcRequest<{filename?: string, filters?: Electron.FileFilter[]}>) => {
@@ -52,3 +58,17 @@ ipcMain.on("current-version", async (event, request: IpcRequest<void>) => {
ipcMain.on("open-logs", async (event, request: IpcRequest<void>) => {
shell.openPath(app.getPath("logs"));
});
ipcMain.on("notify-system", async (event, request: IpcRequest<SystemNotificationOptions>) => {
NotificationService.getInstance().notify(request.args)
});
ipcMain.on("open-steam", async (event, request: IpcRequest<void>) => {
const steam = SteamService.getInstance();
const utils = UtilsService.getInstance();
steam.openSteam().then(res => {
utils.ipcSend(request.responceChannel, {success: true, data: res});
}).catch((e) => {
utils.ipcSend(request.responceChannel, {success: false, error: e});
});
});
+10
View File
@@ -9,4 +9,14 @@ ipcMain.on("open-window-then-close-all", async (event, request: IpcRequest<AppWi
windowManager.openWindow(request.args).then(window => {
windowManager.closeAllWindows(request.args);
});
});
ipcMain.on("close-all-windows", async (event, request: IpcRequest<AppWindow>) => {
const windowManager = WindowManagerService.getInstance();
windowManager.closeAllWindows(request.args);
});
ipcMain.on("close-windows", async (event, request: IpcRequest<AppWindow[]>) => {
const windowManager = WindowManagerService.getInstance();
windowManager.close(...request.args);
});
+56 -9
View File
@@ -9,20 +9,24 @@
* `./src/main.js` using webpack. This gives us some performance wins.
*/
import path from 'path';
import { app } from 'electron';
import { app, protocol } from 'electron';
import log from 'electron-log';
import './ipcs';
import { UtilsService } from './services/utils.service';
import { WindowManagerService } from './services/window-manager.service';
export const PRELOAD_PATH = app.isPackaged ? path.join(__dirname, 'preload.js') : path.join(__dirname, '../../.erb/dll/preload.js')
import { DeepLinkService } from './services/deep-link.service';
import { AppWindow } from 'shared/models/window-manager/app-window.model';
import { LocalMapsManagerService } from './services/additional-content/local-maps-manager.service';
import { LocalPlaylistsManagerService } from './services/additional-content/local-playlists-manager.service';
import { LocalModelsManagerService } from './services/additional-content/local-models-manager.service';
import { APP_NAME } from './constants';
const isDebug = process.env.NODE_ENV === 'development' || process.env.DEBUG_PROD === 'true';
log.transports.file.level = 'info';
log.transports.file.resolvePath = (() => {
const now = new Date();
return path.join(app.getPath("logs"), `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}.log`);
return path.join(app.getPath("logs"), `${now.getFullYear()}-${now.getMonth()+1}-${now.getDate()}-v${app.getVersion()}.log`);
});
log.catchErrors();
@@ -47,11 +51,18 @@ const installExtensions = async () => {
return installer.default(extensions.map((name) => installer[name]), forceDownload).catch(console.log);
};
const createWindow = async () => {
const createWindow = async (window: AppWindow = "launcher.html") => {
if(isDebug){ await installExtensions(); }
WindowManagerService.getInstance().openWindow("launcher.html");
WindowManagerService.getInstance().openWindow(window);
};
const initServicesMustBeInitialized = () => {
LocalMapsManagerService.getInstance();
LocalPlaylistsManagerService.getInstance();
LocalModelsManagerService.getInstance();
// Model
}
app.on('window-all-closed', () => {
// Respect the OSX convention of having the application in memory even
// after all windows have been closed
@@ -60,6 +71,42 @@ app.on('window-all-closed', () => {
}
});
app.whenReady().then(() => {
createWindow();
}).catch(log.error);
const gotTheLock = app.requestSingleInstanceLock();
if(!gotTheLock){
app.quit();
}
else{
app.on('second-instance', (e, argv) => {
const deepLink = argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg));
if(!deepLink){ return; }
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
});
app.whenReady().then(() => {
app.setAppUserModelId(APP_NAME);
initServicesMustBeInitialized();
const deepLink = process.argv.find(arg => DeepLinkService.getInstance().isDeepLink(arg));
if(!deepLink){
createWindow();
}
else{
DeepLinkService.getInstance().dispatchLinkOpened(deepLink);
}
protocol.registerFileProtocol('file', (request, callback) => {
const pathname = decodeURI(request.url.replace('file:///', ''));
callback(pathname);
});
}).catch(log.error);
}
@@ -0,0 +1,306 @@
import path from "path";
import { BSVersion } from "shared/bs-version.interface";
import { BsvMapDetail, RawMapInfoData } from "shared/models/maps";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import { BSLocalVersionService } from "../bs-local-version.service";
import { InstallationLocationService } from "../installation-location.service";
import { UtilsService } from "../utils.service";
import crypto from "crypto";
import { lstatSync, symlinkSync, unlinkSync, readdirSync, createWriteStream } from "fs";
import { copySync } from "fs-extra";
import StreamZip from "node-stream-zip";
import { RequestService } from "../request.service";
import sanitize from "sanitize-filename";
import archiver from "archiver";
import { DeepLinkService } from "../deep-link.service";
import log from 'electron-log';
import { WindowManagerService } from "../window-manager.service";
import { ipcMain } from "electron";
import { IpcRequest } from 'shared/models/ipc';
export class LocalMapsManagerService {
private static instance: LocalMapsManagerService;
public static getInstance(): LocalMapsManagerService{
if(!LocalMapsManagerService.instance){ LocalMapsManagerService.instance = new LocalMapsManagerService(); }
return LocalMapsManagerService.instance;
}
private readonly LEVELS_ROOT_FOLDER = "Beat Saber_Data";
private readonly CUSTOM_LEVELS_FOLDER = "CustomLevels";
private readonly DEEP_LINKS = {
BeatSaver: "beatsaver",
ScoreSaber: "web+bsmap"
};
private readonly localVersion: BSLocalVersionService;
private readonly installLocation: InstallationLocationService;
private readonly utils: UtilsService;
private readonly reqService: RequestService;
private readonly deepLink: DeepLinkService;
private readonly windows: WindowManagerService;
private constructor(){
this.localVersion = BSLocalVersionService.getInstance();
this.installLocation = InstallationLocationService.getInstance();
this.utils = UtilsService.getInstance();
this.reqService = RequestService.getInstance();
this.deepLink = DeepLinkService.getInstance();
this.windows = WindowManagerService.getInstance();
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, (link) => {
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link);
this.openOneClickDownloadMapWindow(new URL(link).host);
});
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ScoreSaber, (link) => {
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.ScoreSaber, link);
this.openOneClickDownloadMapWindow(new URL(link).host, true);
});
}
public async getMapsFolderPath(version?: BSVersion): Promise<string>{
if(version){ return path.join(await this.localVersion.getVersionPath(version), this.LEVELS_ROOT_FOLDER, this.CUSTOM_LEVELS_FOLDER); }
const sharedMapsPath = path.join(this.installLocation.sharedMapsPath, this.CUSTOM_LEVELS_FOLDER);
if(!(await this.utils.pathExist(sharedMapsPath))){
this.utils.createFolderIfNotExist(sharedMapsPath);
}
return sharedMapsPath;
}
private async computeMapHash(mapPath: string, rawInfoString: string): Promise<string>{
const mapRawInfo = JSON.parse(rawInfoString);
let content = rawInfoString;
for(const set of mapRawInfo._difficultyBeatmapSets){
for(const diff of set._difficultyBeatmaps){
const diffFilePath = path.join(mapPath, diff._beatmapFilename);
if(!await this.utils.pathExist(diffFilePath)){ continue; }
const diffContent = (await this.utils.readFileAsync(diffFilePath)).toString();
content += diffContent;
}
}
const shasum = crypto.createHash("sha1");
shasum.update(content);
return shasum.digest("hex");
}
private async loadMapInfoFromPath(mapPath: string): Promise<BsmLocalMap>{
const infoFilePath = path.join(mapPath, "Info.dat");
if(!(await this.utils.pathExist(infoFilePath))){ return null; }
const rawInfoString = await (await (this.utils.readFileAsync(infoFilePath))).toString();
const rawInfo: RawMapInfoData = JSON.parse(rawInfoString);
const coverUrl = new URL(`file:///${path.join(mapPath, rawInfo._coverImageFilename)}`).href;
const songUrl = new URL(`file:///${path.join(mapPath, rawInfo._songFilename)}`).href;
const hash = await this.computeMapHash(mapPath, rawInfoString);
return {rawInfo, coverUrl, songUrl, hash};
}
private async downloadMapZip(zipUrl: string): Promise<{zip: StreamZip.StreamZipAsync, zipPath: string}>{
const fileName = path.basename(zipUrl);
const tempPath = this.utils.getTempPath();
this.utils.createFolderIfNotExist(this.utils.getTempPath());
const dest = path.join(tempPath, fileName);
const zipPath = await this.reqService.downloadFile(zipUrl, dest);
const zip = new StreamZip.async({file : zipPath});
return {zip, zipPath};
}
private async getAbsoluteFolderOfMaps(maps: BsmLocalMap[], version: BSVersion): Promise<string[]>{
const mapsFolder = await this.getMapsFolderPath(version);
const res: string[] = [];
const mapHashs = maps.map(map => map.hash);
const mapsFolders = readdirSync(mapsFolder, {withFileTypes: true});
for(const content of mapsFolders){
if(!content.isDirectory()){ continue; }
const mapFolderPath = path.join(mapsFolder, content.name);
const { hash } = await this.loadMapInfoFromPath(mapFolderPath);
if(mapHashs.includes(hash)){
res.push(mapFolderPath);
}
}
return res;
}
private openOneClickDownloadMapWindow(mapId: string, isHash = false): void{
ipcMain.once("one-click-map-info", async (event, req: IpcRequest<void>) => {
this.utils.ipcSend(req.responceChannel, {success: true, data: {id: mapId, isHash}});
});
this.windows.openWindow("oneclick-download-map.html");
}
public async getMaps(version?: BSVersion): Promise<BsmLocalMap[]>{
const levelsFolder = await this.getMapsFolderPath(version);
const levelsPath = (await this.utils.pathExist(levelsFolder)) ? this.utils.listDirsInDir(levelsFolder, true) : [];
const mapsInfo = await Promise.all(levelsPath.map(levelPath => this.loadMapInfoFromPath(levelPath)));
return mapsInfo.filter(info => !!info);
}
public async versionIsLinked(version: BSVersion): Promise<boolean>{
const levelsPath = await this.getMapsFolderPath(version);
const isPathExist = await this.utils.pathExist(levelsPath);
if(!isPathExist){ return false; }
return lstatSync(levelsPath).isSymbolicLink()
}
public async linkVersionMaps(version: BSVersion, keepMaps: boolean): Promise<void>{
if(await this.versionIsLinked(version)){ return; }
const sharedMapsPath = await this.getMapsFolderPath();
const versionMapsPath = await this.getMapsFolderPath(version);
if(keepMaps){
await this.utils.moveDirContent(versionMapsPath, sharedMapsPath);
}
await this.utils.deleteFolder(versionMapsPath);
symlinkSync(sharedMapsPath, versionMapsPath, "junction");
}
public async unlinkVersionMaps(version: BSVersion, keepMaps: boolean): Promise<void>{
const sharedMapsPath = await this.getMapsFolderPath();
const versionMapsPath = await this.getMapsFolderPath(version);
if(await this.versionIsLinked(version)){
unlinkSync(versionMapsPath);
}
this.utils.createFolderIfNotExist(versionMapsPath);
if(keepMaps){
copySync(sharedMapsPath, versionMapsPath);
}
}
public async deleteMaps(maps: BsmLocalMap[], verion?: BSVersion){
const mapsFolders = await this.getAbsoluteFolderOfMaps(maps, verion);
const mapsHashsToDelete = maps.map(map => map.hash);
for(const folder of mapsFolders){
const { hash } = await this.loadMapInfoFromPath(folder);
if(mapsHashsToDelete.includes(hash)){
await this.utils.deleteFolder(folder);
}
}
}
public async downloadMap(map: BsvMapDetail, version?: BSVersion): Promise<string>{
if(!map.versions.at(0).hash){ throw "Cannot download map, no hash found"; }
const zipUrl = map.versions.at(0).downloadURL;
const mapsFolder = await this.getMapsFolderPath(version);
const {zip, zipPath} = await this.downloadMapZip(zipUrl);
const mapFolderName = sanitize(`${map.id}-${map.name}`);
const mapPath = path.join(mapsFolder, mapFolderName);
if(!zip){ throw `Cannot download ${zipUrl}`; }
this.utils.createFolderIfNotExist(mapPath);
await zip.extract(null, mapPath);
await zip.close();
unlinkSync(zipPath);
return mapPath;
}
public async exportMaps(version: BSVersion, maps: BsmLocalMap[], outPath: string){
const output = createWriteStream(outPath);
const archive = archiver("zip", {zlib: {level: 9}});
archive.pipe(output);
archive.on("error", (e) => {throw e});
if(!maps || maps.length === 0){
const mapsFolder = await this.getMapsFolderPath(version);
archive.directory(mapsFolder, false);
}
else{
const mapsFolders = await this.getAbsoluteFolderOfMaps(maps, version);
for(const folder of mapsFolders){
archive.directory(folder, path.basename(folder));
}
}
await archive.finalize();
}
public async oneClickDownloadMap(map: BsvMapDetail): Promise<void>{
const downloadedMap = await this.downloadMap(map);
const versions = await this.localVersion.getInstalledVersions();
for(const version of versions){
if(await this.versionIsLinked(version)){ continue; }
const versionMapsPath = await this.getMapsFolderPath(version);
this.utils.createFolderIfNotExist(versionMapsPath);
copySync(downloadedMap, path.join(versionMapsPath, path.basename(downloadedMap)), {overwrite: true});
}
}
public enableDeepLinks(): boolean{
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.registerDeepLink(link));
}
public disableDeepLinks(): boolean{
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.unRegisterDeepLink(link));
}
public isDeepLinksEnabled(): boolean{
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistred(link));
}
}
@@ -0,0 +1,126 @@
import { DeepLinkService } from "../deep-link.service";
import log from "electron-log"
import { ipcMain } from "electron";
import { IpcRequest } from "shared/models/ipc";
import { UtilsService } from "../utils.service";
import { WindowManagerService } from "../window-manager.service";
import { MSModel, MSModelType } from "shared/models/model-saber/model-saber.model";
import { BSVersion } from "shared/bs-version.interface";
import { BSLocalVersionService } from "../bs-local-version.service";
import path from "path";
import { RequestService } from "../request.service";
import { copyFileSync } from "fs-extra";
import sanitize from "sanitize-filename";
export class LocalModelsManagerService {
private static instance: LocalModelsManagerService;
public static getInstance(): LocalModelsManagerService{
if(!LocalModelsManagerService.instance){ LocalModelsManagerService.instance = new LocalModelsManagerService(); }
return LocalModelsManagerService.instance;
}
private readonly DEEP_LINKS = {
ModelSaber: "modelsaber",
};
private readonly MODEL_TYPE_FOLDER: Record<Exclude<MSModelType, "misc">, string> = {
avatar: "CustomAvatars",
bloq: "CustomNotes",
platform: "CustomPlatforms",
saber: "CustomSabers"
}
private readonly deepLink: DeepLinkService;
private readonly utils: UtilsService;
private readonly windows: WindowManagerService;
private readonly localVersion: BSLocalVersionService;
private readonly request: RequestService;
private constructor(){
this.deepLink = DeepLinkService.getInstance();
this.utils = UtilsService.getInstance();
this.windows = WindowManagerService.getInstance();
this.localVersion = BSLocalVersionService.getInstance();
this.request = RequestService.getInstance();
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.ModelSaber, (link) => {
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.ModelSaber, link);
const url = new URL(link);
const type = url.host
const id = url.pathname.replace("/", '').split("/").at(0);
this.openOneClickDownloadModelWindow(id, type);
});
}
private openOneClickDownloadModelWindow(id: string, type: string){
ipcMain.once("one-click-model-info", async (event, req: IpcRequest<void>) => {
this.utils.ipcSend(req.responceChannel, {success: true, data: {id, type}});
});
this.windows.openWindow("oneclick-download-model.html");
}
private async getModelFolderPath(type: MSModelType, version?: BSVersion): Promise<string>{
if(!version){ throw "will be implemented whith models management" }
if(type === "misc"){ throw "model type not supported"; }
const versionPath = await this.localVersion.getVersionPath(version);
const modelFolderPath = path.join(versionPath, this.MODEL_TYPE_FOLDER[type]);
this.utils.createFolderIfNotExist(modelFolderPath);
return modelFolderPath;
}
public async downloadModel(model: MSModel, version: BSVersion): Promise<string>{
const modelFolder = await this.getModelFolderPath(model.type, version);
const modelDest = path.join(modelFolder, sanitize(path.basename(model.download)));
return this.request.downloadFile(model.download, modelDest);
}
public async oneClickDownloadModel(model: MSModel): Promise<void>{
if(!model){ return; }
const versions = await this.localVersion.getInstalledVersions();
if(versions?.length === 0){ return; }
const fisrtVersion = versions.shift();
const downloaded = await this.downloadModel(model, fisrtVersion);
for(const version of versions){
const modelDest = path.join(await this.getModelFolderPath(model.type, version), path.basename(downloaded));
copyFileSync(downloaded, modelDest);
}
}
public enableDeepLinks(): boolean{
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.registerDeepLink(link));
}
public disableDeepLinks(): boolean{
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.unRegisterDeepLink(link));
}
public isDeepLinksEnabled(): boolean{
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistred(link));
}
}
@@ -0,0 +1,226 @@
import path from "path";
import { BehaviorSubject, Observable } from "rxjs";
import { BSVersion } from "shared/bs-version.interface";
import { BSLocalVersionService } from "../bs-local-version.service";
import { DeepLinkService } from "../deep-link.service";
import { RequestService } from "../request.service";
import { UtilsService } from "../utils.service";
import { LocalMapsManagerService } from "./local-maps-manager.service";
import log from "electron-log"
import { isValidUrl } from "../../helpers/url.helpers";
import { ipcMain } from "electron";
import { WindowManagerService } from "../window-manager.service";
import { IpcRequest } from "shared/models/ipc";
import { BPList, DownloadPlaylistProgression } from "shared/models/playlists/playlist.interface";
import { copyFileSync, readFileSync } from "fs";
import { BeatSaverService } from "../thrid-party/beat-saver/beat-saver.service";
import { copySync } from "fs-extra";
export class LocalPlaylistsManagerService {
private static instance: LocalPlaylistsManagerService
public static getInstance(): LocalPlaylistsManagerService {
if (!LocalPlaylistsManagerService.instance) { LocalPlaylistsManagerService.instance = new LocalPlaylistsManagerService(); }
return LocalPlaylistsManagerService.instance;
}
private readonly PLAYLISTS_FOLDER = "Playlists";
private readonly DEEP_LINKS = {
BeatSaver: "bsplaylist",
};
private readonly versions: BSLocalVersionService;
private readonly maps: LocalMapsManagerService;
private readonly utils: UtilsService;
private readonly request: RequestService;
private readonly deepLink: DeepLinkService;
private readonly windows: WindowManagerService;
private readonly bsaver: BeatSaverService;
private constructor(){
this.maps = LocalMapsManagerService.getInstance();
this.versions = BSLocalVersionService.getInstance();
this.utils = UtilsService.getInstance();
this.request = RequestService.getInstance();
this.deepLink = DeepLinkService.getInstance();
this.windows = WindowManagerService.getInstance();
this.bsaver = BeatSaverService.getInstance();
this.deepLink.addLinkOpenedListener(this.DEEP_LINKS.BeatSaver, link => {
log.info("DEEP-LINK RECEIVED FROM", this.DEEP_LINKS.BeatSaver, link);
const url = new URL(link);
const bplistUrl = url.host === "playlist" ? url.pathname.replace("/", "") : "";
this.openOneClickDownloadPlaylistWindow(bplistUrl);
});
}
private getPlaylistIdFromDownloadUrl(url: string): string{
if(!isValidUrl(url)){ return ""; }
const splited = url.split("/")
const idIndex = splited.indexOf("id");
if(idIndex < 0){ return ""; }
return splited[idIndex + 1];
}
private async getPlaylistsFolder(version?: BSVersion){
if(!version){ throw "Playlists are not available to be linked yet" }
const versionFolder = await this.versions.getVersionPath(version);
const folder = path.join(versionFolder, this.PLAYLISTS_FOLDER);
await this.utils.createFolderIfNotExist(folder)
return folder;
}
private async installBPListFile(bpListUrlOrPath: string, version: BSVersion): Promise<string>{
const playlistFolder = await this.getPlaylistsFolder(version);
const bpListDest = path.join(playlistFolder, path.basename(bpListUrlOrPath));
if(this.utils.pathExist(bpListUrlOrPath)){
copyFileSync(bpListUrlOrPath, bpListDest);
}
else{
await this.request.downloadFile(bpListUrlOrPath, bpListDest);
}
return bpListDest;
}
private async readPlaylistFile(path: string): Promise<BPList>{
if(!this.utils.pathExist(path)){ throw `bplist file not exist at ${path}`; }
const rawContent = readFileSync(path).toString();
return JSON.parse(rawContent);
}
private openOneClickDownloadPlaylistWindow(downloadUrl: string): void{
ipcMain.once("one-click-playlist-info", async (event, req: IpcRequest<void>) => {
this.utils.ipcSend(req.responceChannel, {success: true, data: {bpListUrl: downloadUrl, id: this.getPlaylistIdFromDownloadUrl(downloadUrl)}});
});
this.windows.openWindow("oneclick-download-playlist.html");
}
public downloadPlaylist(bpListUrl: string, version: BSVersion): Observable<DownloadPlaylistProgression>{
const res = new BehaviorSubject<DownloadPlaylistProgression>({progression: 0, current: null, downloadedMaps: [], mapsPath: [], bpListPath: ""});
const sub = res.subscribe(process => {
this.utils.ipcSend("download-playlist-progress", {success: true, data: process});
}, err => {
this.utils.ipcSend("download-playlist-progress", {success: false, error: err});
});
const observer = async () => {
try{
const bpListPath = await this.installBPListFile(bpListUrl, version);
res.next({...res.value, bpListPath});
const bpList = await this.readPlaylistFile(bpListPath);
for(const song of bpList.songs){
if(!song.key){ continue; }
const map = await this.bsaver.getMapDetailsById(song.key);
res.next({
...res.value,
current: map,
progression: ((res.value.downloadedMaps.length + .5) / bpList.songs.length) * 100
});
const mapPath = await this.maps.downloadMap(map, version);
const progression = ((res.value.downloadedMaps.length + 1) / bpList.songs.length) * 100;
res.next({
...res.value,
current: null,
downloadedMaps: [...res.value.downloadedMaps, map],
mapsPath: [...res.value.mapsPath, mapPath],
progression
});
}
}
catch(e){
res.error(e);
}
res.complete();
}
observer().finally(() => sub.unsubscribe());
return res.asObservable();
}
public async oneClickInstallPlaylist(bpListUrl: string): Promise<void>{
const versions = await this.versions.getInstalledVersions();
const firstVersion = versions.shift();
const fistVersionLinked = await this.maps.versionIsLinked(firstVersion);
const {bpListPath, mapsPath} = await this.downloadPlaylist(bpListUrl, firstVersion).toPromise();
for(const version of versions){
await this.installBPListFile(bpListPath, version);
const versionIsLinked = await this.maps.versionIsLinked(version);
if(fistVersionLinked && versionIsLinked){ continue; }
for(const mapPath of mapsPath){
const versionMapsFolder = await this.maps.getMapsFolderPath(version);
const mapDest = path.join(versionMapsFolder, path.basename(mapPath));
copySync(mapPath, mapDest, {overwrite: true});
}
}
}
public enableDeepLinks(): boolean{
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.registerDeepLink(link));
}
public disableDeepLinks(): boolean{
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.unRegisterDeepLink(link));
}
public isDeepLinksEnabled(): boolean{
return Array.from(Object.values(this.DEEP_LINKS)).every(link => this.deepLink.isDeepLinkRegistred(link));
}
}
+17 -2
View File
@@ -2,12 +2,13 @@ import { BS_APP_ID, BS_DEPOT } from "../constants";
import path from "path";
import { BSVersion } from 'shared/bs-version.interface';
import { UtilsService } from "./utils.service";
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
import { ChildProcessWithoutNullStreams, spawn, spawnSync } from "child_process";
import log from "electron-log";
import { InstallationLocationService } from "./installation-location.service";
import { ctrlc } from "ctrlc-windows";
import { BSLocalVersionService } from "./bs-local-version.service";
import isOnline from 'is-online';
import { WindowManagerService } from "./window-manager.service";
export class BSInstallerService{
@@ -16,6 +17,7 @@ export class BSInstallerService{
private readonly utils: UtilsService;
private readonly installLocationService: InstallationLocationService;
private readonly localVersionService: BSLocalVersionService;
private readonly windows: WindowManagerService;
private downloadProcess: ChildProcessWithoutNullStreams;
@@ -23,8 +25,9 @@ export class BSInstallerService{
this.utils = UtilsService.getInstance();
this.installLocationService = InstallationLocationService.getInstance();
this.localVersionService = BSLocalVersionService.getInstance();
this.windows = WindowManagerService.getInstance();
this.utils.getMainWindow().on("close", () => {
this.windows.getWindows("index.html")?.on("close", () => {
this.killDownloadProcess();
});
}
@@ -62,6 +65,18 @@ export class BSInstallerService{
});
}
public async isDotNet6Installed(): Promise<boolean>{
try{
const process = spawnSync(this.getDepotDownloaderExePath());
const out = process.output.toString();
if(out.includes(".NET runtime can be found at")){ return false; }
return true;
}
catch(e){
return false;
}
}
public async downloadBsVersion(downloadInfos: DownloadInfo): Promise<DownloadEvent>{
if(this.downloadProcess && this.downloadProcess.connected){ throw "AlreadyDownloading"; }
+6 -2
View File
@@ -5,6 +5,7 @@ import { BS_EXECUTABLE, BS_APP_ID } from "../constants";
import { ChildProcessWithoutNullStreams, spawn } from "child_process";
import { SteamService } from "./steam.service";
import { BSLocalVersionService } from "./bs-local-version.service";
import { OculusService } from "./oculus.service";
export class BSLauncherService{
@@ -12,6 +13,7 @@ export class BSLauncherService{
private readonly utilsService: UtilsService;
private readonly steamService: SteamService;
private readonly oculusService: OculusService;
private readonly localVersionService: BSLocalVersionService;
private bsProcess: ChildProcessWithoutNullStreams;
@@ -24,6 +26,7 @@ export class BSLauncherService{
private constructor(){
this.utilsService = UtilsService.getInstance();
this.steamService = SteamService.getInstance();
this.oculusService = OculusService.getInstance();
this.localVersionService = BSLocalVersionService.getInstance();
}
@@ -32,7 +35,8 @@ export class BSLauncherService{
}
public async launch(launchOptions: LauchOption): Promise<LaunchResult>{
if(!this.steamService.steamRunning()){ return "STEAM_NOT_RUNNING" }
if(launchOptions.version.oculus && !this.oculusService.oculusRunning()){ return "OCULUS_NOT_RUNNING" }
if(!launchOptions.version.oculus && !this.steamService.steamRunning()){ return "STEAM_NOT_RUNNING" }
if(this.isBsRunning()){ return "BS_ALREADY_RUNNING" }
const cwd = await this.localVersionService.getVersionPath(launchOptions.version);
@@ -42,7 +46,7 @@ export class BSLauncherService{
const launchMods = [launchOptions.oculus && "-vrmode oculus", launchOptions.desktop && "fpfc", launchOptions.debug && "--verbose"];
this.bsProcess = spawn(`\"${exePath}\"`, launchMods, {shell: true, cwd: cwd, env: {...process.env, "SteamAppId": BS_APP_ID}});
this.bsProcess = spawn(`\"${exePath}\"`, launchMods, {shell: true, cwd, env: {...process.env, "SteamAppId": BS_APP_ID}});
this.bsProcess.on('message', msg => { /** EMIT HERE **/ });
this.bsProcess.on('error', err => { /** EMIT HERE **/ });
+47 -23
View File
@@ -3,7 +3,7 @@ import { BSVersion } from 'shared/bs-version.interface';
import { InstallationLocationService } from "./installation-location.service";
import { SteamService } from "./steam.service";
import { UtilsService } from "./utils.service";
import { BS_APP_ID } from "../constants";
import { BS_APP_ID, OCULUS_BS_DIR } from "../constants";
import path from "path";
import { createInterface } from "readline";
import { createReadStream } from "fs";
@@ -12,6 +12,8 @@ import { ConfigurationService } from "./configuration.service";
import { rename } from "fs/promises";
import { BsmException } from "shared/models/bsm-exception.model";
import log from "electron-log";
import { OculusService } from "./oculus.service";
import { DownloadLinkType } from "shared/models/mods";
export class BSLocalVersionService{
@@ -22,6 +24,7 @@ export class BSLocalVersionService{
private readonly installLocationService: InstallationLocationService;
private readonly utilsService: UtilsService;
private readonly steamService: SteamService;
private readonly oculusService: OculusService;
private readonly remoteVersionService: BSVersionLibService;
private readonly configService: ConfigurationService;
@@ -34,6 +37,7 @@ export class BSLocalVersionService{
this.installLocationService = InstallationLocationService.getInstance();
this.utilsService = UtilsService.getInstance();
this.steamService = SteamService.getInstance();
this.oculusService = OculusService.getInstance();
this.remoteVersionService = BSVersionLibService.getInstance();
this.configService = ConfigurationService.getInstance();
}
@@ -42,12 +46,12 @@ export class BSLocalVersionService{
const versionFilePath = path.join(bsPath, 'Beat Saber_Data', 'globalgamemanagers');
if(!this.utilsService.pathExist(versionFilePath)){ return null; }
const versionsAvailable = await this.remoteVersionService.getAvailableVersions();
return new Promise<string>(resolve => {
return new Promise<string>(resolve => {
const readLine = createInterface({ input: createReadStream(versionFilePath) });
let findVersion: string = null;
readLine.on('line', (line) => {
for(const version of versionsAvailable){
if(line.includes(version.BSVersion)){
if(line.includes(version.BSVersion)){
findVersion = version.BSVersion;
readLine.close();
break;
@@ -80,6 +84,7 @@ export class BSLocalVersionService{
public async getVersionPath(version: BSVersion): Promise<string>{
if(version.steam){ return this.steamService.getGameFolder(BS_APP_ID, "Beat Saber") }
if(version.oculus){ return this.oculusService.getGameFolder(OCULUS_BS_DIR); }
return path.join(
this.installLocationService.versionsDirectory,
this.getVersionFolder(version)
@@ -87,7 +92,6 @@ export class BSLocalVersionService{
}
private removeSpecialChar(seq: string): string{
// eslint-disable-next-line no-useless-escape
return seq.replace( /[<>:"\/\\|?*]+/g, '' );
}
@@ -95,27 +99,46 @@ export class BSLocalVersionService{
return version.name ? `${version.BSVersion}-${version.name}` : version.BSVersion;
}
public getVersionType(version: BSVersion): "steam"|"universal"{
public getVersionType(version: BSVersion): DownloadLinkType{
if(version.steam){ return "steam"; }
if(version.oculus){ return "oculus"; }
return "universal"
}
private async getSteamVersion(): Promise<BSVersion>{
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
if(!steamBsFolder || !this.utilsService.pathExist(steamBsFolder)){ return null; }
const steamBsVersion = await this.getVersionOfBSFolder(steamBsFolder);
if(!steamBsVersion){ return null; }
const version = await this.remoteVersionService.getVersionDetails(steamBsVersion);
if(!version){ return null; }
return {...version, steam: true};
}
private async getOculusVersion(): Promise<BSVersion>{
const oculusBsFolder = await this.oculusService.getGameFolder(OCULUS_BS_DIR);
if(!oculusBsFolder){ return null; }
const oculusBsVersion = await this.getVersionOfBSFolder(oculusBsFolder);
if(!oculusBsVersion){ return null; }
const version = await this.remoteVersionService.getVersionDetails(oculusBsVersion);
if(!version){ return null; }
return {...version, oculus: true};
}
public async getInstalledVersions(): Promise<BSVersion[]>{
const versions: BSVersion[] = [];
const steamBsFolder = await this.steamService.getGameFolder(BS_APP_ID, "Beat Saber");
if(steamBsFolder && this.utilsService.pathExist(steamBsFolder)){
const steamBsVersion = await this.getVersionOfBSFolder(steamBsFolder);
if(steamBsVersion){
const steamVersionDetails = this.remoteVersionService.getVersionDetails(steamBsVersion);
versions.push(steamVersionDetails ? {...steamVersionDetails, steam: true} : {BSVersion: steamBsVersion, steam: true});
}
}
const steamVersion = await this.getSteamVersion();
if(steamVersion){ versions.push(steamVersion); }
const oculusVersion = await this.getOculusVersion();
if(oculusVersion){ versions.push(oculusVersion); }
if(!this.utilsService.pathExist(this.installLocationService.versionsDirectory)){ return versions }
const folderInInstallation = this.utilsService.listDirsInDir(this.installLocationService.versionsDirectory);
folderInInstallation.forEach(f => {
for(const f of folderInInstallation){
log.info("try get version from folder", f);
let version = this.remoteVersionService.getVersionDetails(f);
let version = await this.remoteVersionService.getVersionDetails(f);
if(version){ version = this.getCustomVersions().find(v => v.BSVersion === version.BSVersion && v.name === version.name) ?? version; }
else { version = this.getCustomVersions().find(v => {
const [version, ...rest] = f.split("-");
@@ -125,13 +148,13 @@ export class BSLocalVersionService{
})
}
version && versions.push(version);
});
};
this.setCustomVersions(versions.filter(v => !!v.name || !!v.color));
return versions;
}
public async deleteVersion(version: BSVersion): Promise<boolean>{
if(version.steam){ return false; }
if(version.steam || version.oculus){ return false; }
const versionFolder = await this.getVersionPath(version);
if(!this.utilsService.pathExist(versionFolder)){ return true; }
@@ -141,9 +164,9 @@ export class BSLocalVersionService{
}
public async editVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
if(version.steam){ throw {title: "CantEditSteam", msg: "CantEditSteam"} as BsmException; }
if(version.steam || version.oculus){ throw {title: "CantEditSteam", msg: "CantEditSteam"} as BsmException; }
const oldPath = await this.getVersionPath(version);
const editedVersion: BSVersion = version.BSVersion === name
const editedVersion: BSVersion = version.BSVersion === name
? {...version, name: undefined, color}
: {...version, name: this.removeSpecialChar(name), color};
const newPath = await this.getVersionPath(editedVersion);
@@ -167,9 +190,9 @@ export class BSLocalVersionService{
public async cloneVersion(version: BSVersion, name: string, color: string): Promise<BSVersion>{
const originPath = await this.getVersionPath(version);
const cloneVersion: BSVersion = version.BSVersion === name
const cloneVersion: BSVersion = version.BSVersion === name
? {...version, name: undefined, color}
: {...version, name: this.removeSpecialChar(name), color, steam: false};
: {...version, name: this.removeSpecialChar(name), color, steam: false, oculus: false};
const newPath = await this.getVersionPath(cloneVersion);
if(originPath === newPath){
@@ -180,12 +203,13 @@ export class BSLocalVersionService{
if(this.utilsService.pathExist(newPath)){ throw {title: "VersionAlreadExist"} as BsmException; }
return fs.copy(originPath, newPath).then(() => {
return fs.copy(originPath, newPath, {dereference: true}).then(() => {
this.addCustomVersion(cloneVersion);
return cloneVersion;
}).catch((err: Error) => {
log.error("CLONE", err, version);
throw {title: "CantClone", error: err} as BsmException
})
}
}
}
+14 -13
View File
@@ -16,7 +16,7 @@ export class BSVersionLibService{
private utilsService: UtilsService;
private requestService: RequestService
private bsVersions: BSVersion[] = [];
private bsVersions: BSVersion[];
private constructor(){
this.utilsService = UtilsService.getInstance();
@@ -44,25 +44,26 @@ export class BSVersionLibService{
writeFileSync(localVersionsPath, JSON.stringify(versions, null, "\t"), {encoding: 'utf-8', flag: 'w'});
};
private async loadBsVersions(): Promise<BSVersion[]>{
const [localVersions, remoteVersions] = await Promise.all([
this.getLocalVersions(), (await isOnline({timeout: 1500}) && this.getRemoteVersions())
]);
let resVersions = localVersions;
if(remoteVersions && remoteVersions.length){ resVersions = remoteVersions; this.updateLocalVersions(resVersions); }
this.bsVersions = resVersions;
return this.bsVersions;
private async loadBsVersions(): Promise<BSVersion[]>{
if(this.bsVersions){ return this.bsVersions; }
const [localVersions, remoteVersions] = await Promise.all([
this.getLocalVersions(), (await isOnline({timeout: 1500}) && this.getRemoteVersions())
]);
let resVersions = localVersions;
if(remoteVersions && remoteVersions.length){ resVersions = remoteVersions; this.updateLocalVersions(resVersions); }
this.bsVersions = resVersions;
return this.bsVersions;
}
public async getAvailableVersions(): Promise<BSVersion[]>{
if(this.bsVersions && this.bsVersions.length){ return this.bsVersions; }
const bsVersions = await this.loadBsVersions();
if(!bsVersions || !bsVersions.length){ return []; }
return bsVersions;
}
public getVersionDetails(version: string): BSVersion{
return this.bsVersions.find(v => v.BSVersion === version);
}
public async getVersionDetails(version: string): Promise<BSVersion>{
const versions = await this.getAvailableVersions();
return versions.find(v => v.BSVersion === version);
}
}
+20 -6
View File
@@ -1,23 +1,37 @@
import ElectronStore from "electron-store";
import { InstallationLocationService } from "./installation-location.service";
export class ConfigurationService {
private static instance: ConfigurationService;
private readonly _store: ElectronStore;
public static getInstance(): ConfigurationService{
if(!ConfigurationService.instance){ ConfigurationService.instance = new ConfigurationService(); }
return ConfigurationService.instance;
}
private readonly locations: InstallationLocationService;
private store: ElectronStore;
private constructor(){
this._store = new ElectronStore();
this.locations = InstallationLocationService.getInstance();
this.initStore();
this.locations.onInstallLocationUpdate(() => this.initStore());
}
public get store(): ElectronStore{ return this._store; }
private initStore(){
const contentPath = this.locations.installationDirectory;
this.store = new ElectronStore({
cwd: contentPath,
name: "config",
fileExtension: "cfg",
});
}
public set(key: string, value: any): void{
public set(key: string, value: unknown): void{
this.store.set(key, value);
}
@@ -29,4 +43,4 @@ export class ConfigurationService {
this.store.delete(key);
}
}
}
+99
View File
@@ -0,0 +1,99 @@
import { app } from "electron";
import path from "path";
import { URL } from "url";
import log from "electron-log"
export class DeepLinkService {
// See docs : https://www.electronjs.org/docs/latest/tutorial/launch-app-from-url-in-another-app#main-process-mainjs
private static instance: DeepLinkService;
public static getInstance(): DeepLinkService{
if(!DeepLinkService.instance){ DeepLinkService.instance = new DeepLinkService(); }
return DeepLinkService.instance;
}
private readonly listeners = new Map<string, Listerner[]>()
private constructor(){}
public registerDeepLink(protocol: string): boolean{
if(process.defaultApp && process.argv.length >= 2){
return app.setAsDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])]);
}
return app.setAsDefaultProtocolClient(protocol);
}
public unRegisterDeepLink(protocol: string): boolean{
if(process.defaultApp && process.argv.length >= 2) {
return app.removeAsDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])]);
}
return app.removeAsDefaultProtocolClient(protocol);
}
public isDeepLinkRegistred(protocol: string): boolean{
if(process.defaultApp && process.argv.length >= 2) {
return app.isDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])]);
}
return app.isDefaultProtocolClient(protocol);
}
public addLinkOpenedListener(protocol: string, fn: Listerner){
if(!this.listeners.has(protocol)){
this.listeners.set(protocol, [] as Listerner[]);
}
this.listeners.get(protocol).push(fn);
}
public removeLinkOpenedListener(protocol: string, fn: Listerner){
if(!this.listeners.get(protocol)?.length){ return; }
const listeners = this.listeners.get(protocol);
const fnIndex = listeners.findIndex(listener => listener === fn);
if(fnIndex < 0){ return; }
listeners.splice(fnIndex, 1);
}
public dispatchLinkOpened(link: string){
log.info("DEISPATCH", link);
const url = new URL(link);
const protocolListeners = this.listeners.get(url.protocol.replace(":", "")) ?? [];
protocolListeners.forEach(listerner => {
listerner(link);
});
}
public isDeepLink(link: string): boolean{
try{
const url = new URL(link);
const protocol = url.protocol.replace(":", "");
return Array.from(this.listeners.keys()).some(key => key === protocol);
}
catch(e){
return false;
}
}
}
type Listerner = (link: string) => void;
@@ -1,43 +1,51 @@
import path from "path";
import { ConfigurationService } from "./configuration.service";
import { UtilsService } from "./utils.service";
import fs from 'fs-extra';
import log from "electron-log";
import { app } from "electron";
import { BsmException } from "shared/models/bsm-exception.model";
import ElectronStore from "electron-store";
export class InstallationLocationService {
private static instance: InstallationLocationService;
public static getInstance(): InstallationLocationService{
if(!InstallationLocationService.instance){ InstallationLocationService.instance = new InstallationLocationService(); }
return InstallationLocationService.instance;
}
private readonly INSTALLATION_FOLDER = "BSManager";
private readonly VERSIONS_FOLDER = "BSInstances";
private readonly SHARED_CONTENT_FOLDER = "SharedContent";
private readonly SHARED_MAPS_FOLDER = "SharedMaps";
private readonly STORE_INSTALLATION_PATH_KEY = "installation-folder";
private readonly configService: ConfigurationService;
private readonly utilsService: UtilsService;
private readonly installPathConfig: ElectronStore;
private readonly updateListeners: Set<Listener> = new Set();
private _installationDirectory: string;
public static getInstance(): InstallationLocationService{
if(!InstallationLocationService.instance){ InstallationLocationService.instance = new InstallationLocationService(); }
return InstallationLocationService.instance;
}
private constructor(){
this.configService = ConfigurationService.getInstance();
this.installPathConfig = new ElectronStore({watch: true});
this.utilsService = UtilsService.getInstance();
this.initInstallationLocation();
this.installPathConfig.onDidChange(this.STORE_INSTALLATION_PATH_KEY, () => {
this.triggerListeners();
})
}
private initInstallationLocation(): void{
this._installationDirectory = this.configService.get<string>(this.STORE_INSTALLATION_PATH_KEY) || app.getPath("documents");
this._installationDirectory = this.installPathConfig.get<string>(this.STORE_INSTALLATION_PATH_KEY) as string || app.getPath("documents");
}
public get installationDirectory(): string{ return path.join(this._installationDirectory, this.INSTALLATION_FOLDER); }
public get versionsDirectory(): string { return path.join(this.installationDirectory, this.VERSIONS_FOLDER); }
private triggerListeners(): void{
this.updateListeners.forEach(listener => listener());
}
public setInstallationDirectory(newDir: string): Promise<string>{
const oldDir = this.installationDirectory;
@@ -46,14 +54,26 @@ export class InstallationLocationService {
if(!this.utilsService.pathExist(oldDir)){ this.utilsService.createFolderIfNotExist(oldDir); }
fs.move(oldDir, newDest, { overwrite: true }).then(() => {
this._installationDirectory = newDir;
this.configService.store.set(this.STORE_INSTALLATION_PATH_KEY, newDir);
this.installPathConfig.set(this.STORE_INSTALLATION_PATH_KEY, newDir);
resolve(this.installationDirectory);
}).catch((err: Error) => {
reject({title: "CantMoveFolder", error: err} as BsmException);
log.error(err);
})
})
});
}
}
public onInstallLocationUpdate(fn: Listener){
this.updateListeners.add(fn);
}
public get installationDirectory(): string{ return path.join(this._installationDirectory, this.INSTALLATION_FOLDER); }
public get versionsDirectory(): string { return path.join(this.installationDirectory, this.VERSIONS_FOLDER); }
public get sharedContentPath(): string { return path.join(this.installationDirectory, this.SHARED_CONTENT_FOLDER); }
public get sharedMapsPath(): string { return path.join(this.sharedContentPath, this.SHARED_MAPS_FOLDER); }
}
type Listener = () => void;
-47
View File
@@ -1,47 +0,0 @@
import path from "path";
import { BSVersion } from "shared/bs-version.interface";
import { BSLocalVersionService } from "./bs-local-version.service";
import archiver from "archiver";
import { createWriteStream } from "fs";
import log from "electron-log";
export class MapService{
private readonly MAP_PATH = path.join("Beat Saber_Data", "CustomLevels");
private static instance: MapService;
private readonly localVersionService: BSLocalVersionService;
public static getInstance(): MapService{
if(!MapService.instance){ MapService.instance = new MapService(); }
return MapService.instance;
}
private constructor(){
this.localVersionService = BSLocalVersionService.getInstance();
}
private getMapsPath(versionPath: string): string{
return path.join(versionPath, this.MAP_PATH);
}
public async exportVersionMaps(version: BSVersion, outputZip: string): Promise<void>{
const versionPath = await this.localVersionService.getVersionPath(version);
const mapsPath = this.getMapsPath(versionPath);
const output = createWriteStream(outputZip);
const archive = archiver("zip", {zlib: {level: 9}});
const promise = new Promise<void>((resolve, reject) => {
archive.pipe(output);
archive.directory(mapsPath, false);
archive.on("error", reject);
output.on("close", resolve);
archive.finalize()
});
return promise;
}
}
+27
View File
@@ -0,0 +1,27 @@
import { Notification } from "electron";
import { SystemNotificationOptions } from "shared/models/notification/system-notification.model";
import { UtilsService } from "./utils.service";
export class NotificationService {
private static instance: NotificationService;
public static getInstance(): NotificationService{
if(!NotificationService.instance){ NotificationService.instance = new NotificationService(); }
return NotificationService.instance;
}
private readonly APP_ICON: string;
private readonly utils: UtilsService;
private constructor(){
this.utils = UtilsService.getInstance();
this.APP_ICON = this.utils.getAssetsPath("favicon.ico");
}
public notify(options: SystemNotificationOptions){
new Notification({...options, icon: this.APP_ICON}).show();
}
}
+63
View File
@@ -0,0 +1,63 @@
import { UtilsService } from "./utils.service";
import regedit from 'regedit'
import path from "path";
export class OculusService {
private static instance: OculusService;
private readonly utils: UtilsService;
private oculusPaths: string[];
public static getInstance(): OculusService{
if(!OculusService.instance){ OculusService.instance = new OculusService(); }
return OculusService.instance;
}
private constructor(){
this.utils = UtilsService.getInstance();
}
public oculusRunning(): boolean{
return this.utils.taskRunning("OculusClient.exe");
}
public async getOculusLibsPath(): Promise<string[]>{
if(this.oculusPaths){ return this.oculusPaths; }
const oculusLibsRegKey = "HKCU\\SOFTWARE\\Oculus VR, LLC\\Oculus\\Libraries";
const libsRegData = (await regedit.promisified.list([oculusLibsRegKey]))["HKCU\\SOFTWARE\\Oculus VR, LLC\\Oculus\\Libraries"];
if(!libsRegData.exists || !libsRegData.keys){ return null; }
const libsPath = (await Promise.all(libsRegData.keys.map(async key => {
const originalPath = (await regedit.promisified.list([`${oculusLibsRegKey}\\${key}`]))[`${oculusLibsRegKey}\\${key}`];
if(!originalPath.exists || !libsRegData.values || !originalPath.values["OriginalPath"]){ return null; }
return originalPath.values["OriginalPath"].value as string;
}, []))).filter(path => !!path);
this.oculusPaths = libsPath;
return libsPath;
}
public async getGameFolder(gameFolder: string): Promise<string>{
const libsFolders = await this.getOculusLibsPath();
const rootLibDir = "Software"
for(const lib of libsFolders){
const gameFullPath = path.join(lib, rootLibDir, gameFolder);
if(this.utils.pathExist(gameFullPath)){ return gameFullPath; }
}
return null;
}
}
+1
View File
@@ -27,6 +27,7 @@ export class RequestService {
public downloadFile(url: string, dest: string): Promise<string>{
return new Promise((resolve, reject) => {
const file = createWriteStream(dest);
get(url, res => {
res.pipe(file);
+10
View File
@@ -3,6 +3,7 @@ import regedit from 'regedit'
import path from "path";
import { parse } from "@node-steam/vdf";
import { readFile } from "fs/promises";
import { spawn } from "child_process";
export class SteamService{
@@ -62,4 +63,13 @@ export class SteamService{
return null;
}
public openSteam(): Promise<boolean>{
const process = spawn("start", ["steam://open/games"], {shell: true});
return new Promise(resolve => {
process.on("exit", () => resolve(true));
process.on("error", () => resolve(false));
});
}
}
@@ -0,0 +1,130 @@
import { ApiResult } from "renderer/models/api/api.model";
import { BsvMapDetail } from "shared/models/maps";
import { BsvPlaylist, BsvPlaylistPage, MapFilter, SearchParams, SearchResponse } from "shared/models/maps/beat-saver.model";
import fetch from "node-fetch"
export class BeatSaverApiService {
private static instance: BeatSaverApiService;
public static getInstance(): BeatSaverApiService{
if(!BeatSaverApiService.instance){ BeatSaverApiService.instance = new BeatSaverApiService(); }
return BeatSaverApiService.instance;
}
private readonly bsaverApiUrl = "https://beatsaver.com/api"
private constructor(){}
private mapFilterToUrlParams(filter: MapFilter): URLSearchParams{
if(!filter){ return new URLSearchParams(); }
const enbledTagsString = filter.enabledTags ? Array.from(filter.enabledTags) : null;
const excludedTagsString = filter.excludedTags ? Array.from(filter.excludedTags).map(tag => `!${tag}`) : null;
const tags = (enbledTagsString || excludedTagsString) ? [...enbledTagsString, excludedTagsString].join("|") : null;
const params = {
...(filter.automapper && {automapper: String(filter.automapper)}),
...(filter.chroma && {chroma: String(filter.chroma)}),
...(filter.cinema && {cinema: String(filter.cinema)}),
...(filter.me && {me: String(filter.me)}),
...(filter.noodle && {noodle: String(filter.noodle)}),
...(filter.ranked && {ranked: String(filter.ranked)}),
...(filter.verified && {verified: String(filter.verified)}),
...(filter.curated && {curated: String(filter.curated)}),
...(filter.fullSpread && {fullSpread: String(filter.fullSpread)}),
...(filter.from && {from: String(filter.from)}),
...(filter.to && {to: String(filter.to)}),
...(tags && {tags: String(tags)}),
...(filter.minDuration && {minDuration: String(filter.minDuration)}),
...(filter.maxDuration && {maxDuration: String(filter.maxDuration)}),
...(filter.minNps && {minNps: String(filter.minNps)}),
...(filter.maxNps && {maxNps: String(filter.maxNps)})
};
return new URLSearchParams(params);
}
private searchParamsToUrlParams(search: SearchParams): URLSearchParams{
if(!search){ return new URLSearchParams(); }
const searchParams = {
...(search.includeEmpty && {includeEmpty: String(search.includeEmpty)}),
...(search.sortOrder && {sortOrder: search.sortOrder}),
...(search.q && {q: search.q})
};
const filterUrlParms = this.mapFilterToUrlParams(search.filter);
return new URLSearchParams({
...searchParams,
...Object.fromEntries(filterUrlParms)
});
}
public async getMapsDetailsByHashs<T extends string>(hashs: T[]): Promise<ApiResult<Record<Lowercase<T>, BsvMapDetail>>>{
if(hashs.length > 50){ throw "too musch map hashs"; }
const paramsHashs = hashs.join(",");
const resp = await fetch(`${this.bsaverApiUrl}/maps/hash/${paramsHashs}`);
const data = await resp.json() as Record<Lowercase<T>, BsvMapDetail> | BsvMapDetail;
if((data as BsvMapDetail).id){
const key = (data as BsvMapDetail).versions.at(0).hash.toLowerCase();
const parsedData = {
[key]: data as BsvMapDetail
} as Record<Lowercase<T>, BsvMapDetail>;
return {status: resp.status, data: parsedData}
}
return {status: resp.status, data: (data as Record<Lowercase<T>, BsvMapDetail>)};
}
public async getMapDetailsById(id: string): Promise<ApiResult<BsvMapDetail>>{
const res = await fetch(`${this.bsaverApiUrl}/maps/id/${id}`);
const data = await res.json() as BsvMapDetail;
return {status: res.status, data};
}
public async searchMaps(search: SearchParams): Promise<ApiResult<SearchResponse>>{
const url = new URL(`${this.bsaverApiUrl}/search/text/${search?.page ?? 0}`);
url.search = this.searchParamsToUrlParams(search).toString();
const res = await fetch(url.toString());
if(!res.ok){
return {status: res.status, data: null};
}
const data: any = await res.json();
return {status: res.status, data};
}
public async getPlaylistDetails(id: string): Promise<ApiResult<BsvPlaylist>>{
const res = await fetch(`${this.bsaverApiUrl}/playlists/id/${id}/0`);
const data = await res.json() as BsvPlaylistPage;
return {status: res.status, data: data.playlist};
}
}
@@ -0,0 +1,76 @@
import { splitIntoChunk } from "../../../helpers/array-tools";
import { BsvMapDetail } from "shared/models/maps";
import { BsvPlaylist, SearchParams } from "shared/models/maps/beat-saver.model";
import { BeatSaverApiService } from "./beat-saver-api.service";
export class BeatSaverService {
private static instance: BeatSaverService;
public static getInstance(): BeatSaverService{
if(!BeatSaverService.instance){ BeatSaverService.instance = new BeatSaverService(); }
return BeatSaverService.instance;
}
private readonly bsaverApi: BeatSaverApiService;
private readonly cachedMapsDetails = new Map<string, BsvMapDetail>();
private constructor(){
this.bsaverApi = BeatSaverApiService.getInstance();
}
public async getMapDetailsFromHashs(hashs: string[]): Promise<BsvMapDetail[]>{
const filtredHashs = hashs.map(h => h.toLowerCase()).filter(hash => !Array.from(this.cachedMapsDetails.keys()).includes(hash));
const chunkHash = splitIntoChunk(filtredHashs, 50);
const mapDetails = Array.from(this.cachedMapsDetails.entries()).reduce((res , [hash, details]) => {
if(hashs.includes(hash)){
res.push(details);
}
return res;
}, [] as BsvMapDetail[]);
for(const hashs of chunkHash){
const res = await this.bsaverApi.getMapsDetailsByHashs(hashs);
if(res.status === 200){
mapDetails.push(...Object.values<BsvMapDetail>(res.data).filter(detail => !!detail));
mapDetails.forEach(detail => {
this.cachedMapsDetails.set(detail.versions.at(0).hash.toLowerCase(), detail);
});
}
}
return mapDetails;
}
public async getMapDetailsById(id: string): Promise<BsvMapDetail>{
const res = await this.bsaverApi.getMapDetailsById(id);
return res.data;
}
public searchMaps(search: SearchParams): Promise<BsvMapDetail[]>{
return this.bsaverApi.searchMaps(search).then(res => {
return res.status === 200 ? res.data.docs : [];
}).catch(err => {
return [];
});
}
public async getPlaylistPage(id: string): Promise<BsvPlaylist>{
const res = await this.bsaverApi.getPlaylistDetails(id);
return res.data;
}
}
@@ -0,0 +1,70 @@
import fetch from "node-fetch";
import { ApiResult } from "renderer/models/api/api.model";
import { MSGetQuery, MSGetQueryFilter, MSGetResponse } from "shared/models/model-saber/model-saber.model";
export class ModelSaberApiService {
private static instance: ModelSaberApiService;
public static getInstance(): ModelSaberApiService{
if(!ModelSaberApiService.instance){ ModelSaberApiService.instance = new ModelSaberApiService(); }
return ModelSaberApiService.instance;
}
private readonly API_URL = "https://modelsaber.com/api/v2/";
private readonly ENDPOINTS = {get: "get.php", types: "types.php"};
private constructor(){}
private parseFilters(filters: MSGetQueryFilter[]): string{
if(!filters){ return null; }
const parsed = filters.map(filter => {
const stringFilter = filter.type === "searchName" ? filter.value : `${filter.type}:${filter.value}`;
return filter.isNegative ? `-${stringFilter}` : stringFilter;
});
return parsed.join(",");
}
private buildUrlQuery(query: MSGetQuery): URLSearchParams{
if(!query){ return new URLSearchParams(); }
const filterQuery = this.parseFilters(query.filter);
const searchParams = {
...(query.type && {type: query.type}),
...(query.platform && {platform: query.platform}),
...(query.start && {start: `${query.start}`}),
...(query.end && {end: `${query.end}`}),
...(query.sort && {sort: query.sort}),
...(query.sortDirection && {sortDirection: query.sortDirection}),
...(filterQuery && {filter: filterQuery}),
}
return new URLSearchParams(searchParams);
}
public async searchModel(query: MSGetQuery): Promise<ApiResult<MSGetResponse>>{
const url = new URL(this.ENDPOINTS.get, this.API_URL);
url.search = this.buildUrlQuery(query).toString();
const res = await fetch(url.toString());
if(!res.ok){
return {data: null, status: res.status};
}
const data = await res.json() as MSGetResponse;
return {data, status: res.status};
}
}
@@ -0,0 +1,45 @@
import { MSGetQuery, MSGetQueryFilter, MSModel } from "shared/models/model-saber/model-saber.model";
import { ModelSaberApiService } from "./model-saber-api.service";
export class ModelSaberService {
private static instance: ModelSaberService;
public static getInstance(): ModelSaberService{
if(!ModelSaberService.instance){ ModelSaberService.instance = new ModelSaberService(); }
return ModelSaberService.instance;
}
private readonly modelSaberApi: ModelSaberApiService;
private constructor(){
this.modelSaberApi = ModelSaberApiService.getInstance();
}
public async getModelById(id: number|string): Promise<MSModel>{
const query: MSGetQuery = {
start: 0,
end: 1,
platform: "pc",
filter: [{type: "id", value: id}]
}
try{
const res = await this.modelSaberApi.searchModel(query);
if(res.status !== 200){ return null; }
if(Object.keys(res.data).length === 0){
return null;
}
return res.data[`${id}`];
}
catch(e){
return null;
}
}
}
+28 -10
View File
@@ -1,4 +1,5 @@
import { existsSync, mkdirSync, readdirSync, readFile, unlinkSync } from "fs";
import { existsSync, mkdirSync, readdirSync, readFile, rmSync } from "fs";
import { moveSync } from "fs-extra"
import { spawnSync } from "child_process";
import { homedir } from "os";
import path from "path";
@@ -6,6 +7,9 @@ import { app, BrowserWindow } from "electron";
import { rm, unlink } from "fs/promises";
import { IpcResponse } from "shared/models/ipc";
import log from "electron-log";
import { AppWindow } from "shared/models/window-manager/app-window.model";
// TODO : REFACTOR
export class UtilsService{
@@ -13,7 +17,7 @@ export class UtilsService{
private assetsPath: string = '';
private mainWindow: BrowserWindow;
private windows: Map<AppWindow, BrowserWindow> = new Map<AppWindow, BrowserWindow>();
private constructor(){}
@@ -29,8 +33,8 @@ export class UtilsService{
public getAssestsJsonsPath(): string { return this.getAssetsPath("jsons"); }
public getTempPath(): string{ return path.join(app.getPath("temp"), app.getName()) }
public setMainWindow(win: BrowserWindow){ this.mainWindow = win; }
public getMainWindow(){ return this.mainWindow; }
public setMainWindows(windows: Map<AppWindow, BrowserWindow>){ this.windows = windows; }
public getMainWindows(win: AppWindow){ return this.windows.get(win); }
public pathExist(path: string): boolean{ return existsSync(path); }
@@ -43,7 +47,7 @@ export class UtilsService{
return unlink(pathToFile);
}
public rmDirIfExist(path: string): Promise<void>{
public async rmDirIfExist(path: string): Promise<void>{
if(!this.pathExist(path)){ return; }
return rm(path, {recursive: true, force: true});
}
@@ -68,19 +72,33 @@ export class UtilsService{
});
}
public listDirsInDir(dirPath: string): string[]{
public listDirsInDir(dirPath: string, fullPath = false): string[]{
let files = readdirSync(dirPath, { withFileTypes:true});
files = files.filter(f => f.isDirectory())
return files.map(f => f.name);
return files.map(f => fullPath ? path.join(dirPath, f.name) : f.name);
}
public deleteFolder(folderPath: string): Promise<void>{
return rm(folderPath, {recursive: true});
public async deleteFolder(folderPath: string): Promise<void>{
const folderExist = this.pathExist(folderPath);
if(!folderExist){ return; }
return rmSync(folderPath, {recursive: true});
}
public async moveDirContent(src: string, dest: string, overwrite = false): Promise<void>{
const [srcExist, destExist] = await Promise.all([this.pathExist(src), this.pathExist(dest)]);
if(!srcExist){ return; }
if(!destExist){ await this.createFolderIfNotExist(dest); }
readdirSync(src, {encoding: "utf-8"}).forEach(file => {
const srcFullPath = path.join(src, file);
const destFullPath = path.join(dest, file);
if(!overwrite && this.pathExist(destFullPath)){ return; }
moveSync(srcFullPath, destFullPath, {overwrite});
});
}
public ipcSend<T = any>(channel: string, response: IpcResponse<T>): void{
try {
this.mainWindow.webContents.send(channel, response);
Array.from(this.windows.values()).forEach(window => window.webContents.send(channel, response));
} catch (error) {
log.error(error);
}
+25 -11
View File
@@ -2,25 +2,32 @@ import { app, BrowserWindow, BrowserWindowConstructorOptions } from "electron";
import { resolveHtmlPath } from "../util";
import { UtilsService } from "./utils.service";
import { AppWindow } from "shared/models/window-manager/app-window.model";
import { PRELOAD_PATH } from "../main";
import path from "path";
import { APP_NAME } from "../constants";
export class WindowManagerService{
private static instance: WindowManagerService;
private readonly PRELOAD_PATH = app.isPackaged ? path.join(__dirname, 'preload.js') : path.join(__dirname, '../../../.erb/dll/preload.js')
private readonly utilsService: UtilsService = UtilsService.getInstance();
private readonly appWindowsOptions: Record<AppWindow, BrowserWindowConstructorOptions> = {
"launcher.html": {width: 380, height: 500, minWidth: 380, minHeight: 500, resizable: false},
"index.html": {width: 1080, height: 720, minWidth: 900, minHeight: 500}
"index.html": {width: 1080, height: 720, minWidth: 900, minHeight: 500},
"oneclick-download-map.html": {width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false},
"oneclick-download-playlist.html": {width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false},
"oneclick-download-model.html": {width: 350, height: 400, minWidth: 350, minHeight: 400, resizable: false},
}
private readonly baseWindowOption: BrowserWindowConstructorOptions = {
title: APP_NAME,
icon: this.utilsService.getAssetsPath("favicon.ico"),
show: false,
frame: false,
titleBarOverlay: false,
webPreferences: { preload: PRELOAD_PATH }
webPreferences: { preload: this.PRELOAD_PATH, webSecurity: false }
}
private readonly windows: Map<AppWindow, BrowserWindow> = new Map<AppWindow, BrowserWindow>();
@@ -32,15 +39,16 @@ export class WindowManagerService{
private constructor(){}
public openWindow(windowType: AppWindow): Promise<BrowserWindow>{
const window = new BrowserWindow({...this.appWindowsOptions[windowType], ...this.baseWindowOption});
public openWindow(windowType: AppWindow, options?: BrowserWindowConstructorOptions): Promise<BrowserWindow>{
const window = new BrowserWindow({...this.appWindowsOptions[windowType], ...this.baseWindowOption, ...options});
const promise = window.loadURL(resolveHtmlPath(windowType));
window.removeMenu();
window.setMenu(null);
window.once("ready-to-show", () => {
if (!window) { throw new Error('"window" is not defined'); }
return window.show();
window.show();
});
window.once("closed", () => {
@@ -49,15 +57,11 @@ export class WindowManagerService{
});
this.windows.set(windowType, window);
this.utilsService.setMainWindow(window);
this.utilsService.setMainWindows(this.windows);
return promise.then(() => window);
}
public closeWindow(window: AppWindow){
this.windows.get(window).close();
}
public closeAllWindows(except?: AppWindow){
this.windows.forEach((window, key) => {
if(key === except){ return; }
@@ -65,4 +69,14 @@ export class WindowManagerService{
})
}
public close(...win: AppWindow[]){
win.forEach(window => {
this.windows.get(window)?.close();
});
}
public getWindows(window: AppWindow): BrowserWindow{
return this.windows.get(window);
}
}
-49
View File
@@ -1,49 +0,0 @@
import { NavBar } from "./components/nav-bar/nav-bar.component";
import TitleBar from "./components/title-bar/title-bar.component";
import { Routes, Route, useLocation } from "react-router-dom";
import { AvailableVersionsList } from "./pages/available-versions-list.components";
import { VersionViewer } from "./pages/version-viewer.component";
import { Modal } from "./components/modal/modal.component";
import { SettingsPage } from "./pages/settings-page.component";
import { BsmProgressBar } from "./components/progress-bar/bsm-progress-bar.component";
import { useEffect } from "react";
import { ThemeService } from "./services/theme.service";
import { NotificationOverlay } from "./components/notification/notification-overlay.component";
import { PageStateService } from "./services/page-state.service";
export default function App() {
const themeService = ThemeService.getInstance();
const pageState = PageStateService.getInstance();
const location = useLocation();
pageState.setState(location.state);
useEffect(() => {
themeService.theme$.subscribe(() => {
if(themeService.isDark || (themeService.isOS && window.matchMedia('(prefers-color-scheme: dark)').matches)){ return document.documentElement.classList.add('dark'); }
document.documentElement.classList.remove('dark');
});
}, []);
return (
<div className="relative w-screen h-screen overflow-hidden flex bg-light-main-color-1 dark:bg-main-color-1 z-0 max-w-full">
<Modal/>
<NavBar/>
<NotificationOverlay/>
<div className="relative flex flex-col grow max-w-full min-w-0">
<TitleBar/>
<div className="bg-light-main-color-2 dark:bg-main-color-2 relative rounded-tl-lg grow overflow-hidden max-w-full">
<Routes>
<Route path={"/bs-version/:versionNumber"} element={<VersionViewer/>}/>
<Route path={"/settings"} element={<SettingsPage/>}/>
<Route path="*" element={<AvailableVersionsList/>}/>
</Routes>
<BsmProgressBar/>
</div>
</div>
</div>
);
}
@@ -16,10 +16,6 @@ export function AvailableVersionsSlide(props: {year: string}) {
});
}, [])
//w-full max-w-full max-h-full flex items-start justify-center overflow-x-hidden overflow-y-scroll content-start scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900
//relative left-[2px] flex justify-center items-start content-start flex-wrap max-w-6xl
return (
<ol className="w-full flex items-start justify-center gap-6 shrink-0 content-start flex-wrap p-4 overflow-x-hidden overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
{availableVersions.map((version, index) =>
@@ -23,10 +23,10 @@ export function AvailableVersionsSlider() {
return (
<div className="w-full h-fit max-h-full flex flex-col items-center grow min-h-0">
<TabNavBar className="mb-3" tabsText={availableYears} onTabChange={setSelectedYear}/>
<TabNavBar className="mb-3" tabIndex={yearIndex} tabsText={availableYears} onTabChange={setSelectedYear}/>
<ol className="w-full min-h-0 flex transition-transform duration-300" style={{transform: `translate(${-(yearIndex * 100)}%, 0)`}}>
{ availableYears.map((year, index) =>
<AvailableVersionsSlide key={index} year={year}></AvailableVersionsSlide>
{ availableYears.map(year =>
<AvailableVersionsSlide key={year} year={year}/>
)}
</ol>
</div>
@@ -0,0 +1,183 @@
import { MapFilter, MapSpecificity, MapStyle, MapTag, MapType } from "shared/models/maps/beat-saver.model"
import {motion} from "framer-motion"
import { MutableRefObject} from "react"
import { MAP_TYPES } from "renderer/partials/maps/map-tags/map-types"
import { MAP_STYLES } from "renderer/partials/maps/map-tags/map-styles"
import { BsmCheckbox } from "../shared/bsm-checkbox.component"
import { min_to_s } from "renderer/helpers/time-utils"
import dateFormat from "dateformat"
import { BsmRange } from "../shared/bsm-range.component"
import { useTranslation } from "renderer/hooks/use-translation.hook"
import { MAP_SPECIFICITIES } from "renderer/partials/maps/map-general/map-specificity"
import { MAP_REQUIREMENTS } from "renderer/partials/maps/map-requirements/map-requirements"
import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties/map-difficulties-colors"
export type Props = {
className?: string,
ref?: MutableRefObject<undefined>
playlist?: boolean,
filter: MapFilter
onChange?: (filter: MapFilter) => void
}
export function FilterPanel({className, ref, playlist = false, filter, onChange}: Props) {
const t = useTranslation();
const MIN_NPS = 0;
const MAX_NPS = 17;
const MIN_DURATION = 0;
const MAX_DURATION = min_to_s(30);
const npss = [filter?.minNps || MIN_NPS, filter?.maxNps || MAX_NPS];
const durations = [filter?.minDuration || MIN_DURATION, filter?.maxDuration || MAX_DURATION];
const isTagActivated = (tag: MapTag): boolean => filter?.enabledTags?.has(tag) || filter?.excludedTags?.has(tag);
const isTagExcluded = (tag: MapTag): boolean => filter?.excludedTags?.has(tag);
const renderDurationLabel = (sec: number): JSX.Element => {
const textValue = (() => {
if(sec === MIN_DURATION){ return t("maps.map-filter-panel.duration"); }
if(sec === MAX_DURATION){ return "∞"; }
const date = new Date(0);
date.setSeconds(sec);
return sec > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss");
})();
return renderLabel(textValue, sec === MAX_DURATION);
}
const renderNpsLabel = (nps: number): JSX.Element => {
const textValue = (() => {
if(nps === MIN_NPS){ return "NPS"; }
if(nps === MAX_NPS){ return "∞"; }
return nps;
})();
return renderLabel(textValue, nps === MAX_NPS);
}
const renderLabel = (text: unknown, isMax: boolean): JSX.Element => {
return (
<span className={`bg-inherit absolute top-[calc(100%+4px)] h-5 font-bold rounded-md shadow-center shadow-black px-1 flex items-center ${isMax ? "text-lg" : "text-sm"}`}>
{text}
</span>
)
}
const onNpssChange = ([min, max]: number[]) => {
const newFilter: MapFilter = {...filter, minNps: min, maxNps: max};
if(max === MAX_NPS){
delete newFilter["maxNps"];
}
onChange(newFilter);
}
const onDurationsChange = ([min, max]: number[]) => {
const newFilter: MapFilter = {...filter, minDuration: min, maxDuration: max};
if(max === MAX_DURATION){
delete newFilter["maxDuration"];
}
onChange(newFilter);
}
const handleTagClick = (tag: MapTag) => {
const enabledTags = filter.enabledTags ?? new Set<MapTag>();
const excludedTags = filter.excludedTags ?? new Set<MapTag>();
if(isTagExcluded(tag)){
excludedTags.delete(tag);
}
else if(isTagActivated(tag)){
excludedTags.add(tag);
enabledTags.delete(tag);
}
else{
enabledTags.add(tag);
}
onChange({
...filter,
enabledTags,
excludedTags
});
}
const translateMapType = (type: MapType): string => {
return t(`maps.map-types.${type}`);
}
const translateMapStyle = (style: MapStyle): string => {
return t(`maps.map-styles.${style}`)
}
const translateMapSpecificity = (specificity: MapSpecificity): string => {
return t(`maps.map-specificities.${specificity}`);
}
type BooleanKeys<T> = { [k in keyof T]: T[k] extends boolean ? k : never }[keyof T];
const handleCheckbox = (key: BooleanKeys<MapFilter>) => {
const newFilter = {...(filter ?? {})};
if(newFilter[key] === true){
delete newFilter[key];
}
else{
newFilter[key] = true;
}
onChange(newFilter);
}
return !playlist ? (
<motion.div ref={ref} className={`${className} bg-light-main-color-2 dark:bg-main-color-3`} initial={{opacity: 0}} animate={{opacity: 1}} exit={{opacity: 0}}>
<div className="w-full h-6 grid grid-cols-2 gap-x-12 px-4 mb-6 pt-2">
<BsmRange min={MIN_NPS} max={MAX_NPS} values={npss} onChange={onNpssChange} renderLabel={renderNpsLabel} step={.1}/>
<BsmRange min={MIN_DURATION} max={MAX_DURATION} values={durations} onChange={onDurationsChange} renderLabel={renderDurationLabel} step={5}/>
</div>
<div className="w-full h-full flex gap-x-2">
<section className="shrink-0">
<h2 className="mb-1 uppercase text-sm">{t("maps.map-filter-panel.specificities")}</h2>
{MAP_SPECIFICITIES.map(specificity => (
<div key={specificity} className="flex justify-start items-center h-[22px] z-20 relative py-0.5 cursor-pointer" onClick={() => handleCheckbox(specificity)}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.[specificity]} onChange={() => handleCheckbox(specificity)}/>
<span className="grow capitalize">{translateMapSpecificity(specificity)}</span>
</div>
))}
<h2 className="my-1 uppercase text-sm">{t("maps.map-filter-panel.requirements")}</h2>
{MAP_REQUIREMENTS.map(requirement => (
<div key={requirement} className="flex justify-start items-center h-[22px] z-20 relative py-0.5 cursor-pointer" onClick={() => handleCheckbox(requirement)}>
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1" checked={filter?.[requirement]} onChange={() => handleCheckbox(requirement)}/>
<span className="grow capitalize">{requirement}</span>
</div>
))}
</section>
<section className="grow capitalize">
<h2 className="uppercase text-sm mb-1">{t("maps.map-filter-panel.tags")}</h2>
<div className="w-full flex flex-row flex-wrap items-start justify-start content-start gap-1 mb-2">
{MAP_TYPES.map(tag => (
<span key={tag} onClick={() => handleTagClick(tag)} className={`text-[12.5px] text-black rounded-md px-1 font-bold cursor-pointer ${(!isTagActivated(tag)) && "opacity-40 hover:opacity-90"}`} style={{backgroundColor: isTagExcluded(tag) ? MAP_DIFFICULTIES_COLORS.Expert : MAP_DIFFICULTIES_COLORS.Normal}}>{translateMapType(tag)}</span>
))}
</div>
<div className="w-full flex flex-row flex-wrap items-start justify-start content-start gap-1">
{MAP_STYLES.map(tag => (
<span key={tag} onClick={() => handleTagClick(tag)} className={`text-[12.5px] text-black rounded-md px-1 font-bold cursor-pointer ${(!isTagActivated(tag)) && "opacity-40 hover:opacity-90"}`} style={{backgroundColor: isTagExcluded(tag) ? MAP_DIFFICULTIES_COLORS.Expert : MAP_DIFFICULTIES_COLORS.Easy}}>{translateMapStyle(tag)}</span>
))}
</div>
</section>
</div>
</motion.div>
) : (
<></>
)
}
@@ -0,0 +1,260 @@
import { MapsManagerService } from "renderer/services/maps-manager.service"
import { BSVersion } from "shared/bs-version.interface"
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface"
import { Subscription } from "rxjs"
import { MapItem, ParsedMapDiff } from "./map-item.component"
import { BsvMapCharacteristic, MapFilter } from "shared/models/maps/beat-saver.model"
import { useInView } from "framer-motion"
import { MapsDownloaderService } from "renderer/services/maps-downloader.service"
import { BsmImage } from "../shared/bsm-image.component"
import BeatConflict from "../../../../assets/images/apngs/beat-conflict.png"
import { BsmButton } from "../shared/bsm-button.component"
import { useTranslation } from "renderer/hooks/use-translation.hook"
type Props = {
version: BSVersion,
className?: string,
filter?: MapFilter
search?: string,
}
export const LocalMapsListPanel = forwardRef(({version, className, filter, search} : Props, forwardRef) => {
const mapsManager = MapsManagerService.getInstance();
const mapsDownloader = MapsDownloaderService.getInstance();
const ref = useRef(null)
const isVisible = useInView(ref, {once: true});
const [maps, setMaps] = useState<BsmLocalMap[]>([]);
const [subs] = useState<Subscription[]>([]);
const [selectedMaps, setSelectedMaps] = useState([]);
const t = useTranslation();
useImperativeHandle(forwardRef ,()=>({
deleteMaps(){
const mapsToDelete = selectedMaps.length === 0 ? maps : selectedMaps
mapsManager.deleteMaps(mapsToDelete, version).finally(loadMaps);
},
exportMaps(){
mapsManager.exportMaps(version, selectedMaps)
}
}), [selectedMaps, maps, version]);
useEffect(() => {
if(isVisible){
loadMaps();
subs.push(mapsManager.versionLinked$.subscribe(loadMaps));
subs.push(mapsManager.versionUnlinked$.subscribe(loadMaps));
mapsDownloader.addOnMapDownloadedListener(loadMaps);
}
return () => {
setMaps(() => []);
subs.forEach(s => s.unsubscribe());
mapsDownloader.removeOnMapDownloadedListene(loadMaps);
}
}, [isVisible, version]);
const loadMaps = () => {
subs.push(mapsManager.getMaps(version).subscribe(localMaps => setMaps(() => [...localMaps])));
}
const handleDelete = useCallback((map: BsmLocalMap) => {
mapsManager.deleteMaps([map], version).then(res => res && loadMaps())
}, [version]);
const extractMapDiffs = (map: BsmLocalMap): Map<BsvMapCharacteristic, ParsedMapDiff[]> => {
const res = new Map<BsvMapCharacteristic, ParsedMapDiff[]>();
if(map.bsaverInfo?.versions[0]?.diffs){
map.bsaverInfo.versions[0].diffs.forEach(diff => {
const arr = res.get(diff.characteristic) || [];
const diffName = map.rawInfo._difficultyBeatmapSets.find(set => set._beatmapCharacteristicName === diff.characteristic)._difficultyBeatmaps.find(rawDiff => rawDiff._difficulty === diff.difficulty)?._customData?._difficultyLabel || diff.difficulty
arr.push({name: diffName, type: diff.difficulty, stars: diff.stars});
res.set(diff.characteristic, arr);
});
return res;
}
map.rawInfo._difficultyBeatmapSets.forEach(set => {
set._difficultyBeatmaps.forEach(diff => {
const arr = res.get(set._beatmapCharacteristicName) || [];
arr.push({name: diff._customData?._difficultyLabel || diff._difficulty, type: diff._difficulty, stars: null});
res.set(set._beatmapCharacteristicName, arr);
});
});
return res;
}
const onMapSelected = useCallback((map: BsmLocalMap) => {
const maps = [...selectedMaps];
if(maps.some(selectedMap => selectedMap.hash === map.hash)){
const i = maps.findIndex(selectedMap => selectedMap.hash === map.hash);
maps.splice(i, 1);
}
else{
maps.push(map);
}
setSelectedMaps(() => maps);
}, [selectedMaps]);
const isMapFitFilter = (map: BsmLocalMap): boolean => {
// Can be more clean and optimized i think
const fitEnabledTags = (() => {
if(!filter?.enabledTags || filter.enabledTags.size === 0){ return true; }
if(!map?.bsaverInfo?.tags){ return false; }
return Array.from(filter.enabledTags.values()).every(tag => map.bsaverInfo.tags.some(mapTag => mapTag === tag));
})();
if(!fitEnabledTags){ return false; }
const fitExcluedTags = (() => {
if(!filter?.excludedTags || filter.excludedTags.size === 0){ return true; }
if(!map?.bsaverInfo?.tags){ return true; }
return !map.bsaverInfo.tags.some(tag => filter.excludedTags.has(tag));
})();
if(!fitExcluedTags){ return false; }
const fitMinNps = (() => {
if(!filter?.minNps){ return true; }
if(!map?.bsaverInfo?.versions?.at(0)){ return false; }
return !map.bsaverInfo.versions.some(version => {
return version.diffs.some(diff => diff.nps < filter.minNps);
});
})();
if(!fitMinNps){ return false; }
const fitMaxNps = (() => {
if(!filter?.maxNps){ return true; }
if(!map?.bsaverInfo?.versions?.at(0)){ return false; }
return !map.bsaverInfo.versions.some(version => {
return version.diffs.some(diff => diff.nps > filter.maxNps);
});
})();
if(!fitMaxNps){ return false; }
const fitMinDuration = (() => {
if(!filter?.minDuration){ return true; }
if(!map?.bsaverInfo?.metadata?.duration){ return false; }
return map.bsaverInfo.metadata.duration >= filter.minDuration;
})();
if(!fitMinDuration){ return false; }
const fitMaxDuration = (() => {
if(!filter?.maxDuration){ return true; }
if(!map?.bsaverInfo?.metadata?.duration){ return false; }
return map.bsaverInfo.metadata.duration <= filter.maxDuration;
})();
if(!fitMaxDuration){ return false; }
const fitNoodle = (() => {
if(!filter?.noodle){ return true; }
if(!map?.bsaverInfo?.versions?.at(0)){ return false; }
return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.ne));
})();
if(!fitNoodle){ return false; }
const fitMe = (() => {
if(!filter?.me){ return true; }
if(!map?.bsaverInfo?.versions?.at(0)){ return false; }
return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.me));
})();
if(!fitMe){ return false; }
const fitCinema = (() => {
if(!filter?.cinema){ return true; }
if(!map?.bsaverInfo?.versions?.at(0)){ return false; }
return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.cinema));
})();
if(!fitCinema){ return false; }
const fitChroma =(() => {
if(!filter?.chroma){ return true; }
if(!map?.bsaverInfo?.versions?.at(0)){ return false; }
return map.bsaverInfo.versions.some(version => version.diffs.some(diff => !!diff.chroma));
})();
if(!fitChroma){ return false; }
const fitFullSpread = (() => {
if(!filter?.fullSpread){ return true; }
if(!map?.bsaverInfo?.versions?.at(0)){ return false; }
return map.bsaverInfo.versions.some(version => version?.diffs?.length >= 5);
})();
if(!fitFullSpread){ return false; }
if(!(filter?.automapper ? map.bsaverInfo?.automapper === filter.automapper : true)){ return false }
if(!(filter?.ranked ? map.bsaverInfo?.ranked === filter.ranked : true)){ return false }
if(!(filter?.curated ? !!map.bsaverInfo?.curatedAt : true)){ return false }
if(!(filter?.verified ? !!map.bsaverInfo?.uploader?.verifiedMapper : true)){ return false }
const searchCheck = (() => {
return (
((map.rawInfo?._songName ?? map.bsaverInfo?.name) || "")?.toLowerCase().includes(search.toLowerCase()) ||
((map.rawInfo?._songAuthorName ?? map.bsaverInfo?.metadata?.songAuthorName) || "")?.toLowerCase().includes(search.toLowerCase()) ||
((map.rawInfo?._levelAuthorName ?? map.bsaverInfo?.metadata?.levelAuthorName) || "")?.toLowerCase().includes(search.toLowerCase()));
})();
if(!searchCheck){ return false };
return true;
}
const renderMaps = (): JSX.Element[] => {
return maps.reduce((acc, current) => {
if(isMapFitFilter(current)){
acc.push(renderMapItem(current));
}
return acc
}, []);
}
const renderMapItem = (map: BsmLocalMap) => {
return <MapItem
key={map.hash}
hash={map.hash}
title={map.rawInfo._songName}
coverUrl={map.coverUrl}
songUrl={map.songUrl}
autor={map.rawInfo._levelAuthorName}
songAutor={map.rawInfo._songAuthorName}
bpm={map.rawInfo._beatsPerMinute}
duration={map.bsaverInfo?.metadata?.duration}
selected={selectedMaps.some(_map => _map.hash === map.hash)}
diffs={extractMapDiffs(map)} mapId={map.bsaverInfo?.id} qualified={null} ranked={map.bsaverInfo?.ranked} autorId={map.bsaverInfo?.uploader?.id} likes={map.bsaverInfo?.stats?.upvotes} createdAt={map.bsaverInfo?.createdAt}
onDelete={handleDelete}
onSelected={onMapSelected}
callBackParam={map}
/>;
}
return (
<div ref={ref} className={className}>
<ul className="p-3 w-full grow flex flex-wrap justify-center content-start gap-2 overflow-y-scroll scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900">
{maps?.length ? renderMaps() : (
<div className="h-full flex flex-col items-center justify-center flex-wrap gap-1">
<BsmImage className="h-32" image={BeatConflict}/>
<span className="font-bold">{t("pages.version-viewer.maps.tabs.maps.empty-maps.text")}</span>
<BsmButton className="font-bold rounded-md p-2" text="pages.version-viewer.maps.tabs.maps.empty-maps.button" typeColor="primary" withBar={false} onClick={e => {e.preventDefault(); mapsDownloader.openDownloadMapModal(version)}}/>
</div>
)}
</ul>
</div>
)
})
@@ -0,0 +1,242 @@
import { BsmImage } from "../shared/bsm-image.component";
import { BsvMapCharacteristic, BsvMapDifficultyType } from "shared/models/maps/beat-saver.model"
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { BsmLink } from "../shared/bsm-link.component";
import { BsmIcon } from "../svgs/bsm-icon.component";
import { BsmButton } from "../shared/bsm-button.component";
import { AnimatePresence, motion } from "framer-motion";
import { useState, Fragment, memo, useRef } from "react";
import { LinkOpenerService } from "renderer/services/link-opener.service";
import dateFormat from "dateformat";
import { AudioPlayerService } from "renderer/services/audio-player.service";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { map } from "rxjs/operators";
import useDelayedState from "use-delayed-state";
import equal from "fast-deep-equal/es6";
import { getMapZipUrlFromHash } from "renderer/helpers/maps-utils";
import { BsmBasicSpinner } from "../shared/bsm-basic-spinner/bsm-basic-spinner.component";
import defaultImage from '../../../../assets/images/default-version-img.jpg'
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { MAP_DIFFICULTIES } from "renderer/partials/maps/map-difficulties/map-difficulties";
import { MAP_DIFFICULTIES_COLORS } from "renderer/partials/maps/map-difficulties/map-difficulties-colors"
import useDoubleClick from "use-double-click";
export type ParsedMapDiff = {type: BsvMapDifficultyType, name: string, stars: number}
export type MapItemProps<T = any> = {
hash: string,
title: string,
autor: string,
songAutor: string,
coverUrl: string,
songUrl: string,
autorId: number,
mapId: string,
diffs: Map<BsvMapCharacteristic, ParsedMapDiff[]>,
qualified: boolean,
ranked: boolean,
bpm: number,
duration: number,
likes: number,
createdAt: string,
selected?: boolean,
downloading?: boolean,
callBackParam: T
onDelete?: (param: T) => void,
onDownload?: (param: T) => void,
onSelected?: (param: T) => void,
onCancelDownload?: (param: T) => void,
onDoubleClick?: (param: T) => void
}
export const MapItem = memo(({hash, title, autor, songAutor, coverUrl, songUrl, autorId, mapId, diffs, qualified, ranked, bpm, duration, likes, createdAt, selected, downloading, callBackParam, onDelete, onDownload, onSelected, onCancelDownload, onDoubleClick}: MapItemProps) => {
const linkOpener = LinkOpenerService.getInstance();
const audioPlayer = AudioPlayerService.getInstance();
const color = useThemeColor("first-color");
const t = useTranslation();
const ref = useRef(null);
const [hovered, setHovered] = useState(false);
const [bottomBarHovered, setBottomBarHovered, cancelBottomBarHovered] = useDelayedState(false);
const [diffsPanelHovered, setDiffsPanelHovered] = useState(false);
useDoubleClick({
ref,
latency: onDoubleClick ? 200 : 0,
onSingleClick: e => onSelected?.(callBackParam),
onDoubleClick: e => onDoubleClick?.(callBackParam)
})
const songPlaying = useObservable(audioPlayer.playing$.pipe(map(playing => playing && audioPlayer.src === songUrl)));
const zipUrl = getMapZipUrlFromHash(hash);
const previewUrl = mapId ? `https://skystudioapps.com/bs-viewer/?url=${zipUrl}` : null;
const mapUrl = mapId ? `https://beatsaver.com/maps/${mapId}` : null;
const authorUrl = autorId ? `https://beatsaver.com/profile/${autorId}` : null;
const createdDate = createdAt ? dateFormat(createdAt, "d mmm yyyy") : null;
const likesText = likes ? Intl.NumberFormat(undefined, {notation: "compact"}).format(likes).split(" ").join("") : null;
const durationText = (() => {
if(!duration){ return null; }
const date = new Date(0);
date.setSeconds(duration);
return duration > 3600 ? dateFormat(date, "h:MM:ss") : dateFormat(date, "MM:ss");
})();
const parseDiffLabel = (diffLabel: string) => {
if(MAP_DIFFICULTIES.includes(diffLabel as BsvMapDifficultyType)){
return t(`maps.difficulties.${diffLabel}`);
}
return diffLabel;
}
const openPreview = () => linkOpener.open(previewUrl, true);
const copyBsr = () => navigator.clipboard.writeText(`!bsr ${mapId}`);
const toogleMusic = () => {
if(songPlaying){
return audioPlayer.pause();
}
if(!audioPlayer.playing && audioPlayer.src === songUrl){
return audioPlayer.resume();
}
audioPlayer.play(songUrl, bpm);
}
const bottomBarHoverStart = () => {
cancelBottomBarHovered();
setBottomBarHovered(true, (diffsPanelHovered || bottomBarHovered) ? 0 : 300);
}
const bottomBarHoverEnd = () => {
cancelBottomBarHovered();
setBottomBarHovered(false, 100);
}
const diffsPanelHoverStart = () => setDiffsPanelHovered(true);
const diffsPanelHoverEnd = () => setDiffsPanelHovered(false);
const renderDiffPreview = () => {
const diffSets = Array.from(diffs.entries());
if(diffSets.length === 1){
const [diffType, diffSet] = diffSets[0];
return (
<>
<BsmIcon className="h-4 w-4 mr-px" icon="bsMapDifficulty"/>
<div className="flex py-[2px] gap-[1px] h-full">
{diffSet.map(diff => (
<span key={crypto.randomUUID()} className="h-full w-[6px] rounded-full" style={{backgroundColor: MAP_DIFFICULTIES_COLORS[diff.type]}}/>
))}
</div>
</>
)
}
if(diffSets.length > 1){
return diffSets.map(([diffType, diffSet]) => (
<Fragment key={crypto.randomUUID()}>
<BsmIcon className="h-full w-fit mr-px" icon={diffType}/>
<span className="mr-2 font-bold text-[15px] h-full flex items-center pb-px">{diffSet.length}</span>
</Fragment>
));
}
}
return (
<motion.li className="relative h-[100px] min-w-[370px] shrink-0 grow basis-0 text-white group cursor-pointer" onHoverStart={() => setHovered(true)} onHoverEnd={() => setHovered(false)} style={{zIndex: hovered && 5, transform: "translateZ(0) scale(1.0, 1.0)", backfaceVisibility: "hidden"}}>
{(hovered || selected) && onSelected && <motion.span className="glow-on-hover !transition-none" animate={{opacity: 1}} transition={{duration: .2, ease: "linear"}}/>}
<AnimatePresence>
{(diffsPanelHovered || bottomBarHovered) && (
<motion.ul key={hash} className="absolute top-[calc(100%-10px)] w-full h-fit max-h-[200%] pt-4 pb-2 px-2 overflow-y-scroll bg-light-main-color-3 dark:bg-main-color-3 text-main-color-1 dark:text-current brightness-125 rounded-md flex flex-col gap-3 scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 shadow-sm shadow-black" initial={{opacity: 0}} animate={{opacity: 1}} exit={{opacity: 0}} transition={{duration: .15}} onHoverStart={diffsPanelHoverStart} onHoverEnd={diffsPanelHoverEnd}>
{Array.from(diffs.entries()).map(([charac, diffSet]) => (
<ol key={crypto.randomUUID()} className="flex flex-col w-full gap-1">
{diffSet.map(({type, name, stars}) => (
<li key={`${type}${name}${stars}`} className="w-full h-4 flex items-center gap-1">
<BsmIcon className="h-full w-fit p-px" icon={charac}/>
<span className="h-full px-2 flex items-center text-xs font-bold bg-current rounded-full" style={{color: MAP_DIFFICULTIES_COLORS[type]}}>
{
stars ? (
<span className="h-full block brightness-[.25]"> {stars}</span>
) : (
<span className="h-full brightness-[.25] leading-4 pb-[2px] capitalize">{parseDiffLabel(type)}</span>
)
}
</span>
<span className="text-sm leading-4 pb-[2px] capitalize">{parseDiffLabel(name)}</span>
</li>
))}
</ol>
))}
</motion.ul>
)}
</AnimatePresence>
<div className="h-full w-full relative pl-[100px] rounded-md overflow-hidden flex flex-row justify-end">
<div className="absolute top-0 left-0 h-full aspect-square cursor-pointer">
<BsmImage className="w-full h-full object-cover" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} loading="lazy"/>
<span className="absolute flex justify-center items-center w-full h-full pr-1 bg-transparent top-0 left-0 group-hover:bg-black group-hover:bg-opacity-40" style={{color}} onClick={(e) => {e.stopPropagation(); e.preventDefault(); toogleMusic()}}>
<BsmIcon className="w-full h-full p-7 opacity-0 group-hover:opacity-100 text-white hover:text-current" icon={songPlaying ? "pause" : "play"}/>
</span>
</div>
<div className="relative h-full w-full z-[1] rounded-md overflow-hidden -translate-x-1" ref={ref}>
<BsmImage className="absolute top-0 left-0 w-full h-full -z-[1] object-cover" image={coverUrl} placeholder={defaultImage} errorImage={defaultImage} loading="lazy"/>
<div className="pt-1 pl-2 pr-7 top-0 left-0 w-full h-full bg-gray-600 bg-opacity-80 flex flex-col justify-between group-hover:bg-main-color-1 group-hover:bg-opacity-80">
<h1 className="font-bold whitespace-nowrap text-ellipsis overflow-hidden w-full leading-5 tracking-wide text-lg" title={title}><BsmLink className="hover:underline" href={mapUrl}>{title}</BsmLink></h1>
<h2 className="font-bold whitespace-nowrap text-ellipsis overflow-hidden w-full text-sm mb-[3px]">{songAutor && t("maps.map-item.by", {songAutor})}</h2>
<h3 className="font-bold whitespace-nowrap text-ellipsis overflow-hidden w-full text-xs">{autor && (<> {t("maps.map-item.mapped-by")} <BsmLink href={authorUrl} className="brightness-150 hover:underline" style={{color}}>{autor}</BsmLink></>)}</h3>
<div className="w-full h-4 text-xs gap-2 flex opacity-0 group-hover:opacity-100">
{likesText && (
<div className="h-full flex items-center">
<BsmIcon className="h-full py-[2px] w-fit shrink-0 mr-1" icon="thumbUpFill"/>
<span className="mb-[2px]">{likesText}</span>
</div>
)}
{durationText && (
<div className="h-full flex items-center">
<BsmIcon className="h-full py-[2px] w-fit shrink-0 mr-1" icon="timerFill"/>
<span className="mb-[2px]">{durationText}</span>
</div>
)}
{createdAt && (
<div className="h-full flex items-center">
<BsmIcon className="h-full py-[2px] w-fit shrink-0 mr-1" icon="checkCircleIcon"/>
<time className="mb-[2px]">{createdDate}</time>
</div>
)}
</div>
<motion.div className="w-full h-5 pb-1 pr-7 flex items-center gap-1" onHoverStart={bottomBarHoverStart} onHoverEnd={bottomBarHoverEnd}>
{ranked && (
<div className="text-yellow-300 bg-current rounded-full px-1 h-full flex items-center justify-center">
<span className="uppercase text-xs font-bold tracking-wide brightness-[.25]">{t("maps.map-specificities.ranked")}</span>
</div>
)}
<div className="h-full grow flex items-start content-start">
{renderDiffPreview()}
</div>
</motion.div>
</div>
</div>
<div className="absolute bg-light-main-color-1 dark:bg-main-color-3 top-0 h-full z-[1] w-[30px] -right-5 group-hover:right-0 transition-all">
<span className="absolute w-[10px] h-[10px] top-0 right-full bg-inherit" style={{clipPath: 'path("M11 -1 L11 10 L10 10 A10 10 0 0 0 0 0 L0 -1 Z")'}}/>
<span className="absolute w-[10px] h-[10px] bottom-0 right-full bg-inherit" style={{clipPath: 'path("M11 11 L11 0 L10 0 A10 10 0 0 1 0 10 L 0 11 Z")'}}/>
<div className="flex flex-col justify-center items-center gap-1 w-full h-full overflow-hidden opacity-0 group-hover:opacity-100">
{onDelete && !downloading && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-150" iconColor={color} icon="trash" withBar={false} onClick={e => {e.stopPropagation(); onDelete(callBackParam)}}/>}
{onDownload && !downloading && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-150" iconColor={color} icon="download" withBar={false} onClick={e => {e.stopPropagation(); onDownload(callBackParam)}}/>}
{onCancelDownload && !downloading && <BsmButton className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-150" iconColor="red" icon="cross" withBar={false} onClick={e => {e.stopPropagation(); onCancelDownload(callBackParam)}}/>}
{downloading && <BsmBasicSpinner className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2 flex items-center justify-center" spinnerClassName="brightness-150" style={{color}} thikness="3px"/>}
{previewUrl && <BsmButton className="w-6 h-6 p-[2px] rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-150" iconColor={color} icon="eye" withBar={false} onClick={e => {e.stopPropagation(); openPreview()}}/>}
{mapId && <BsmButton className="w-6 h-6 p-1 rounded-md !bg-inherit hover:!bg-light-main-color-2 hover:dark:!bg-main-color-2" iconClassName="w-full h-full brightness-150" iconColor={color} icon="twitch" withBar={false} onClick={e => {e.stopPropagation(); copyBsr()}}/>}
</div>
</div>
</div>
</motion.li>
)
}, areEqual)
function areEqual(prevProps: MapItemProps, nextProps: MapItemProps): boolean {
return equal(prevProps, nextProps);
}
@@ -0,0 +1,142 @@
import { DetailedHTMLProps, useEffect, useRef, useState } from "react"
import { BSVersion } from "shared/bs-version.interface"
import { TabNavBar } from "../shared/tab-nav-bar.component"
import { LocalMapsListPanel } from "./local-maps-list-panel.component"
import { BsmDropdownButton, DropDownItem } from "../shared/bsm-dropdown-button.component"
import { FilterPanel } from "./filter-panel.component"
import { MapFilter } from "shared/models/maps/beat-saver.model"
import { useThemeColor } from "renderer/hooks/use-theme-color.hook"
import { MapsManagerService } from "renderer/services/maps-manager.service"
import { motion, Variants } from "framer-motion";
import { MapsDownloaderService } from "renderer/services/maps-downloader.service"
import { BsmImage } from "../shared/bsm-image.component"
import wipGif from "../../../../assets/images/gifs/wip.gif"
import { OsDiagnosticService } from "renderer/services/os-diagnostic.service"
import { useObservable } from "renderer/hooks/use-observable.hook"
import { BsmIcon } from "../svgs/bsm-icon.component"
import { useTranslation } from "renderer/hooks/use-translation.hook"
type Props = {
version?: BSVersion
}
export function MapsPlaylistsPanel({version}: Props) {
const mapsService = MapsManagerService.getInstance();
const mapsDownloader = MapsDownloaderService.getInstance();
const osDiagnostic = OsDiagnosticService.getInstance();
const [tabIndex, setTabIndex] = useState(0);
const [mapFilter, setMapFilter] = useState<MapFilter>({});
const [mapSearch, setMapSearch] = useState("");
const [playlistSearch, setPlaylistSearch] = useState("");
const [mapsLinked, setMapsLinked] = useState(false);
const isOnline = useObservable(osDiagnostic.isOnline$);
const color = useThemeColor("first-color");
const t = useTranslation();
const mapsRef = useRef<any>();
useEffect(() => {
loadMapIsLinked();
}, [version]);
const loadMapIsLinked = () => {
mapsService.versionHaveMapsLinked(version).then(setMapsLinked);
}
const handleSearch = (value: string) => {
if(tabIndex === 0){
return setMapSearch(() => value);
}
return setPlaylistSearch(() => value);
}
const handleMapsLinkClick = () => {
if(!mapsLinked){
return mapsService.linkVersion(version).then(loadMapIsLinked);
}
return mapsService.unlinkVersion(version).then(loadMapIsLinked);
}
const handleMapsAddClick = () => {
mapsDownloader.openDownloadMapModal(version);
}
const renderTab = (props: DetailedHTMLProps<React.HTMLAttributes<HTMLLIElement>, HTMLLIElement>, text: string, index: number): JSX.Element => {
const linkedColor = mapsLinked ? color : "red";
const onClickLink = (index: number) => {
if(index === 0){ handleMapsLinkClick(); }
}
const onClickAdd = (index: number) => {
if(index === 0){ handleMapsAddClick(); }
}
const variants: Variants = { hover: {rotate: 22.5}, tap: {rotate: 45} };
return (
<li className="relative text-center text-lg font-bold hover:backdrop-brightness-75 flex justify-center items-center content-center" onClick={props.onClick}>
<span className="text-main-color-1 dark:text-gray-200">{text}</span>
{index === 0 &&(
<div className="h-full flex absolute right-0 top-0 gap-1.5 items-center pr-2">
{isOnline && (
<motion.div whileHover="hover" whileTap="tap" className="relative h-[calc(100%-5px)] flex flex-row justify-center items-center shrink-0 rounded-full overflow-hidden pr-2" style={{color}} onClick={e => {e.stopPropagation(); onClickAdd(index)}}>
<span className="absolute top-0 left-0 h-full w-full brightness-50 opacity-75 dark:opacity-20 dark:filter-none" style={{backgroundColor: "currentcolor"}}/>
<motion.div className="h-full p-0.5" variants={variants}>
<BsmIcon className="block h-full aspect-square brightness-150" icon="add"/>
</motion.div>
<span className="text-sm brightness-150">{t("pages.version-viewer.maps.tabs.maps.actions.add-maps.text")}</span>
</motion.div>
)}
{(!!version) && (
<motion.div variants={variants} whileHover="hover" whileTap="tap" initial={{rotate: 0}} className="block p-0.5 h-[calc(100%-5px)] aspect-square blur-0 hover:brightness-75" title={t(mapsLinked ? "pages.version-viewer.maps.tabs.maps.actions.link-maps.tooltips.unlink" : "pages.version-viewer.maps.tabs.maps.actions.link-maps.tooltips.link")} onClick={e => {e.stopPropagation(); onClickLink(index)}}>
<span className="absolute top-0 left-0 h-full w-full rounded-full brightness-50 opacity-75 dark:opacity-20 dark:filter-none" style={{backgroundColor: linkedColor}}/>
<BsmIcon className="p-1 absolute top-0 left-0 h-full w-full !bg-transparent -rotate-45 brightness-150" icon={mapsLinked ? "link" : "unlink"} style={{color: linkedColor}} />
</motion.div>
)}
</div>
)}
</li>
)
}
const dropDownItems = ((): DropDownItem[] => {
if(tabIndex === 1){
return [
]
}
return [
{icon:"export", text: "pages.version-viewer.maps.search-bar.dropdown.export-maps", onClick: () => mapsRef.current.exportMaps?.()},
{icon: "trash", text: "pages.version-viewer.maps.search-bar.dropdown.delete-maps", onClick: () => mapsRef.current.deleteMaps?.()}
]
})()
return (
<div className="w-full h-full flex flex-col items-center justify-center">
<nav className="w-full shrink-0 flex h-9 justify-center px-40 gap-2 mb-3 text-main-color-1 dark:text-white">
<div className="h-full rounded-full bg-light-main-color-2 dark:bg-main-color-2 grow p-[6px]">
<input type="text" className="h-full w-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={tabIndex === 0 ? mapSearch : playlistSearch} onChange={e => handleSearch(e.target.value)}/>
</div>
<BsmDropdownButton className="h-full relative z-[1] flex justify-center" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" withBar={false}>
<FilterPanel className="absolute top-[calc(100%+3px)] origin-top w-[500px] h-fit p-2 rounded-md shadow-md shadow-black" filter={mapFilter} onChange={setMapFilter}/>
</BsmDropdownButton>
<BsmDropdownButton className="h-full flex aspect-square relative rounded-full z-[1] bg-light-main-color-1 dark:bg-main-color-3" buttonClassName="rounded-full h-full w-full p-[6px]" icon="three-dots" withBar={false} items={dropDownItems} menuTranslationY="6px" align="center"/>
</nav>
<div className="w-full h-full flex flex-col bg-light-main-color-3 dark:bg-main-color-2 rounded-md shadow-black shadow-md overflow-hidden">
<TabNavBar className="!rounded-none shadow-sm" tabIndex={tabIndex} tabsText={["misc.maps", "misc.playlists"]} onTabChange={setTabIndex} renderTab={renderTab}/>
<div className="w-full grow min-h-0 flex flex-row items-center transition-transform duration-300" style={{transform: `translate(${-(tabIndex * 100)}%, 0)`}}>
<LocalMapsListPanel ref={mapsRef} className="w-full h-full shrink-0 flex flex-col" version={version} filter={mapFilter} search={mapSearch}/>
<div className="w-full h-full shrink-0 flex flex-col justify-center items-center content-center gap-2 overflow-hidden">
<BsmImage className="rounded-md" image={wipGif}/>
<span>Work in progress</span>
</div>
</div>
</div>
</div>
)
}
@@ -0,0 +1,11 @@
type Props = {
className?: string
}
export function MapsToolbar({className}: Props) {
return (
<div className={className}>
</div>
)
}
@@ -0,0 +1,31 @@
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import { BsmLocalMap } from "shared/models/maps/bsm-local-map.interface";
import BeatConflict from '../../../../../assets/images/apngs/beat-conflict.png'
export const DeleteMapsModal: ModalComponent<void, {linked: boolean, maps: BsmLocalMap[]}> = ({resolver, data:{linked, maps}}) => {
const t = useTranslation();
const multiple = maps.length > 1;
const titleText = multiple ? "modals.maps-actions.delete-maps.title.multiple" : "modals.maps-actions.delete-maps.title.single";
const descText = multiple ? "modals.maps-actions.delete-maps.desc.multiple" : "modals.maps-actions.delete-maps.desc.single";
const infoText = multiple ? "modals.maps-actions.delete-maps.info.desc.multiple" : "modals.maps-actions.delete-maps.info.desc.single";
const infoTitleText = multiple ? "modals.maps-actions.delete-maps.info.title.multiple" : "modals.maps-actions.delete-maps.info.title.single";
return (
<form className="text-gray-800 dark:text-gray-200">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t(titleText)}</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict} />
<p className="max-w-sm w-full">{t(descText, multiple ? {nb: maps.length.toString()} : {name: maps.at(0).rawInfo._songName})}</p>
{linked && <p className="text-sm italic mt-2 cursor-help w-fit" title={t(infoTitleText)}>{t(infoText)}</p>}
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/>
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.COMPLETED})} withBar={false} text="misc.delete"/>
</div>
</form>
)
}
@@ -0,0 +1,181 @@
import { motion } from "framer-motion";
import { useCallback, useEffect, useRef, useState } from "react";
import { FilterPanel } from "renderer/components/maps-mangement-components/filter-panel.component";
import { MapItem, ParsedMapDiff } from "renderer/components/maps-mangement-components/map-item.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmDropdownButton } from "renderer/components/shared/bsm-dropdown-button.component";
import { BsmSelect, BsmSelectOption } from "renderer/components/shared/bsm-select.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { BSV_SORT_ORDER } from "renderer/partials/beat-saver/sort-order";
import { BeatSaverService } from "renderer/services/thrird-partys/beat-saver.service";
import { MapsDownloaderService } from "renderer/services/maps-downloader.service";
import { MapsManagerService } from "renderer/services/maps-manager.service";
import { ModalComponent } from "renderer/services/modale.service";
import { BSVersion } from "shared/bs-version.interface";
import { BsvMapCharacteristic, BsvMapDetail, MapFilter, SearchOrder, SearchParams } from "shared/models/maps/beat-saver.model";
import BeatWaitingImg from "../../../../../assets/images/apngs/beat-waiting.png";
import equal from "fast-deep-equal/es6";
import { ProgressBarService } from "renderer/services/progress-bar.service";
import { useTranslation } from "renderer/hooks/use-translation.hook";
export const DownloadMapsModal: ModalComponent<void, BSVersion> = ({data}) => {
const beatSaver = BeatSaverService.getInstance();
const mapsManager = MapsManagerService.getInstance();
const mapsDownloader = MapsDownloaderService.getInstance();
const progressBar = ProgressBarService.getInstance();
const currentDownload = useObservable(mapsDownloader.currentMapDownload$);
const mapsInQueue = useObservable(mapsDownloader.mapsInQueue$);
const t = useTranslation();
const [filter, setFilter] = useState<MapFilter>({});
const [query, setQuery] = useState("");
const [maps, setMaps] = useState<BsvMapDetail[]>([]);
const [sortOrder, setSortOrder] = useState<SearchOrder>(BSV_SORT_ORDER.at(0));
const [ownedMapHashs, setOwnedMapHashs] = useState<string[]>([]);
const [searchParams, setSearchParams] = useState<SearchParams>({
sortOrder,
filter,
page: 0,
q: query
});
const loaderRef = useRef(null);
const sortOptions: BsmSelectOption[] = (() => {
return BSV_SORT_ORDER.map(sort => ({text: `beat-saver.maps-sorts.${sort}`, value: sort}));
})();
useEffect(() => {
loadMaps(searchParams);
}, [searchParams]);
useEffect(() => {
mapsManager.getMaps(data, false).toPromise().then(maps => setOwnedMapHashs(maps.map(map => map.hash)));
const onMapDownloaded = (map: BsvMapDetail, verion: BSVersion) => {
if(!equal(verion, data) || !map?.versions){ return; }
const downloadedHash = map.versions.at(0).hash;
setOwnedMapHashs((prev) => [...prev, downloadedHash]);
}
mapsDownloader.addOnMapDownloadedListener(onMapDownloaded);
if(mapsDownloader.isDownloading){
progressBar.setStyle(mapsDownloader.progressBarStyle);
}
return () => {
mapsDownloader.removeOnMapDownloadedListene(onMapDownloaded);
progressBar.setStyle(null);
}
}, [])
const loadMaps = (params: SearchParams) => {
beatSaver.searchMaps(params).then((maps => setMaps(prev => [...prev, ...maps])));
}
const extractMapDiffs = (map: BsvMapDetail): Map<BsvMapCharacteristic, ParsedMapDiff[]> => {
const res = new Map<BsvMapCharacteristic, ParsedMapDiff[]>();
if(map.versions.at(0).diffs){
map.versions.at(0).diffs.forEach(diff => {
const arr = res.get(diff.characteristic) || [];
arr.push({name: diff.difficulty, type: diff.difficulty, stars: diff.stars});
res.set(diff.characteristic, arr);
});
}
return res;
}
const renderMap = (map: BsvMapDetail) => {
const isMapOwned = map.versions.some(version => ownedMapHashs.includes(version.hash));
const isDownloading = map.id === currentDownload?.map?.id;
const inQueue = mapsInQueue.some(toDownload => equal(toDownload.version, data) && toDownload.map.id === map.id);
return (
<MapItem
autor={map.metadata.levelAuthorName}
autorId={map.uploader.id}
bpm={map.metadata.bpm}
coverUrl={map.versions.at(0).coverURL}
createdAt={map.createdAt}
duration={map.metadata.duration}
hash={map.versions.at(0).hash}
likes={map.stats.upvotes}
mapId={map.id}
ranked={map.ranked}
qualified={map.qualified}
title={map.name}
songAutor={map.metadata.songAuthorName}
diffs={extractMapDiffs(map)}
songUrl={map.versions.at(0).previewURL}
key={map.id}
onDownload={(!isMapOwned && !inQueue) && (handleDownloadMap)}
onDoubleClick={(!isMapOwned && !inQueue) && (handleDownloadMap)}
onCancelDownload={(inQueue && !isDownloading) && (handleCancelDownload)}
downloading={isDownloading}
callBackParam={map}
/>
)
}
const handleDownloadMap = useCallback((map: BsvMapDetail) => {
mapsDownloader.addMapToDownload({map, version: data})
}, []);
const handleCancelDownload = useCallback((map: BsvMapDetail) => {
mapsDownloader.removeMapToDownload({map, version: data});
}, []);
const handleSortChange = (newSort: string) => {
setSortOrder(() => newSort as SearchOrder);
setMaps(() => []);
setSearchParams(() => ({...searchParams, sortOrder: (newSort as SearchOrder)}));
}
const handleSearch = () => {
const searchParams: SearchParams = {
sortOrder,
filter,
q: query.trim(),
page: 0,
};
setMaps(() => []);
setSearchParams(() => searchParams);
}
const handleLoadMore = () => {
setSearchParams((prev) => {
return {...prev, page: prev.page + 1}
});
}
return (
<form className="text-gray-800 dark:text-gray-200 flex flex-col max-w-[95vw] w-[970px] h-[85vh] gap-3" onSubmit={e => {e.preventDefault(); handleSearch()}}>
<div className="flex h-9 gap-2 shrink-0">
<BsmDropdownButton className="shrink-0 h-full relative z-[1] flex justify-start" buttonClassName="flex items-center justify-center h-full rounded-full px-2 py-1 !bg-light-main-color-1 dark:!bg-main-color-1" icon="filter" text="pages.version-viewer.maps.search-bar.filters-btn" withBar={false}>
<FilterPanel className="absolute top-[calc(100%+3px)] origin-top w-[450px] h-fit p-2 rounded-md shadow-md shadow-black" filter={filter} onChange={setFilter}/>
</BsmDropdownButton>
<input className="h-full bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-2 grow pb-0.5" type="text" name="" id="" placeholder={t("pages.version-viewer.maps.search-bar.search-placeholder")} value={query} onChange={e => setQuery(e.target.value)}/>
<BsmButton className="shrink-0 rounded-full py-1 px-3 !bg-light-main-color-1 dark:!bg-main-color-1 flex justify-center items-center capitalize" icon="search" text="modals.download-maps.search-btn" withBar={false} onClick={e => {e.preventDefault(); handleSearch()}}/>
<BsmSelect className="bg-light-main-color-1 dark:bg-main-color-1 rounded-full px-1 pb-0.5 text-center" options={sortOptions} onChange={handleSortChange}/>
</div>
<ul className="w-full grow flex content-start flex-wrap gap-2 px-2 overflow-y-scroll overflow-x-hidden scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900" >
{maps.length === 0 ? (
<div className="w-full h-full flex flex-col items-center justify-center">
<img className="w-32 h-32 spin-loading" src={BeatWaitingImg} alt=" "/>
<span className="text-lg">{t("modals.download-maps.loading-maps")}</span>
</div>
) : (
<>
{maps.map(renderMap)}
<motion.span onViewportEnter={handleLoadMore} ref={loaderRef} className="block w-full h-8"/>
</>
)}
</ul>
</form>
)
}
@@ -5,18 +5,17 @@ import { BsmIcon } from "renderer/components/svgs/bsm-icon.component";
import { DefaultConfigKey } from "renderer/config/default-configuration.config";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ConfigurationService } from "renderer/services/configuration.service";
import { ModalExitCode, ModalResponse, ModalService } from "renderer/services/modale.service";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import { BSVersion } from "shared/bs-version.interface";
export function EditVersionModal({resolver, clone = false}: {resolver: (x: ModalResponse<{name: string, color: string}>) => void, clone?: boolean}) {
export const EditVersionModal: ModalComponent<{name: string, color: string}, {version: BSVersion, clone?: boolean}> = ({resolver, data}) => {
const {version, clone} = data;
const configService = ConfigurationService.getInstance();
const modalService = ModalService.getInsance();
const modalData: BSVersion = modalService.getModalData();
const [name, setName] = useState(modalData.name || modalData.BSVersion);
const [color, setColor] = useState(modalData.color ?? configService.get<string>("second-color" as DefaultConfigKey));
const [name, setName] = useState(version.name || version.BSVersion);
const [color, setColor] = useState(version.color ?? configService.get<string>("second-color" as DefaultConfigKey));
const t = useTranslation();
const rename = () => {
@@ -36,11 +35,11 @@ export function EditVersionModal({resolver, clone = false}: {resolver: (x: Modal
<p className="max-w-sm mb-2 text-gray-800 dark:text-gray-200">{t("modals.clone-version.description")}</p>
)}
<div className="mb-3">
<label className="block font-bold cursor-pointer tracking-wide text-gray-800 dark:text-gray-200" htmlFor="name">{t("Nom")}</label>
<input className="w-full bg-light-main-color-1 dark:bg-main-color-1 px-1 py-[2px] rounded-md outline-none" onChange={e => setName(e.target.value)} value={name} type="text" name="name" id="name" minLength={2} maxLength={15} placeholder={t("Nom de la version")}/>
<label className="block font-bold cursor-pointer tracking-wide text-gray-800 dark:text-gray-200" htmlFor="name">{t("modals.clone-version.inputs.name.label")}</label>
<input className="w-full bg-light-main-color-1 dark:bg-main-color-1 px-1 py-[2px] rounded-md outline-none" onChange={e => setName(e.target.value)} value={name} type="text" name="name" id="name" minLength={2} maxLength={15} placeholder={t("modals.clone-version.inputs.name.placeholder")}/>
</div>
<div>
<span className="block font-bold tracking-wide text-gray-800 dark:text-gray-200">Couleur</span>
<span className="block font-bold tracking-wide text-gray-800 dark:text-gray-200">{t("modals.clone-version.inputs.color.label")}</span>
<div className="relative w-full h-7 mb-4 bg-light-main-color-1 dark:bg-main-color-1 flex justify-center rounded-md py-1 z-[1]">
<SettingColorChooser color={color} onChange={setColor} pickerClassName="!h-32 !w-32"/>
<div className="absolute right-2 top-0 h-full flex items-center">
@@ -1,11 +1,11 @@
import { ModalExitCode, ModalResponse } from "../../../services/modale.service";
import { ModalComponent, ModalExitCode } from "../../../services/modale.service";
import { useState } from "react";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
export function GuardModal({resolver}: {resolver: (x: ModalResponse<string>) => void}) {
export const GuardModal: ModalComponent<string> = ({resolver}) => {
const [guardCode, setGuardCode] = useState('');
const t = useTranslation();
@@ -1,16 +1,15 @@
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalExitCode, ModalResponse } from "renderer/services/modale.service";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"
export function InstallationFolderModal({resolver}: {resolver: (x: ModalResponse) => void}) {
export const InstallationFolderModal: ModalComponent<void> = ({resolver}) => {
const t = useTranslation();
return (
<>
<span className="absolute bg-gradient-to-r from-blue-500 to-red-500 top-0 w-full left-0 h-1"></span>
<form>
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.install-folder.title")}</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict}/>
<p className="max-w-sm text-gray-800 dark:text-gray-200">{t("modals.install-folder.description")}</p>
@@ -18,6 +17,6 @@ export function InstallationFolderModal({resolver}: {resolver: (x: ModalResponse
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/>
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.COMPLETED})} withBar={false} text="modals.install-folder.buttons.submit"/>
</div>
</>
</form>
)
}
@@ -0,0 +1,30 @@
import { useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import BeatRunning from '../../../../../assets/images/apngs/beat-running.png'
export const LinkMapsModal: ModalComponent<boolean> = ({resolver}) => {
const t = useTranslation();
const [keepMaps, setKeepMaps] = useState(true);
return (
<form className="text-gray-800 dark:text-gray-200">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("modals.maps-actions.link-maps.title")}</h1>
<BsmImage className="mx-auto h-24" image={BeatRunning} />
<p className="max-w-sm w-full">{t("modals.maps-actions.link-maps.desc")}</p>
<p className="max-w-sm w-full text-sm italic mt-2">{t("modals.maps-actions.link-maps.info")}</p>
<div className="relative h-5 flex my-3 items-center">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1.5 z-[1]" checked={keepMaps} onChange={setKeepMaps}/>
<span className="text-sm mb-0.5 cursor-help" title={t("modals.maps-actions.link-maps.keep-maps.title")}>{t("modals.maps-actions.link-maps.keep-maps.label")}</span>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/>
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.COMPLETED, data: keepMaps})} withBar={false} text="modals.maps-actions.link-maps.valid-btn"/>
</div>
</form>
)
}
@@ -2,10 +2,10 @@ import { useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalExitCode, ModalResponse } from "renderer/services/modale.service";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import BeatImpatient from '../../../../../assets/images/apngs/beat-impatient.png'
export function LoginModal({resolver}: {resolver: (x: ModalResponse<any>) => void}) {
export const LoginModal: ModalComponent<{username: string, password: string, stay: boolean}> = ({resolver}) => {
const [username, setUsername] = useState('');
const [password, setPassword] = useState('');
@@ -35,7 +35,7 @@ export function LoginModal({resolver}: {resolver: (x: ModalResponse<any>) => voi
</div>
<div className="grid grid-flow-col grid-cols-2 gap-4">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => {resolver({exitCode: ModalExitCode.CANCELED})}} withBar={false} text="misc.cancel"/>
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" type="submit" withBar={false} text={t("modals.steam-login.buttons.submit")}/>
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" type="submit" withBar={false} text="modals.steam-login.buttons.submit"/>
</div>
</form>
)
@@ -0,0 +1,28 @@
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import BeatConflict from '../../../../../assets/images/apngs/beat-conflict.png'
export const ModsDisclaimerModal: ModalComponent<void, void> = ({resolver}) => {
const t = useTranslation();
return (
<form className="max-w-[800px] text-gray-800 dark:text-gray-200">
<h1 className="text-3xl uppercase tracking-wide w-full text-center text-gray-800 dark:text-gray-200">{t("modals.mods-disclaimer.title")}</h1>
<BsmImage className="mx-auto h-20" image={BeatConflict} />
<p className="mb-1">{t("modals.mods-disclaimer.p-1")}</p>
<ul className="list-disc w-full pl-6">
<li>{t("modals.mods-disclaimer.li-1")}</li>
<li>{t("modals.mods-disclaimer.li-2")}</li>
<li>{t("modals.mods-disclaimer.li-3")}</li>
</ul>
<p className="mt-1">{t("modals.mods-disclaimer.p-2")}</p>
<div className="grid grid-flow-col grid-cols-2 gap-4 pt-3">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => {resolver({exitCode: ModalExitCode.CANCELED})}} withBar={false} text="misc.refuse"/>
<BsmButton typeColor="primary" className="rounded-md text-center transition-all" onClick={() => {resolver({exitCode: ModalExitCode.COMPLETED})}} withBar={false} text="misc.accept"/>
</div>
</form>
)
}
@@ -1,4 +1,4 @@
import { ModalExitCode, ModalResponse, ModalService } from "../../../services/modale.service";
import { ModalComponent, ModalExitCode } from "../../../services/modale.service";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
@@ -6,9 +6,9 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
import { BSVersion } from "shared/bs-version.interface";
export function UninstallAllModsModal({resolver}: {resolver: (x: ModalResponse) => void}) {
export const UninstallAllModsModal: ModalComponent<void, BSVersion> = ({resolver, data}) => {
const version = ModalService.getInsance().getModalData<BSVersion>();
const version = data;
const t = useTranslation();
const desc = "modals.uninstall-all-mods.description";
@@ -1,4 +1,4 @@
import { ModalExitCode, ModalResponse, ModalService } from "../../../services/modale.service";
import { ModalComponent, ModalExitCode } from "../../../services/modale.service";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
@@ -6,9 +6,9 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
import { Mod } from "shared/models/mods/mod.interface";
export function UninstallModModal({resolver}: {resolver: (x: ModalResponse) => void}) {
export const UninstallModModal: ModalComponent<void, Mod> = ({resolver, data}) => {
const mod = ModalService.getInsance().getModalData<Mod>();
const mod = data;
const t = useTranslation();
const desc = mod.name.toLowerCase() === "bsipa" ? "modals.uninstall-mod.description-bsipa" : "modals.uninstall-mod.description";
@@ -1,13 +1,13 @@
import { ModalExitCode, ModalResponse, ModalService } from "../../../services/modale.service";
import { ModalComponent, ModalExitCode } from "../../../services/modale.service";
import BeatConflict from "../../../../../assets/images/apngs/beat-conflict.png"
import { BSVersion } from 'shared/bs-version.interface';
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
export function UninstallModal({resolver}: {resolver: (x: ModalResponse) => void}) {
export const UninstallModal: ModalComponent<void, BSVersion> = ({resolver, data}) => {
const version = ModalService.getInsance().getModalData<BSVersion>();
const version = data;
const t = useTranslation();
return (
@@ -0,0 +1,29 @@
import { useState } from "react";
import { BsmButton } from "renderer/components/shared/bsm-button.component";
import { BsmCheckbox } from "renderer/components/shared/bsm-checkbox.component";
import { BsmImage } from "renderer/components/shared/bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { ModalComponent, ModalExitCode } from "renderer/services/modale.service";
import BeatConflict from '../../../../../assets/images/apngs/beat-conflict.png'
export const UnlinkMapsModal: ModalComponent<boolean> = ({resolver}) => {
const t = useTranslation();
const [keepMaps, setKeepMaps] = useState(true);
return (
<form className="text-gray-800 dark:text-gray-200">
<h1 className="text-3xl uppercase tracking-wide w-full text-center">{t("modals.maps-actions.unlink-maps.title")}</h1>
<BsmImage className="mx-auto h-24" image={BeatConflict} />
<p className="max-w-sm w-full">{t("modals.maps-actions.unlink-maps.desc")}</p>
<div className="relative h-5 flex my-3 items-center">
<BsmCheckbox className="h-full aspect-square relative bg-inherit mr-1.5 z-[1]" checked={keepMaps} onChange={setKeepMaps}/>
<span className="text-sm mb-0.5 cursor-help" title={t("modals.maps-actions.unlink-maps.keep-maps.title")}>{t("modals.maps-actions.unlink-maps.keep-maps.label")}</span>
</div>
<div className="grid grid-flow-col grid-cols-2 gap-4 mt-4">
<BsmButton typeColor="cancel" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.CANCELED})} withBar={false} text="misc.cancel"/>
<BsmButton typeColor="error" className="rounded-md text-center transition-all" onClick={() => resolver({exitCode: ModalExitCode.COMPLETED, data: keepMaps})} withBar={false} text="modals.maps-actions.unlink-maps.valid-btn"/>
</div>
</form>
)
}
@@ -1,44 +1,55 @@
import { ModalExitCode, ModalService, ModalType } from "renderer/services/modale.service";
import { ModalExitCode, ModalService } from "renderer/services/modale.service";
import { AnimatePresence, motion } from "framer-motion";
import { LoginModal } from "./modal-types/login-modal.component";
import { GuardModal } from "./modal-types/guard-modal.component";
import { UninstallModal } from "./modal-types/uninstall-modal.component";
import { InstallationFolderModal } from "./modal-types/installation-folder-modal.component";
import { EditVersionModal } from "./modal-types/edit-version-modal.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { UninstallModModal } from "./modal-types/uninstall-mod-modal.component";
import { UninstallAllModsModal } from "./modal-types/uninstall-all-mods-modal.component";
import { useEffect } from "react";
import { BsmIcon } from "../svgs/bsm-icon.component";
export function Modal() {
const modalSevice = ModalService.getInsance();
const modalType = useObservable(modalSevice.modalType$);
const ModalComponent = useObservable(modalSevice.getModalToShow());
const modalData = modalSevice.getModalData();
const resolver = modalSevice.getResolver();
const {firstColor, secondColor} = useThemeColor();
const renderModal = () => {
const resolver = modalSevice.getResolver();
if(modalType === ModalType.STEAM_LOGIN){ return <LoginModal resolver={resolver}/> }
if(modalType === ModalType.GUARD_CODE){ return <GuardModal resolver={resolver}/> }
if(modalType === ModalType.UNINSTALL){ return <UninstallModal resolver={resolver}/> }
if(modalType === ModalType.INSTALLATION_FOLDER){ return <InstallationFolderModal resolver={resolver}/> }
if(modalType === ModalType.EDIT_VERSION){ return <EditVersionModal resolver={resolver}/> }
if(modalType === ModalType.CLONE_VERSION){ return <EditVersionModal resolver={resolver}/> }
if(modalType === ModalType.UNINSTALL_MOD){ return <UninstallModModal resolver={resolver}/> }
if(modalType === ModalType.UNINSTALL_ALL_MODS){ return <UninstallAllModsModal resolver={resolver}/> }
return null;
}
useEffect(() => {
const onEscape = (e: KeyboardEvent) => {
if(e.key !== "Escape"){ return; }
resolver?.(ModalExitCode.NO_CHOICE);
}
if(!!ModalComponent){
window.addEventListener("keyup", onEscape)
}
else{
window.removeEventListener("keyup", onEscape);
}
return () => {
window.removeEventListener("keyup", onEscape);
}
}, [ModalComponent])
return (
<AnimatePresence>
{modalType && (
{ModalComponent && (
<div className="top-0 absolute w-screen h-screen flex content-center items-center justify-center z-50">
<motion.span key="modal-overlay" onClick={() => modalSevice.resolve({exitCode: ModalExitCode.NO_CHOICE})} className="absolute top-0 bottom-0 right-0 left-0 bg-black" initial={{opacity: 0}} animate={{opacity: modalType && .60}} exit={{opacity: 0}} transition={{duration: .2}}/>
<motion.span key="modal-overlay" onClick={() => modalSevice.resolve({exitCode: ModalExitCode.NO_CHOICE})} className="absolute top-0 bottom-0 right-0 left-0 bg-black" initial={{opacity: 0}} animate={{opacity: ModalComponent && .60}} exit={{opacity: 0}} transition={{duration: .2}}/>
<motion.div key="modal" initial={{y: "100vh"}} animate={{y: 0}} exit={{y: "100vh"}}>
<div className="relative p-4 text-gray-800 dark:text-gray-200 overflow-hidden rounded-md shadow-lg shadow-black bg-gradient-to-br from-light-main-color-3 to-light-main-color-2 dark:from-main-color-3 dark:to-main-color-2">
<span className="absolute top-0 w-full left-0 h-1" style={{backgroundImage: `linear-gradient(to right, ${firstColor}, ${secondColor})`}}/>
{renderModal()}
<div className="relative p-4 text-gray-800 dark:text-gray-200 rounded-md shadow-lg shadow-black bg-gradient-to-br from-light-main-color-3 to-light-main-color-2 dark:from-main-color-3 dark:to-main-color-2">
<div className="absolute top-0 w-full left-0 h-1 rounded-t-md overflow-hidden">
<span className="block w-full h-full" style={{backgroundImage: `linear-gradient(to right, ${firstColor}, ${secondColor})`}}/>
</div>
<div className="w-2.5 h-2.5 absolute top-2.5 right-1.5 cursor-pointer" onClick={(e) => {e.stopPropagation(); resolver(ModalExitCode.CLOSED)}}>
<BsmIcon className="w-full h-full" icon="cross"/>
</div>
<ModalComponent resolver={resolver} data={modalData}/>
</div>
</motion.div>
</div>
@@ -1,13 +1,34 @@
import { memo } from "react"
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { motion } from "framer-motion";
import { motion, Variants } from "framer-motion";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { AudioPlayerService } from "renderer/services/audio-player.service";
export const BsManagerIcon = memo(({className}: {className?: string}) => {
const audioPlayer = AudioPlayerService.getInstance();
const {firstColor, secondColor} = useThemeColor();
const playing= useObservable(audioPlayer.playing$);
const bpm = audioPlayer.bpm;
const transitions: Variants = {
playing: {
scale: [1, 1.05, 1],
transition: {repeat: Infinity, duration: ((60/bpm))/2, repeatDelay: ((60/bpm))/2}
},
idle: {}
}
const clickAction = () => {
if(playing){
audioPlayer.pause();
}
}
return (
<motion.div className="cursor-pointer" whileTap={{rotate: -10}}>
<motion.div className="cursor-pointer" whileTap={{rotate: -10}} variants={transitions} animate={playing && bpm > 0 ? "playing" : "idle"} onClick={clickAction}>
<svg className={className} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
<g fill={firstColor}>
<path d="M626.75,485.91l20.82,351.94a35.21,35.21,0,0,1-15.49,31.36L487.37,965.35l-.06,0A35.56,35.56,0,0,1,476,970.12a34.85,34.85,0,0,1-23.51-2.87L136.58,809.9a115.45,115.45,0,0,1-63.75-96.49L52,361.46a35.26,35.26,0,0,1,15.23-31.21l145.05-96.36a35.72,35.72,0,0,1,11.27-4.7,34.87,34.87,0,0,1,23.51,2.87L563,389.42a115.41,115.41,0,0,1,63.75,96.49Z"/>
@@ -5,12 +5,13 @@ import { useEffect, useState } from "react";
import { combineLatest, Subscription } from "rxjs";
import { BSLauncherService, LaunchMods } from "renderer/services/bs-launcher.service";
import { ConfigurationService } from "renderer/services/configuration.service";
import { BsmButton } from "../shared/bsm-button.component";
import { BSUninstallerService } from "renderer/services/bs-uninstaller.service";
import { BSVersionManagerService } from "renderer/services/bs-version-manager.service";
import { BsmIcon } from "../svgs/bsm-icon.component";
import { ReactFitty } from "react-fitty";
import { BsmIcon } from "../../svgs/bsm-icon.component";
import { useThemeColor } from 'renderer/hooks/use-theme-color.hook';
import { NavBarItem } from './nav-bar-item.component';
import useFitText from 'use-fit-text';
import Tippy from '@tippyjs/react';
export function BsVersionItem(props: {version: BSVersion}) {
@@ -21,13 +22,14 @@ export function BsVersionItem(props: {version: BSVersion}) {
const bsUninstallerService = BSUninstallerService.getInstance();
const { state } = useLocation() as { state: BSVersion};
const { fontSize, ref } = useFitText();
const [downloading, setDownloading] = useState(false);
const [downloadPercent, setDownloadPercent] = useState(0);
const {firstColor, secondColor} = useThemeColor();
const secondColor = useThemeColor("second-color");
const isActive = (): boolean => {
return props.version?.BSVersion === state?.BSVersion && props?.version.steam === state?.steam && props?.version.name === state?.name;
return props.version?.BSVersion === state?.BSVersion && props?.version.steam === state?.steam && props?.version.oculus === state?.oculus && props?.version.name === state?.name;
}
const handleDoubleClick = () => {
@@ -52,9 +54,9 @@ export function BsVersionItem(props: {version: BSVersion}) {
useEffect(() => {
const subs: Subscription[] = [];
const downloadSub = combineLatest([downloaderService.currentBsVersionDownload$, downloaderService.downloadProgress$]).subscribe(vals => {
if(vals[0]?.BSVersion === props.version.BSVersion && vals[0]?.steam === props.version.steam && vals[0]?.name === props.version.name){
setDownloading(true);
setDownloadPercent(vals[1]);
if(vals[0]?.BSVersion === props.version.BSVersion && vals[0]?.steam === props.version.steam && vals[0].oculus === props.version.oculus && vals[0]?.name === props.version.name){
setDownloading(true);
setDownloadPercent(vals[1]);
}
else{
setDownloading(false);
@@ -65,21 +67,41 @@ export function BsVersionItem(props: {version: BSVersion}) {
return () => { subs.forEach(s => s.unsubscribe()); }
}, []);
const renderIcon = () => {
const classes = "w-[19px] h-[19px] mr-[5px] shrink-0"
if(props.version.steam){
return <BsmIcon icon="steam" className={classes}/>
}
if(props.version.oculus){
return <BsmIcon icon="oculus" className={`${classes} p-[2px] rounded-full bg-main-color-1 text-white dark:bg-white dark:text-black`}/>
}
return <BsmIcon icon="bsNote" className={classes} style={{color: props.version?.color ?? secondColor}}/>
}
return (
<li className={`outline-none relative p-[1px] overflow-hidden rounded-xl flex justify-center items-center mb-1 ${downloading && "nav-item-download"} active:translate-y-[1px]`}>
{downloading && <div className="download-progress absolute top-0 w-full h-full" style={{transform: `translate(${-(100 - downloadPercent)}%, 0)`, background: `linear-gradient(90deg, ${firstColor}, ${secondColor}, ${firstColor}, ${secondColor})`}}/>}
<div className={`wrapper z-[1] px-1 py-[3px] w-full rounded-xl ${downloading && 'bg-white dark:bg-black'} ${!downloading && "hover:bg-light-main-color-3 dark:hover:bg-main-color-3"} ${(isActive() && !downloading) && "bg-light-main-color-3 dark:bg-main-color-3"}`}>
<Link onDoubleClick={handleDoubleClick} to={`/bs-version/${props.version.BSVersion}`} state={props.version} title={props.version.name && `${props.version.BSVersion} - ${props.version.name}`} className="w-full flex items-center justify-start content-center max-w-full">
{props.version.steam && <BsmIcon icon="steam" className="w-[19px] h-[19px] mr-[5px] shrink-0"/>}
{!props.version.steam && <BsmIcon icon="bsNote" className="w-[19px] h-[19px] mr-[5px] shrink-0" style={{color: props.version?.color ?? secondColor}}/>}
<div className="overflow-hidden whitespace-nowrap text-xl dark:text-gray-200 text-gray-800 font-bold tracking-wide">
<ReactFitty maxSize={19} minSize={9} className='align-middle pb-[2px] max-w-full overflow-hidden text-ellipsis'>{props.version.name || props.version.BSVersion}</ReactFitty>
const renderVersionText = () => {
if(props.version.name){
return (
<Tippy content={props.version.BSVersion} placement="right-end" arrow={false} className="font-bold !bg-main-color-3" duration={[200, 0]}>
<div className="h-8 flex items-center dark:text-gray-200 text-gray-800 overflow-hidden">
<div ref={ref} className='whitespace-nowrap font-bold tracking-wide w-full text-center' style={{fontSize}}>{props.version.name}</div>
</div>
</Tippy>
)
}
return (
<div className="h-8 flex items-center text-xl dark:text-gray-200 text-gray-800 font-bold tracking-wide">
<span className='pb-[2px] max-w-full text-ellipsis'>{props.version.BSVersion}</span>
</div>
</Link>
{downloading && <BsmButton onClick={cancel} className="my-1 text-xs text-white rounded-md text-center" withBar={false} text="misc.cancel" typeColor="error"/>}
</div>
</li>
)
}
)
return (
<NavBarItem onCancel={cancel} progress={downloading ? downloadPercent : 0} isActive={isActive() && !downloading} isDownloading={downloading}>
<Link onDoubleClick={handleDoubleClick} to={`/bs-version/${props.version.BSVersion}`} state={props.version} className="w-full flex items-center justify-start content-center max-w-full">
{renderIcon()}
{renderVersionText()}
</Link>
</NavBarItem>
)
}
@@ -0,0 +1,24 @@
import { Link } from "react-router-dom";
import { BsmIcon } from "renderer/components/svgs/bsm-icon.component";
import { useObservable } from "renderer/hooks/use-observable.hook";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { PageStateService } from "renderer/services/page-state.service";
import { NavBarItem } from "./nav-bar-item.component";
export function MapsNavBarItem() {
const pageState = PageStateService.getInstance();
const route = useObservable(pageState.route$);
const color = useThemeColor("first-color");
return (
<NavBarItem isActive={route === "/maps"}>
<Link to="maps" className="w-full flex items-center justify-start content-center max-w-full h-[30px]">
<BsmIcon className="w-[19px] h-[19px] mr-[5px] shrink-0 brightness-125" icon="bsMapDifficulty" style={{color}}/>
<span className="dark:text-gray-200 text-gray-800 font-bold tracking-wide">Maps</span>
</Link>
</NavBarItem>
)
}
@@ -0,0 +1,25 @@
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { BsmButton } from "../../shared/bsm-button.component";
type Props = {
children: JSX.Element,
isDownloading?: boolean
progress?: number,
isActive?: boolean,
onCancel?: (e: React.MouseEvent) => void
}
export function NavBarItem({progress, isDownloading, children, isActive, onCancel}: Props) {
const {firstColor, secondColor} = useThemeColor();
return (
<li className={`outline-none relative p-[1px] overflow-hidden rounded-xl flex justify-center items-center mb-1 ${isDownloading && "nav-item-download"} active:translate-y-[1px]`}>
{isDownloading && <div className="download-progress absolute top-0 w-full h-full" style={{transform: `translate(${-(100 - progress)}%, 0)`, background: `linear-gradient(90deg, ${firstColor}, ${secondColor}, ${firstColor}, ${secondColor})`}}/>}
<div className={`wrapper z-[1] px-1 py-[3px] w-full rounded-xl ${isDownloading && 'bg-white dark:bg-black'} ${!isDownloading && "hover:bg-light-main-color-3 dark:hover:bg-main-color-3"} ${(isActive && !isDownloading) && "bg-light-main-color-3 dark:bg-main-color-3"}`}>
{children}
{isDownloading && <BsmButton onClick={onCancel} className="my-1 text-xs text-white rounded-md text-center" withBar={false} text="misc.cancel" typeColor="error"/>}
</div>
</li>
)
}
@@ -0,0 +1,5 @@
export function NavBarSpliter() {
return (
<span className="block w-[calc(100%-25px)] mx-auto my-2 h-1 rounded-full bg-main-color-3"/>
)
}
@@ -1,10 +1,12 @@
import './nav-bar.component.css'
import { BsVersionItem } from './bs-version-item.component';
import { BsVersionItem } from './nav-bar-items/bs-version-item.component';
import { BSVersionManagerService } from '../../services/bs-version-manager.service';
import { Link } from 'react-router-dom';
import { BsmIcon } from '../svgs/bsm-icon.component';
import { useObservable } from 'renderer/hooks/use-observable.hook';
import { BsManagerIcon } from './bsmanager-icon.component';
import { MapsNavBarItem } from './nav-bar-items/maps-nav-bar-item.component';
import { NavBarSpliter } from './nav-bar-spliter.component';
export function NavBar() {
@@ -16,13 +18,16 @@ export function NavBar() {
<nav id='nav-bar' className='z-10 flex flex-col h-full max-h-full items-center p-1 bg-light-main-color-1 dark:bg-main-color-1'>
<BsManagerIcon className='relative aspect-square w-16 h-16 mb-3'/>
<ol id='versions' className='w-fit max-w-[120px] relative left-[2px] grow overflow-y-hidden scrollbar-track-transparent scrollbar-thin scrollbar-thumb-rounded-full scrollbar-thumb-neutral-900 hover:overflow-y-scroll'>
<MapsNavBarItem/>
<NavBarSpliter/>
{installedVersions && installedVersions.map((version) => <BsVersionItem key={JSON.stringify(version)} version={version}/>)}
</ol>
<div className='w-full p-2 flex flex-col items-center content-center justify-start'>
<Link className='mb-2' to={"blah"}>
<NavBarSpliter/>
<div className='w-full pb-2 flex flex-col items-center content-center justify-start gap-2'>
<Link to="blah">
<BsmIcon icon='add' className='text-blue-500 h-[34px]'/>
</Link>
<Link to={"settings"}>
<Link to="settings">
<BsmIcon icon='settings' className='text-blue-500 h-7'/>
</Link>
</div>
@@ -1,5 +1,5 @@
import { NotificationResult, NotificationType, Notification } from "renderer/services/notification.service"
import { motion } from "framer-motion"
import { motion, PanInfo } from "framer-motion"
import { BsmImage } from "../shared/bsm-image.component";
import BeatRunningImg from "../../../../assets/images/apngs/beat-running.png";
import BeatConflictImg from "../../../../assets/images/apngs/beat-conflict.png";
@@ -10,27 +10,37 @@ import { useTranslation } from "renderer/hooks/use-translation.hook";
export function NotificationItem({resolver, notification}: {resolver?: (value: NotificationResult|string) => void, notification: Notification}) {
const t = useTranslation();
const t = useTranslation();
const renderImage = (() => {
if(notification.type === NotificationType.SUCCESS){ return BeatRunningImg; }
if(notification.type === NotificationType.WARNING){ return BeatWaitingImg; }
if(notification.type === NotificationType.ERROR){ return BeatConflictImg; }
return BeatImpatientImg;
})()
const renderImage = (() => {
if(notification.type === NotificationType.SUCCESS){ return BeatRunningImg; }
if(notification.type === NotificationType.WARNING){ return BeatWaitingImg; }
if(notification.type === NotificationType.ERROR){ return BeatConflictImg; }
return BeatImpatientImg;
})();
const renderNeonColors = (() => {
if(notification.type === NotificationType.SUCCESS){ return "bg-green-400 shadow-green-400"; }
if(notification.type === NotificationType.WARNING){ return "bg-yellow-400 shadow-yellow-400"; }
if(notification.type === NotificationType.ERROR){ return "bg-red-500 shadow-red-500"; }
return "bg-gray-800 shadow-gray-800 dark:bg-white dark:shadow-white";
})()
const renderNeonColors = (() => {
if(notification.type === NotificationType.SUCCESS){ return "bg-green-400 shadow-green-400"; }
if(notification.type === NotificationType.WARNING){ return "bg-yellow-400 shadow-yellow-400"; }
if(notification.type === NotificationType.ERROR){ return "bg-red-500 shadow-red-500"; }
return "bg-gray-800 shadow-gray-800 dark:bg-white dark:shadow-white";
})();
const handleDragEnd = (e: MouseEvent, info: PanInfo) => {
const offset = info.offset.x;
const velocity = info.velocity.x;
if(offset > 30 && velocity > 300){
resolver(NotificationResult.CLOSE);
}
}
return (
<motion.li layout initial={{x: "110%"}} animate={{x: "0%"}} exit={{x:"110%"}} className="relative w-[270px] rounded-md overflow-hidden -left-[285px] mb-4 shadow-md shadow-black bg-gradient-to-br from-light-main-color-2 to-light-main-color-2 dark:from-main-color-2 dark:to-main-color-2 text-gray-800 dark:text-white">
<motion.li layout drag="x" dragConstraints={{left: 0, right: 0}} dragElastic={{left: 0, right: .3}} onDragEnd={handleDragEnd} initial={{x: "110%"}} animate={{x: "0%"}} exit={{x:"110%"}} className="relative w-[290px] rounded-md overflow-hidden -left-[300px] mb-4 cursor-grab active:cursor-grabbing shadow-md shadow-black bg-gradient-to-br from-light-main-color-2 to-light-main-color-2 dark:from-main-color-2 dark:to-main-color-2 text-gray-800 dark:text-white">
<div className="w-full flex flex-col">
<div className="flex items-center justify-start pl-1">
<BsmImage className="h-14 mr-1" image={renderImage}/>
<BsmImage className="h-14 mr-1 pointer-events-none" image={renderImage}/>
<div className="flex flex-col py-2">
<h1 className="font-bold leading-4 tracking-wide pr-5 my-1">{t(notification.title)}</h1>
{notification.desc && <p className="text-xs pr-1">{t(notification.desc)}</p>}
@@ -1,25 +1,25 @@
import { useState } from "react";
import { useRef, useState } from "react";
import { HexColorPicker } from "react-colorful";
import { motion, AnimatePresence } from "framer-motion"
import OutsideClickHandler from "react-outside-click-handler";
import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
export default function SettingColorChooser({color, onChange, pickerClassName}: {color?: string, onChange?: (color: string) => void, pickerClassName?: string}) {
const [colorVisible, setColorVisible] = useState(false);
const ref = useRef(null);
useClickOutside(ref, (e) => setColorVisible(false));
return (
<OutsideClickHandler onOutsideClick={() => setColorVisible(false)}>
<div className="relative cursor-pointer mx-3 h-full aspect-square flex flex-col items-center">
return (
<div ref={ref} className="relative cursor-pointer mx-3 h-full aspect-square flex flex-col items-center">
<span className="z-[1] block h-full w-full border-2 border-white rounded-full" onClick={() => setColorVisible(!colorVisible)} style={{backgroundColor: color}}/>
<AnimatePresence>
{colorVisible &&
<motion.div initial={{opacity: 0}} animate={{opacity: 1}} transition={{duration: .1}} exit={{opacity: 0}} className="fixed flex items-center justify-center translate-y-9 shadow-lg rounded-lg shadow-black">
<div className="absolute w-2/4 aspect-square rotate-45 bg-light-main-color-3 dark:bg-main-color-3 -translate-y-12"></div>
<div className="absolute w-2/4 aspect-square rotate-45 bg-light-main-color-3 dark:bg-main-color-3 -translate-y-12"/>
<HexColorPicker color={color} onChange={onChange} className={pickerClassName} />
</motion.div>
}
</AnimatePresence>
</div>
</OutsideClickHandler>
)
)
}
@@ -1,14 +1,23 @@
import { ReactNode } from "react";
import { useTranslation } from "renderer/hooks/use-translation.hook";
export function SettingContainer({className, title, minorTitle, description, children}: {className?: string, title?: string, minorTitle?: string, description?: string, children?: ReactNode}) {
type Props = {
id?: string,
className?: string,
title?: string,
minorTitle?: string,
description?: string,
children?: ReactNode
}
export function SettingContainer({id, className, title, minorTitle, description, children}: Props) {
const t = useTranslation();
return (
<div className={className || "relative mb-5"}>
<div id={id} className={className || "relative mb-5"}>
{ title && <h1 className="font-bold text-2xl mb-1 tracking-wide">{t(title)}</h1> }
{ minorTitle && <h2 className="font-bold mb-1 tracking-wide text-gray-600 dark:text-gray-400">{t(minorTitle)}</h2> }
{ minorTitle && <h2 className="font-bold mb-1 tracking-wide text-gray-600 dark:text-gray-300">{t(minorTitle)}</h2> }
{ description && <p className="mb-3 text-sm text-gray-600 dark:text-gray-400">{t(description)}</p> }
{ children }
</div>
@@ -1,21 +0,0 @@
import {motion, useCycle} from "framer-motion"
import { useState } from "react"
type Props = {className?: string, nbClickToShow?: number, children?: JSX.Element}
export function SpoilerClick({className, children, nbClickToShow = 5}: Props) {
const [remainingClicks, setRemainingClicks] = useState(nbClickToShow);
const [translateY, setTranslateY] = useState("100%");
const handleClicks = () => {
if(remainingClicks - 1 > 0){ setRemainingClicks(nb => nb - 1); }
setTranslateY(`${(((remainingClicks - 1) / nbClickToShow) * 100)}%`)
}
return (
<motion.div className={className} initial={{y: translateY}} animate={{y: translateY}} transition={{type: "spring", stiffness: 300, mass: 1 }} onClick={handleClicks}>
{children}
</motion.div>
)
}
@@ -0,0 +1,17 @@
.loader {
width: 100%;
height: 100%;
border-radius: 50%;
display: inline-block;
box-sizing: border-box;
animation: rotation 1s linear infinite;
}
@keyframes rotation {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
@@ -0,0 +1,17 @@
import { CSSProperties } from "react"
import "./bsm-basic-spinner.component.css"
type Props = {
className?: string,
style?: CSSProperties,
spinnerClassName?: string,
thikness?: string
}
export function BsmBasicSpinner({className, style, spinnerClassName, thikness = "5px"}: Props) {
return (
<div className={className} style={style}>
<span className={`${spinnerClassName} loader`} style={{border: `${thikness} solid currentColor`, borderBottomColor: "transparent"}}/>
</div>
)
}
@@ -1,16 +1,16 @@
import { BsmIcon, BsmIconType } from "../svgs/bsm-icon.component"
import OutsideClickHandler from 'react-outside-click-handler';
import React, { MutableRefObject, Ref } from "react";
import { useRef, CSSProperties } from "react";
import { BsmImage } from "./bsm-image.component";
import { useTranslation } from "renderer/hooks/use-translation.hook";
import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
import { useThemeColor } from "renderer/hooks/use-theme-color.hook";
import { getCorrectTextColor } from "renderer/helpers/correct-text-color";
type BsmButtonType = "primary"|"success"|"cancel"|"error";
type propsType = {
type Props = {
className?: string,
style?: React.CSSProperties,
style?: CSSProperties,
imgClassName?: string,
iconClassName?: string,
icon?: BsmIconType,
@@ -25,43 +25,44 @@ type propsType = {
typeColor?:BsmButtonType,
color?: string,
title?: string,
iconColor?: string,
textClassName?: string
}
export function BsmButton({className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title}: propsType) {
export function BsmButton({className, style, imgClassName, iconClassName, icon, image, text, type, active, withBar = true, disabled, onClickOutside, onClick, typeColor, color, title, iconColor, textClassName}: Props) {
const t = useTranslation();
const secondColor = useThemeColor("second-color");
const t = useTranslation();
const secondColor = useThemeColor("second-color");
const ref = useRef(null);
useClickOutside(ref, onClickOutside);
const primaryColor = typeColor === "primary" && useThemeColor("first-color");
const primaryColor = typeColor === "primary" && useThemeColor("first-color");
const textColor = (() => {
if(primaryColor){ return getCorrectTextColor(primaryColor); }
return typeColor ? "white" : undefined;
})();
const textColor = (() => {
if(primaryColor){ return getCorrectTextColor(primaryColor); }
return typeColor ? "white" : undefined;
})();
const renderTypeColor = (() => {
if(typeColor === "primary"){ return ""; }
if(!typeColor){ return `bg-light-main-color-2 dark:bg-main-color-2 ${!withBar && "hover:bg-light-main-color-3 dark:hover:bg-main-color-3"}`; }
if(typeColor === "cancel"){ return "bg-gray-500"; }
if(typeColor === "error"){ return "bg-red-500"; }
if(typeColor === "success"){ return "bg-green-500"; }
return "";
})()
const renderTypeColor = (() => {
if(typeColor === "primary"){ return ""; }
if(!typeColor){ return `bg-light-main-color-2 dark:bg-main-color-2 ${!withBar && "hover:brightness-125"}`; }
if(typeColor === "cancel"){ return "bg-gray-500"; }
if(typeColor === "error"){ return "bg-red-500"; }
if(typeColor === "success"){ return "bg-green-500"; }
return "";
})();
return (
<OutsideClickHandler onOutsideClick={e => onClickOutside && onClickOutside(e)}>
<div onClick={onClick} title={t(title)} className={`${className} overflow-hidden cursor-pointer group ${(!withBar && !disabled && (!!typeColor || !!color)) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, backgroundColor: primaryColor || color}}>
{ image && <BsmImage image={image} className={imgClassName}/> }
{ icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"}/> }
{text && (type === "submit" ? <button type="submit" className="w-full h-full" style={{...(!!textColor && {color: textColor})}}>{t(text)}</button> : <span style={{...(!!textColor && {color: `${textColor}`})}}>{t(text)}</span>)}
{ withBar && (
<div className="absolute bottom-0 left-0 w-full h-1 bg-current" style={{color: secondColor}}>
<div className="absolute top-0 left-0 h-full w-full bg-current brightness-50"/>
<div className={`absolute top-0 left-0 h-full w-full bg-inherit -translate-x-full group-hover:translate-x-0 transition-transform shadow-center shadow-current ${active && "translate-x-0"}`}/>
</div>
)}
</div>
</OutsideClickHandler>
)
return (
<div ref={ref} onClick={onClick} title={t(title)} className={`${className} overflow-hidden cursor-pointer group ${(!withBar && !disabled && (!!typeColor || !!color)) && "hover:brightness-[1.15]"} ${disabled && "brightness-75 cursor-not-allowed"} ${renderTypeColor}`} style={{...style, backgroundColor: primaryColor || color}}>
{ image && <BsmImage image={image} className={imgClassName}/> }
{ icon && <BsmIcon icon={icon} className={iconClassName ?? "h-full w-full text-gray-800 dark:text-white"} style={{color: iconColor}}/> }
{text && (type === "submit" ? <button type="submit" className={textClassName || "h-full w-full"} style={{...(!!textColor && {color: textColor})}}>{t(text)}</button> : <span className={textClassName} style={{...(!!textColor && {color: `${textColor}`})}}>{t(text)}</span>)}
{ withBar && (
<div className="absolute bottom-0 left-0 w-full h-1 bg-current" style={{color: secondColor}}>
<div className="absolute top-0 left-0 h-full w-full bg-current brightness-50"/>
<div className={`absolute top-0 left-0 h-full w-full bg-inherit -translate-x-full group-hover:translate-x-0 transition-transform shadow-center shadow-current ${active && "translate-x-0"}`}/>
</div>
)}
</div>
)
}
@@ -1,31 +1,63 @@
import { useState } from "react"
import { BsmIconType } from "../svgs/bsm-icon.component"
import { useRef, useState } from "react"
import { BsmIconType, BsmIcon } from "../svgs/bsm-icon.component"
import { BsmButton } from "./bsm-button.component"
import { BsmIcon } from "../svgs/bsm-icon.component"
import { useTranslation } from "renderer/hooks/use-translation.hook"
import { AnimatePresence } from "framer-motion"
import { useClickOutside } from "renderer/hooks/use-click-outside.hook"
export interface DropDownItem {text: string, icon?: BsmIconType, onClick?: () => void}
type Props = {className?: string, items?: DropDownItem[], align?: "left"|"right", withBar?: boolean, icon?: BsmIconType, buttonClassName?: string, menuTranslationY?: string|number}
type Props = {
className?: string,
items?: DropDownItem[],
align?: "left"|"right"|"center",
withBar?: boolean,
icon?: BsmIconType,
buttonClassName?: string,
menuTranslationY?: string|number
children?: JSX.Element,
text?: string,
textClassName?: string
}
export function BsmDropdownButton({className, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY}: Props) {
export function BsmDropdownButton({className, items, align, withBar = true, icon = "settings", buttonClassName, menuTranslationY, children, text, textClassName}: Props) {
const [expanded, setExpanded] = useState(false)
const t = useTranslation();
const ref = useRef(null)
useClickOutside(ref, () => setExpanded(false));
const defaultButtonClassName = "relative z-[1] p-1 rounded-md text-inherit w-full h-full shadow-md shadow-black"
const handleClickOutside = () => {
if(children){ return; }
setExpanded(false);
}
const alignClass = (() => {
if(align === "center"){ return "right-1/2 origin-top-right translate-x-[50%]" }
if(align === "left"){ return "left-0 origin-top-left"; }
return "right-0 origin-top-right";
})()
return (
<div className={`${className}`}>
<BsmButton onClick={() => setExpanded(!expanded)} className={buttonClassName ?? defaultButtonClassName} icon={icon} active={expanded} onClickOutside={() => {setExpanded(false)}} withBar={withBar}/>
<div className={`py-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] ease-in-out ${align === "left" ? "left-0 origin-top-left" : "right-0 origin-top-right"}`} style={{scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}`}}>
{ items?.map((i, index) => !!i &&(
<div key={index} onClick={!!i.onClick ? i.onClick : undefined} className="flex w-full px-3 py-2 hover:backdrop-brightness-150">
{i.icon && <BsmIcon icon={i.icon} className="h-5 w-5 mr-1 text-inherit"></BsmIcon>}
<div ref={ref} className={className}>
<BsmButton onClick={() => setExpanded(!expanded)} className={buttonClassName ?? defaultButtonClassName} icon={icon} active={expanded} onClickOutside={handleClickOutside} withBar={withBar} text={text}/>
<div className={`py-1 w-fit absolute cursor-pointer top-[calc(100%-4px)] rounded-md bg-inherit text-sm text-gray-800 dark:text-gray-200 shadow-md shadow-black transition-[scale] ease-in-out ${alignClass}`} style={{scale: expanded ? "1" : "0", translate: `0 ${menuTranslationY}`}}>
{ items?.map((i) => i && (
<div key={crypto.randomUUID()} onClick={() => i.onClick?.()} className="flex w-full px-3 py-2 hover:backdrop-brightness-150">
{i.icon && <BsmIcon icon={i.icon} className="h-5 w-5 mr-1 text-inherit"/>}
<span className="w-max">{t(i.text)}</span>
</div>
))}
</div>
{!!children && (
<AnimatePresence>
{expanded && (
children
)}
</AnimatePresence>
)}
</div>
)
}
@@ -1,8 +1,42 @@
import { CSSProperties } from "react";
import { CSSProperties, forwardRef, SyntheticEvent, useState } from "react";
export function BsmImage({className, image, errorImage, placeholder, loading, style}: {className?: string, image: string, errorImage?: string, placeholder?: string, loading?: "lazy"|"eager", style?: CSSProperties}) {
return (
<img className={`${className} pointer-events-none`} src={image} loading={loading} onError={({currentTarget}) => { currentTarget.onerror = null; currentTarget.src = errorImage || "" }} style={{...style, backgroundImage: `url(${placeholder})`, backgroundSize: "cover"}} alt=""/>
)
type Props = {
className?: string,
image: string,
errorImage?: string,
placeholder?: string,
loading?: "lazy"|"eager",
style?: CSSProperties
title?: string,
onClick?: (e: MouseEvent) => void
}
export const BsmImage = forwardRef(({className, image, errorImage, placeholder, loading, style, title, onClick}: Props, ref) => {
const [isLoaded, setIsLoaded] = useState(false);
image = image || (placeholder||errorImage);
const styles: CSSProperties = (() => {
return {
...style,
...(!isLoaded && {backgroundImage: `url(${placeholder})`}),
...(!isLoaded && {backgroundSize: "cover"}),
...(!isLoaded && {backgroundPosition: "center"}),
}
})();
const handleError = (e: SyntheticEvent<HTMLImageElement, Event>) => {
e.currentTarget.onerror = null;
e.currentTarget.src = errorImage || "";
}
const handleLoaded = () => {
setIsLoaded(() => true);
}
return (
// @ts-ignore
<img ref={ref} title={title} className={className} src={image} loading={loading} onLoad={handleLoaded} onError={handleError} style={styles} onClick={(e) => onClick?.(e)} alt=" "/>
)
})
@@ -0,0 +1,23 @@
import { LinkOpenerService } from "renderer/services/link-opener.service"
type Props = {
className?: string,
href?: string,
internal?: boolean
children?: React.ReactNode,
style?: React.CSSProperties
}
export function BsmLink({className, href, children, style, internal}: Props) {
const linkOpener = LinkOpenerService.getInstance();
const openLink = () => {
if(!href){ return; }
linkOpener.open(href, internal);
}
return (
<a className={`${className} ${href && "cursor-pointer"}`} onClick={e => {e.stopPropagation(); openLink()}} style={style}>{children}</a>
)
}
@@ -0,0 +1,28 @@
import { AnimatePresence, motion } from "framer-motion";
import { useRef } from "react";
import { useClickOutside } from "renderer/hooks/use-click-outside.hook";
import { useObservable } from "renderer/hooks/use-observable.hook"
import { LinkOpenerService } from "renderer/services/link-opener.service";
export function BsmIframeView() {
const linkService = LinkOpenerService.getInstance()
const iframeUrl = useObservable(linkService.iframeLink$);
const ref = useRef(null);
useClickOutside(ref, () => linkService.closeIframe());
return (
<AnimatePresence>
{iframeUrl && (
<div className="top-0 absolute w-screen h-screen flex content-center items-center justify-center z-50">
<motion.span className="absolute w-full h-full bg-black top-0 right-0" initial={{opacity: 0}} animate={{opacity: .60}} exit={{opacity: 0}} transition={{duration: .2}}/>
<motion.div ref={ref} className="z-[1] w-[calc(100vw-250px)] h-[calc(100vh-250px)] shadow-black shadow-md rounded-md overflow-hidden bg-black" initial={{y: "100vh"}} animate={{y: 0}} exit={{y: "100vh"}}>
<iframe className="w-full h-full" src={iframeUrl}/>
</motion.div>
</div>
)}
</AnimatePresence>
)
}
@@ -0,0 +1,63 @@
import { getTrackBackground, Range } from "react-range"
import { getCorrectTextColor } from "renderer/helpers/correct-text-color"
import { ThemeColor, useThemeColor } from "renderer/hooks/use-theme-color.hook"
type Props = {
colorType?: ThemeColor
values: number[],
onChange?: (values: number[]) => void,
onFinalChange?: (values: number[]) => void,
min: number,
max: number,
step?: number,
renderLabel?: (value: number) => JSX.Element,
}
export function BsmRange({colorType = "first-color", values, onChange, onFinalChange, min, max, renderLabel, step = 1} : Props) {
const color = useThemeColor(colorType);
const labelTextColor = getCorrectTextColor(color);
return (
<Range
values={values}
min={min}
max={max}
step={step}
onChange={(v) => onChange?.(v)}
onFinalChange={(v) => onFinalChange?.(v)}
renderTrack={({props, children}) => (
<div
onMouseDown={props.onMouseDown}
onTouchStart={props.onTouchStart}
className="w-full rounded-full h-1"
{...props}
style={{
...props.style,
background: getTrackBackground({
values: values,
colors: ["#ccc", color, "#ccc"],
min: min,
max: max
}),
}}
>
{children}
</div>
)}
renderThumb={({ index, props }) => (
<div
className="relative w-4 h-4 rounded-full shadow-center shadow-black brightness-125 flex justify-center outline-none"
{...props}
style={{
...props.style,
backgroundColor: color,
color: labelTextColor,
}}
>
{renderLabel && renderLabel(values[index])}
</div>
)}
/>
)
}
@@ -0,0 +1,31 @@
import { ChangeEvent, CSSProperties } from "react"
import { useTranslation } from "renderer/hooks/use-translation.hook"
type Props = {
className?: string,
style?: CSSProperties,
options?: BsmSelectOption[],
onChange?: (value: string) => void
}
export function BsmSelect({className, style, options, onChange}: Props) {
const t = useTranslation();
const handleChange = (event: ChangeEvent<HTMLSelectElement>) => {
onChange?.(event.target.value);
}
return (
<select className={className} style={style} onChange={handleChange}>
{options && options.map(option => (
<option key={option.value} value={option.value}>{t(option.text)}</option>
))}
</select>
)
}
export interface BsmSelectOption{
text: string,
value: string
}

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