233 Commits

Author SHA1 Message Date
4d1b273335 wip grafana things 2023-05-22 00:05:11 +09:00
2c68489c2f Try to remove usless things but its not usless 2023-05-18 10:54:18 +09:00
b2ea764b96 Add grafana quickstart 2023-05-18 09:57:33 +09:00
Arthur Jamet
45c07b68a0 Merge pull request #200 from Chroma-Case/front/osmd-view 2023-05-08 13:44:27 +01:00
Arthur Jamet
5bc4289bdc Front: Merge 2023-05-08 12:55:16 +01:00
Arthur Jamet
9f17cd9f83 Front: Score View: Back Button On Error 2023-05-07 14:57:06 +01:00
Clément Le Bihan
c82cdc0445 PR fixes 2023-05-06 17:25:49 +02:00
Clément Le Bihan
4d77007010 fixed indent and removing debug comment 2023-05-06 17:25:49 +02:00
Clément Le Bihan
2ece5b44ac added duplicatas removal and reverse order 2023-05-06 17:25:49 +02:00
Clément Le Bihan
1fa3d77e8d added search history in API.ts and displaying it on the HomePage 2023-05-06 17:25:49 +02:00
Clément Le Bihan
7166bb46ed Fixed sessiontoken lock 2023-05-06 15:58:48 +02:00
Arthur Jamet
d0166c6b27 Music: Add Short Music 2023-05-06 14:31:29 +01:00
Arthur Jamet
393ff3c2c7 Front: Remove Log 2023-05-06 14:30:19 +01:00
Arthur Jamet
ae4d1f00d9 Front: Partition: Prevent skip of first note 2023-05-06 14:18:18 +01:00
Arthur Jamet
8a3d4f9c25 Front: Merge 2023-05-06 13:52:01 +01:00
Arthur Jamet
4034d29056 Front: Partition View: Management of long rests 2023-05-05 13:15:35 +01:00
Arthur Jamet
cce560031a Front: Partition View: Set arbitrary bpm if not found in musicxml 2023-05-04 13:06:15 +01:00
Arthur Jamet
f6226ce127 Front: Play View: Prevent double midi setup 2023-05-04 13:03:23 +01:00
Arthur Jamet
20deb7ae99 Front: Partition View: Handle End reached 2023-05-04 13:02:36 +01:00
Arthur Jamet
3c3ed74995 Front: Song Lobby: Fallback if no previous score 2023-05-04 13:02:08 +01:00
Arthur Jamet
c3e8fb1c45 Back: Fix Musicxml route 2023-05-04 11:47:44 +01:00
Arthur Jamet
97be3b9c76 Front: Play View: Fix Loading Flow 2023-05-04 11:47:29 +01:00
Arthur Jamet
5662082048 Front: Get Song History: Actually use API 2023-05-04 11:40:39 +01:00
Arthur Jamet
9dc2389c35 Front: Score View: Fix artist in cards 2023-05-04 11:33:35 +01:00
Arthur Jamet
10f80b6191 Merge pull request #197 from Chroma-Case/front/fix-game-end-flow 2023-05-04 06:49:47 +01:00
Arthur Jamet
b943b9a621 Front: Partition View: Fix speed + play sound from musicxml 2023-05-03 14:35:31 +01:00
Arthur Jamet
fb9467e58e Front: Prevent Crash on Quit and restart 2023-05-02 15:32:57 +01:00
Arthur Jamet
bf4b84e1f8 Front: Partition: Infinite Scroll 2023-05-02 15:04:05 +01:00
Arthur Jamet
03ac681dd6 Front: Partition View: Give rendering div a proper name 2023-05-02 14:16:40 +01:00
Arthur Jamet
9d60993f8d Front: Partition View: Fix disappearing cursor on resize 2023-05-02 14:13:57 +01:00
Arthur Jamet
10e53abfc1 Front: Play View: Draft of Partition View 2023-05-01 15:32:45 +01:00
Arthur Jamet
18cc79f4a2 Front: API: Type Raw responses 2023-05-01 15:32:23 +01:00
Arthur Jamet
5c8e35ba7d Front: Navigation: Add key to routes + logout button on profile error 2023-05-01 15:31:24 +01:00
Arthur Jamet
2db657dd59 Front: Home View: Fix Overlap when empty card grid 2023-04-28 16:53:50 +02:00
Arthur Jamet
cbe8d291dd Front: Remove unused expo configuration files 2023-04-28 14:23:06 +01:00
Arthur Jamet
e0bdd5fd8f Front: Hide Stop Button while game has not started 2023-04-28 13:57:08 +01:00
Arthur Jamet
030cbfc786 Front: Navigation: Prevent Back action in home and score view 2023-04-28 13:56:50 +01:00
Arthur Jamet
d0597f0e95 Score View: Fix Condition 2023-04-26 21:35:02 +02:00
Arthur Jamet
f640b0d6f8 Merge pull request #194 from Chroma-Case/front/link-settings 2023-04-25 18:41:28 +01:00
Arthur Jamet
c005ebbdc9 Front: Merge 2023-04-25 15:55:50 +01:00
Arthur Jamet
cf6b61e0e9 Front: Global Error Handling on User Profile Fetch Error 2023-04-25 15:53:24 +01:00
Arthur Jamet
9a7c4405bb Merge pull request #193 from Chroma-Case/front/play-page-connection 2023-04-24 17:29:17 +01:00
Arthur Jamet
7716c5f9c6 Front: Indet fix 2023-04-24 16:48:45 +01:00
Arthur Jamet
42ab0f6ed6 Merge branch 'scoro_rework' of github.com:Chroma-Case/Chromacase into front/play-page-connection 2023-04-24 16:45:50 +01:00
GitBluub
8628e07be1 me dumb 2023-04-25 00:02:49 +09:00
GitBluub
fa60fca466 cat the diff for the CI 2023-04-24 23:54:18 +09:00
GitBluub
a51aa60e20 fix tests 2023-04-24 23:43:13 +09:00
Arthur Jamet
4b44ef0c11 Front: Remove comment 2023-04-23 15:32:44 +01:00
Arthur Jamet
bb96d57f27 Front: Use API to get settings 2023-04-23 15:30:37 +01:00
Arthur Jamet
8ccc90eceb Front: LoadingView Component 2023-04-23 15:18:07 +01:00
Arthur Jamet
467f440c54 Front: Play View State: Reduce conditional complexity 2023-04-20 12:37:38 +01:00
Arthur Jamet
88cfd1ecde Front: Fix Merge 2023-04-19 13:06:44 +01:00
Arthur Jamet
deaaaac2cd Front: Navigation: Route Props include route info 2023-04-19 06:01:13 +02:00
Clément Le Bihan
6871aaf759 added privacy settings in the settings store and linked to the settings ui 2023-04-19 06:01:13 +02:00
Clément Le Bihan
31d3909e80 fixed the new useNavigation hook usage 2023-04-19 06:01:13 +02:00
Clément Le Bihan
ae36edfff4 now using useSelector with the settings sotre instead of standalone useStates 2023-04-19 06:01:13 +02:00
Clément Le Bihan
fe782a4f94 fixed useQuery some code style and unused hooks 2023-04-19 06:01:13 +02:00
Clément Le Bihan
9d8bb499ba Added midjourey generated images 2023-04-19 06:01:13 +02:00
Clément Le Bihan
968ae149a5 fixed dark theme for big Action button 2023-04-19 06:01:13 +02:00
Clément Le Bihan
50f6fe6851 fixed dark mode for element list baxShadow and hover color 2023-04-19 06:01:13 +02:00
Clément Le Bihan
eddbe6e2be fixed responsive issue with an explicit flex-shrink 2023-04-19 06:01:13 +02:00
Clément Le Bihan
c4bc7c795c added some margin in TabRow navigator because some icons were displayed onto the title 2023-04-19 06:01:13 +02:00
Clément Le Bihan
509d079bce fixed style layout in GuestToUserView and added SignOutGuestWarning translations 2023-04-19 06:01:13 +02:00
Clément Le Bihan
af3da974bf fixed bad naming for gamePlayed user data 2023-04-19 06:01:13 +02:00
Clément Le Bihan
da0b43c348 added missing translation for the profile setting page 2023-04-19 06:01:13 +02:00
Clément Le Bihan
1c76266444 added the front API function to tranform guest account into regular account and added translations 2023-04-19 06:01:13 +02:00
Clément Le Bihan
9033fbe937 Added a tab settings when user is guest to transform guest account into regular account 2023-04-19 06:01:13 +02:00
Clément Le Bihan
34d646021f Added some premium features options to show of possibilities of the ElementList 2023-04-19 06:01:13 +02:00
Clément Le Bihan
3aa104a923 Removed the previous inputs of settings preference view 2023-04-19 06:01:13 +02:00
Clément Le Bihan
047cd054bd hardcoded a width value for range element slider and separated settings in settings preference view into multiple categories 2023-04-19 06:01:13 +02:00
Clément Le Bihan
b0dddbe815 used ElementList to reimplement Settings Preference View and changed callback function name for ElementRange from onValueChange to onChange 2023-04-19 06:01:13 +02:00
Clément Le Bihan
70b506c6c2 Moved Settings Preference View into its dedicated file and started to implement ElementRange 2023-04-19 06:01:13 +02:00
Clément Le Bihan
8ce1beb518 used ElementList in the PrivacyView and using the dedicated file 2023-04-19 06:01:13 +02:00
Clément Le Bihan
4ec8878e8a fixed a bug in ElementToggle and added statte for notifications and ElementList overhaul 2023-04-19 06:01:13 +02:00
Clément Le Bihan
d9ede44d7d moved functionnal NotificationView into it's dedicated file 2023-04-19 06:01:13 +02:00
Clément Le Bihan
0cd8846e2c removed test entries in the SettingsProfileView 2023-04-19 06:01:13 +02:00
Clément Le Bihan
1b63d27f74 fixed a little style issue of ElementList and setting the mail pressed action to navigate to the mail options 2023-04-19 06:01:13 +02:00
Clément Le Bihan
8cde4747a7 added visual arrow when text is pressable 2023-04-19 06:01:13 +02:00
Clément Le Bihan
5a5654d4f5 fix missing disbled feature not working with pressable 2023-04-19 06:01:13 +02:00
Clément Le Bihan
9c8395b578 added the actionClick support for toggle element type 2023-04-19 06:01:13 +02:00
Clément Le Bihan
0ce17054fc Element functions are now split into multiple file and started the ground work for onPress support 2023-04-19 06:01:13 +02:00
Clément Le Bihan
87fecb7522 now using data for every type of Elements (no more node for custom type) and fixed child keys warning 2023-04-19 06:01:13 +02:00
Clément Le Bihan
9d5060fc31 fixed missing isGuest info in the return of the user info from the front API wrapper, added a warning pop over when clicking disconnect while using a guest account 2023-04-19 06:01:13 +02:00
Clément Le Bihan
2f19c0e547 Added the disabled implementation for elements 2023-04-19 06:01:13 +02:00
Clément Le Bihan
c5ba72229f Added a better handling of text and descriptions of the buttons with different sizes 2023-04-19 06:01:13 +02:00
Clément Le Bihan
067f5e711d helperText is working great 2023-04-19 06:01:13 +02:00
Clément Le Bihan
f725a89c0c created separate types for each element type in order to handle the different props more sanely 2023-04-19 06:01:13 +02:00
Clément Le Bihan
69fbbd5e00 implemented the toggle element 2023-04-19 06:01:13 +02:00
Clément Le Bihan
a1a2b77a16 updated missed xp usage in homeview 2023-04-19 06:01:13 +02:00
Clément Le Bihan
deda1f738b fixed text values that weren't seeable in dark mode and styling in the ElementList component 2023-04-19 06:01:13 +02:00
Clément Le Bihan
cf1e98f9e6 added createdAt info in user data and added types of elements to render in the list in order to have a more coherent look and a simpler api for the ElementList user 2023-04-19 06:01:13 +02:00
Clément Le Bihan
124f87c199 starting to implemnt GtkUI inspired Element list 2023-04-19 06:01:13 +02:00
Clément Le Bihan
572bb0056d reamed user metrics into user data and added avatar picture and started to implement the user settings page 2023-04-19 06:01:13 +02:00
Clément Le Bihan
fbf85a635e fixed guest creation API front wrapping 2023-04-19 06:01:13 +02:00
Clément Le Bihan
92e439892d fix various style issues and implemented the mechanics to allow guest creation 2023-04-19 06:01:13 +02:00
Clément Le Bihan
93a2141c7c hiding the navigation bar for startpage, added content on the start page (paragraph & website button) 2023-04-19 06:01:13 +02:00
Clément Le Bihan
dc9f74c047 fixed styling and hiding navigation bar on StartPage and login and register button are navigating to login page 2023-04-19 06:01:13 +02:00
Clément Le Bihan
950e4c7767 the start page view layout is finished 2023-04-19 06:01:13 +02:00
Clément Le Bihan
c9c95be60f removed the scale animation of the title due to a lack of anchorpoint support 2023-04-19 06:01:13 +02:00
Clément Le Bihan
d931d00187 added the register button but the button animations/styling needs to be reworked 2023-04-19 06:01:13 +02:00
Clément Le Bihan
a0040c26ca added the first version of BigActionButton 2023-04-19 06:01:13 +02:00
Clément Le Bihan
728bb3d6a2 added guest support in the API and started the StartPageView layout 2023-04-19 06:01:13 +02:00
Arthur Jamet
84f91e0d7f Front: Play View: Get Score 2023-04-16 14:43:09 +01:00
Arthur Jamet
5a42f098d6 Front: Pull Scorometer 2023-04-16 14:33:52 +01:00
GitBluub
0922e6038b fix assign on dict 2023-04-15 22:54:53 +09:00
GitBluub
c45f425a5d use the info object instead of class properties 2023-04-15 22:48:38 +09:00
Arthur Jamet
2accb7dd72 Merge pull request #190 from Chroma-Case/front/typesafe-navigator
Front: Navigation: Use actual routes to build a typed navigator
2023-04-14 14:27:03 +01:00
Arthur Jamet
db5e62c6ab Front: Navigator: Wrap components in navigator to avoid having to pass additional props 2023-04-14 12:57:13 +01:00
Arthur Jamet
b0e01ffbed Front: Navigation: Use actual routes to build a typed navigator 2023-04-14 11:46:40 +01:00
Arthur Jamet
9cd6c90188 Merge pull request #189 from Chroma-Case/front/typesafe-store 2023-04-13 18:26:34 +01:00
Arthur Jamet
e108bf2c66 Front: Song Lobby: Add 'practice button' 2023-04-13 15:10:50 +01:00
Arthur Jamet
b43979dd58 Front: Store: Fix typesafety 2023-04-13 14:17:54 +01:00
GitBluub
7b20792a51 info object sent on each request 2023-04-13 11:02:39 +09:00
Arthur Jamet
4f9a3a9333 Front: Home Page: Use History 2023-04-12 11:31:20 +01:00
Amaury
a26efefd01 Feature/adc/#50 users settings route (#89)
* #50 - migration 20221023123919_ + route settings

* #50 - migration 20221023123919_ + route settings

* #50 - settings creation at user creation + update migration

* changed settings acces from by id to userId

* deleting the user results in deleting it's associated userSettings row

* pr fixes + robot tests + other minor fixes

* removed useless comments

* added settings endpoint to /auth/me and automated creation to /register

* clean code before merge
2023-04-12 05:32:41 +03:00
Arthur Jamet
ac4d4f6f66 Front: Models: Update History 2023-04-10 10:20:08 +01:00
Arthur Jamet
2764805c04 Front: Play Page: Fix timer 2023-04-10 10:19:28 +01:00
e43a8fd111 Fix duplicated guests 2023-04-09 10:35:10 +02:00
Arthur Jamet
29414b5392 Front: Format Feedback Messages from Scorometer 2023-04-08 12:01:26 +01:00
Arthur Jamet
ef5a74da3b Front: Pull Scorometer 2023-04-08 11:34:45 +01:00
GitBluub
39bb7ced04 send a response on a note on and a note off with separation of duration and timing 2023-04-08 01:26:24 +09:00
Clément Le Bihan
6cc7090360 translated Settings categories names and added icons 2023-04-05 16:33:24 +02:00
Clément Le Bihan
9dfc2881a2 added partyPlayed into the user metrics model and added a dummy profile setting view page 2023-04-05 16:33:24 +02:00
Clément Le Bihan
4e26925113 moved and reformat with tab SettingsView and updated view s with amaury's forms 2023-04-05 16:33:24 +02:00
Clément Le Bihan
46f4ac82a8 Set a dynamic hight for TabRowNavigator 2023-04-05 16:33:24 +02:00
Clément Le Bihan
77a230c944 Added an internal initial view to handle homepage for settings 2023-04-05 16:33:24 +02:00
Clément Le Bihan
fd8b4c59de responsive mecamic is working great but styling still need to be done 2023-04-05 16:33:24 +02:00
Clément Le Bihan
e4d998b0ff added overflow support when there's a lot of options 2023-04-05 16:33:24 +02:00
Clément Le Bihan
7722eba86f TabRowNavigator is now quite usable and almost pleasing to look at 2023-04-05 16:33:24 +02:00
Clément Le Bihan
5ada22d267 added support for icons in front of the names 2023-04-05 16:33:24 +02:00
Clément Le Bihan
8d665175fd started to colorise the layout 2023-04-05 16:33:24 +02:00
Clément Le Bihan
8eb524cc81 v1 of TabRowNavigator 2023-04-05 16:33:24 +02:00
Clément Le Bihan
8728707b28 updated API.ts handling code to update email and password 2023-04-05 16:33:24 +02:00
Clément Le Bihan
1e667813ad fixed: a translation, missing import and updated navigation to actually be on the new settings page 2023-04-05 16:33:24 +02:00
Clément Le Bihan
aa8782a5de fixed merge translations 2023-04-05 16:33:24 +02:00
danis
ac4012087c password check 2023-04-05 16:33:24 +02:00
danis
6cca70a290 removed console.log + better reject message 2023-04-05 16:33:24 +02:00
danis
0fd64bfba0 clean code 2023-04-05 16:33:24 +02:00
danis
1fa43555df 94 - functionnal screens 2023-04-05 16:33:24 +02:00
danis
47629e3938 94 - API methods GET/POST 2023-04-05 16:33:24 +02:00
Chloé CHAUVIN
8abf3e339a [ADD] call the email changing form component 2023-04-05 16:33:24 +02:00
Chloé CHAUVIN
9297a28d7a [ADD] new translations relative to the password modification 2023-04-05 16:33:24 +02:00
Chloé CHAUVIN
44411454b2 [WIP] finished the aesthetic but not functionnal 2023-04-05 16:33:24 +02:00
Chloé CHAUVIN
88dea2784c [ADD] new translations relative to the password modification 2023-04-05 16:33:24 +02:00
Chloé CHAUVIN
6e6dff526b [WIP] finished the aesthetic but not functionnal 2023-04-05 16:33:24 +02:00
Chloé CHAUVIN
185f415e8d [UPD] separated some windows and added the form to change the password 2023-04-05 16:33:24 +02:00
danis
ec4ee5b94a handlers 2023-04-05 16:33:24 +02:00
Arthur Jamet
0e2d2cf51c Merge branch 'main' of github.com:Chroma-Case/Chromacase into front/play-page-connection 2023-04-05 11:14:27 +01:00
Zoe Roux
fb5e313f6f Fix duplicated history (#182) 2023-04-05 15:30:48 +09:00
GitBluub
f1f7500b44 fix: messages in separate file 2023-04-03 01:58:06 +09:00
Arthur Jamet
08494936af Front: Play Page: Use new Scorometer API 2023-04-02 16:11:02 +01:00
Arthur Jamet
e3ba076870 Merge branch 'main' of github.com:Chroma-Case/Chromacase into front/play-page-connection 2023-04-02 15:44:23 +01:00
GitBluub
5ac118efbd format 2023-04-02 23:21:07 +09:00
GitBluub
7882deab0b fix: send better error 2023-04-02 23:21:07 +09:00
GitBluub
58ac90d68d updating tests output for the score 2023-04-02 23:21:07 +09:00
GitBluub
9bb5139f76 changing how to handle message and tests 2023-04-02 23:21:07 +09:00
GitBluub
31771f18ff wip: validated dataclasses and input 2023-04-02 23:21:07 +09:00
GitBluub
a3191eda3c ci 2023-04-02 23:21:07 +09:00
GitBluub
5f34fc4310 ci 2023-04-02 23:21:07 +09:00
GitBluub
86337d4525 ci 2023-04-02 23:21:07 +09:00
GitBluub
a4299aadb9 scorometer shebang bahs 2023-04-02 23:21:07 +09:00
GitBluub
aa0c7d9621 scorometer test CI 2023-04-02 23:21:07 +09:00
GitBluub
73695e2580 logs are sent to stderr 2023-04-02 23:21:07 +09:00
GitBluub
e8f1a34372 every note missed at the end diminish score and test 2023-04-02 23:21:07 +09:00
GitBluub
a7dc6a76e9 hold not enough test 2023-04-02 23:21:07 +09:00
GitBluub
d051d36406 hold too long test 2023-04-02 23:21:07 +09:00
GitBluub
31e46904a8 random miss test 2023-04-02 23:21:07 +09:00
GitBluub
e79fad1208 fix: runner exit code 2023-04-02 23:21:07 +09:00
GitBluub
faf12839bc invalid song test 2023-04-02 23:21:07 +09:00
GitBluub
bebea61036 early and late test 2023-04-02 23:21:07 +09:00
GitBluub
38aa680b82 almost perfect play test 2023-04-02 23:21:07 +09:00
GitBluub
9910f51c2a oops 2023-04-02 23:21:07 +09:00
GitBluub
9d74673cff oops 2023-04-02 23:21:07 +09:00
GitBluub
b9513ad154 oops 2023-04-02 23:21:07 +09:00
GitBluub
7cb01a3cba feat: runner of tests 2023-04-02 23:21:07 +09:00
GitBluub
3a32fcf559 fix: Exception was not json serializable 2023-04-02 23:21:07 +09:00
GitBluub
445b949fa8 format 2023-04-02 23:12:32 +09:00
GitBluub
437d5c7b5c fix: send better error 2023-04-02 23:11:54 +09:00
GitBluub
e7b9accb50 updating tests output for the score 2023-04-02 23:10:15 +09:00
GitBluub
09fd62706b changing how to handle message and tests 2023-04-02 23:10:15 +09:00
GitBluub
b2e11b013c wip: validated dataclasses and input 2023-04-02 23:10:15 +09:00
GitBluub
057726617b ci 2023-04-02 23:10:15 +09:00
GitBluub
b333ec676a ci 2023-04-02 23:10:15 +09:00
GitBluub
9db8e84086 ci 2023-04-02 23:10:15 +09:00
GitBluub
8a741b920b scorometer shebang bahs 2023-04-02 23:10:15 +09:00
GitBluub
702d9bcaef scorometer test CI 2023-04-02 23:10:15 +09:00
GitBluub
165ef44c77 logs are sent to stderr 2023-04-02 23:10:15 +09:00
GitBluub
9edccf1fb4 every note missed at the end diminish score and test 2023-04-02 23:10:15 +09:00
GitBluub
12120fb25a hold not enough test 2023-04-02 23:10:14 +09:00
GitBluub
f60172b160 hold too long test 2023-04-02 23:10:14 +09:00
GitBluub
3da5d927cf random miss test 2023-04-02 23:10:14 +09:00
GitBluub
1b947a580a fix: runner exit code 2023-04-02 23:10:14 +09:00
GitBluub
34c7205cfe invalid song test 2023-04-02 23:10:14 +09:00
GitBluub
c1f8ab51b0 early and late test 2023-04-02 23:10:14 +09:00
GitBluub
da9570da65 almost perfect play test 2023-04-02 23:10:14 +09:00
GitBluub
dad54f81f2 oops 2023-04-02 23:10:14 +09:00
GitBluub
db926a2747 oops 2023-04-02 23:10:14 +09:00
GitBluub
7aec52ee43 oops 2023-04-02 23:10:14 +09:00
GitBluub
71f7dae657 feat: runner of tests 2023-04-02 23:10:14 +09:00
GitBluub
f8bb6ed1c0 fix: Exception was not json serializable 2023-04-02 23:10:14 +09:00
Arthur Jamet
81ac9b91ef Front: Play Page: Timer: Use miliseconds 2023-04-01 13:13:17 +01:00
Arthur Jamet
870489a220 Front: Play Page: Fix connection and message format w/ scorometer 2023-04-01 12:44:01 +01:00
Clément Le Bihan
b8811a7ff7 added env vars into all docker composes (dev & prod) 2023-03-29 18:57:29 +02:00
Clément Le Bihan
8784e8de3c Added support for env vars in nginx.conf.template and fixed websocket proxy 2023-03-29 18:57:29 +02:00
Clément Le Bihan
e9f6adab63 cleanup before merge 2023-03-29 18:55:06 +02:00
Clément Le Bihan
b6feab715b Updated VirtualPiano Iconbutton props to new version 2023-03-29 18:55:06 +02:00
Clément Le Bihan
ed8be27b11 removed trailing line 2023-03-29 18:55:06 +02:00
Clément Le Bihan
64b1355712 implemented first version of the virtual piano inside the play bar 2023-03-29 18:55:06 +02:00
Clément Le Bihan
c29740dc2e fixed a bug in strToKey function where the octaveNumber isn't parsed and filtering correctly highlighted notes to the correct octave (default broadcasted to every octave) 2023-03-29 18:55:06 +02:00
Clément Le Bihan
d094c81418 fixed strToKey issue when discarding the accidental part of the string 2023-03-29 18:55:06 +02:00
Clément Le Bihan
885c819ab5 removed accidental as it's own type and integrated it with note type 2023-03-29 18:55:06 +02:00
Clément Le Bihan
c3d2e0a4e5 updating the showNoteNames policy down to PianoKey 2023-03-29 18:55:06 +02:00
Clément Le Bihan
aa72f34a6c fixed issue with keyToStr function 2023-03-29 18:55:06 +02:00
Clément Le Bihan
efede253dc pretty big changes: added highlighted keys refactored Octave component to use a PianoKeyComponent and updated TS types to enums 2023-03-29 18:55:06 +02:00
Clément Le Bihan
a9cd0f16ae added support for showOctaveNumber property 2023-03-29 18:55:06 +02:00
Clément Le Bihan
6a10ad2398 reworked the octave css layout to get rid of the Zindex added support for showNoteNames policy and added an onhover policy 2023-03-29 18:55:06 +02:00
Clément Le Bihan
f43561460d Added hover and clik effects 2023-03-29 18:55:06 +02:00
Clément Le Bihan
eb100e843b Now displaying Notes correctly on the keys fixed map key issue and positionned correctly black keys using hard coded sizes 2023-03-29 18:55:06 +02:00
Clément Le Bihan
cc364cfe7a revert PianoKey to Note for start and endNote and now start for blackkeys support 2023-03-29 18:55:06 +02:00
Clément Le Bihan
319295d2e5 moved models and utils variables/types into models/Piano.ts to fix recursive include and now handling Notes with PianoKey type 2023-03-29 18:55:06 +02:00
Clément Le Bihan
7bf8f32805 added the foundation for the virtualPiano 2023-03-29 18:55:06 +02:00
Arthur Jamet
7e463662be Front: Wrap IconButton 2023-03-25 22:38:51 +01:00
f788872f9b Fix tests and cleanup api responses 2023-03-24 19:01:34 +09:00
a9574cb75a Make the number of party played increment 2023-03-24 19:01:34 +09:00
f24e43a392 Add robot tests 2023-03-24 19:01:34 +09:00
a0bf718e1d Add guest profiles 2023-03-24 19:01:34 +09:00
Arthur Jamet
cf3c9b8c86 Front: Score View: Responsivity 2023-03-23 19:17:58 +01:00
Arthur Jamet
89d39812a6 Score Page: Fix Color Of 'back' Button 2023-03-23 19:17:58 +01:00
Arthur Jamet
b5584a12d0 Play Page: Fix Dark Theme responsivity 2023-03-23 19:17:58 +01:00
Arthur Jamet
3ca2bdaa90 Song Card: Rework Layout for responsivity 2023-03-23 19:17:58 +01:00
Arthur Jamet
d5b15cee13 Front: Responsive Home Page: Fix items dimensions 2023-03-23 19:17:58 +01:00
Arthur Jamet
8a332ede38 Front: Home Page: Basic Responsive Layout 2023-03-23 19:17:58 +01:00
Arthur Jamet
2bed2e1c64 Front/use search api (#167) 2023-03-08 16:29:30 +00:00
128 changed files with 5792 additions and 1054 deletions

View File

@@ -1,7 +1,10 @@
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_NAME=
POSTGRES_HOST=
DATABASE_URL=
JWT_SECRET=
API_URL=
POSTGRES_USER=user
POSTGRES_PASSWORD=eip
POSTGRES_NAME=chromacase
POSTGRES_HOST=db
DATABASE_URL=postgresql://user:eip@db:5432/chromacase
JWT_SECRET=wow
POSTGRES_DB=chromacase
API_URL=http://localhost:80/api
SCORO_URL=ws://localhost:6543

View File

@@ -1,15 +0,0 @@
> Why do I have a folder named ".expo" in my project?
The ".expo" folder is created when an Expo project is started using "expo start" command.
> What do the files contain?
- "devices.json": contains information about devices that have recently opened this project. This is used to populate the "Development sessions" list in your development builds.
- "packager-info.json": contains port numbers and process PIDs that are used to serve the application to the mobile device/simulator.
- "settings.json": contains the server configuration that is used to serve the application manifest.
> Should I commit the ".expo" folder?
No, you should not share the ".expo" folder. It does not contain any information that is relevant for other developers working on the project, it is specific to your machine.
Upon project creation, the ".expo" folder is already added to your ".gitignore" file.

View File

@@ -1,8 +0,0 @@
{
"hostType": "lan",
"lanType": "ip",
"dev": true,
"minify": false,
"urlRandomness": null,
"https": false
}

View File

@@ -96,6 +96,11 @@ jobs:
docker-compose ps -a
wget --retry-connrefused http://localhost:3000 # /healthcheck
- name: Run scorometer tests
run: |
pip install -r scorometer/requirements.txt
cd scorometer/tests && ./runner.sh
- name: Run robot tests
run: |
pip install -r back/test/robot/requirements.txt
@@ -106,7 +111,7 @@ jobs:
name: results
path: out
- name: Write results to Pull Request and Summarry
- name: Write results to Pull Request and Summary
if: always() && github.event_name == 'pull_request'
uses: joonvena/robotframework-reporter-action@v2.1
with:
@@ -114,7 +119,7 @@ jobs:
gh_access_token: ${{ secrets.GITHUB_TOKEN }}
only_summary: false
- name: Write results to Summarry
- name: Write results to Summary
if: always() && github.event_name != 'pull_request'
uses: joonvena/robotframework-reporter-action@v2.1
with:

4
.gitignore vendored
View File

@@ -11,4 +11,6 @@ report.html
log.html
.expo
node_modules/
./front/coverage
./front/coverage
.venv
grafana/.data

View File

@@ -0,0 +1,20 @@
-- CreateTable
CREATE TABLE "UserSettings" (
"id" SERIAL NOT NULL,
"userId" INTEGER NOT NULL,
"pushNotification" BOOLEAN NOT NULL DEFAULT true,
"emailNotification" BOOLEAN NOT NULL DEFAULT true,
"trainingNotification" BOOLEAN NOT NULL DEFAULT true,
"newsongNotification" BOOLEAN NOT NULL DEFAULT true,
"dataCollection" BOOLEAN NOT NULL DEFAULT true,
"CustomAdds" BOOLEAN NOT NULL DEFAULT true,
"Recommendations" BOOLEAN NOT NULL DEFAULT true,
CONSTRAINT "UserSettings_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "UserSettings_userId_key" ON "UserSettings"("userId");
-- AddForeignKey
ALTER TABLE "UserSettings" ADD CONSTRAINT "UserSettings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "isGuest" BOOLEAN NOT NULL DEFAULT false;

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "User" ADD COLUMN "partyPlayed" INTEGER NOT NULL DEFAULT 0;

View File

@@ -0,0 +1,25 @@
/*
Warnings:
- You are about to drop the column `CustomAdds` on the `UserSettings` table. All the data in the column will be lost.
- You are about to drop the column `Recommendations` on the `UserSettings` table. All the data in the column will be lost.
- You are about to drop the column `dataCollection` on the `UserSettings` table. All the data in the column will be lost.
- You are about to drop the column `newsongNotification` on the `UserSettings` table. All the data in the column will be lost.
*/
-- DropForeignKey
ALTER TABLE "UserSettings" DROP CONSTRAINT "UserSettings_userId_fkey";
-- AlterTable
ALTER TABLE "UserSettings" DROP COLUMN "CustomAdds",
DROP COLUMN "Recommendations",
DROP COLUMN "dataCollection",
DROP COLUMN "newsongNotification",
ADD COLUMN "leaderBoard" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "newSongNotification" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "recommendations" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "showActivity" BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN "weeklyReport" BOOLEAN NOT NULL DEFAULT true;
-- AddForeignKey
ALTER TABLE "UserSettings" ADD CONSTRAINT "UserSettings_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,16 @@
/*
Warnings:
- The primary key for the `LessonHistory` table will be changed. If it partially fails, the table could be left without primary key constraint.
- The primary key for the `SongHistory` table will be changed. If it partially fails, the table could be left without primary key constraint.
*/
-- AlterTable
ALTER TABLE "LessonHistory" DROP CONSTRAINT "LessonHistory_pkey",
ADD COLUMN "id" SERIAL NOT NULL,
ADD CONSTRAINT "LessonHistory_pkey" PRIMARY KEY ("id");
-- AlterTable
ALTER TABLE "SongHistory" DROP CONSTRAINT "SongHistory_pkey",
ADD COLUMN "id" SERIAL NOT NULL,
ADD CONSTRAINT "SongHistory_pkey" PRIMARY KEY ("id");

View File

@@ -10,13 +10,30 @@ datasource db {
}
model User {
id Int @id @default(autoincrement())
username String @unique
password String
email String
LessonHistory LessonHistory[]
SongHistory SongHistory[]
searchHistory SearchHistory[]
id Int @id @default(autoincrement())
username String @unique
password String
email String
isGuest Boolean @default(false)
partyPlayed Int @default(0)
LessonHistory LessonHistory[]
SongHistory SongHistory[]
searchHistory SearchHistory[]
settings UserSettings?
}
model UserSettings {
id Int @id @default(autoincrement())
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
userId Int @unique
pushNotification Boolean @default(true)
emailNotification Boolean @default(true)
trainingNotification Boolean @default(true)
newSongNotification Boolean @default(true)
recommendations Boolean @default(true)
weeklyReport Boolean @default(true)
leaderBoard Boolean @default(true)
showActivity Boolean @default(true)
}
model SearchHistory {
@@ -43,14 +60,13 @@ model Song {
}
model SongHistory {
id Int @id @default(autoincrement())
song Song @relation(fields: [songID], references: [id], onDelete: Cascade, onUpdate: Cascade)
songID Int
user User @relation(fields: [userID], references: [id], onDelete: Cascade, onUpdate: Cascade)
userID Int
score Int
difficulties Json
@@id([songID, userID])
}
model Genre {
@@ -86,12 +102,11 @@ model Lesson {
}
model LessonHistory {
id Int @id @default(autoincrement())
lesson Lesson @relation(fields: [lessonID], references: [id])
lessonID Int
user User @relation(fields: [userID], references: [id])
userID Int
@@id([lessonID, userID])
}
enum Skill {

View File

@@ -7,13 +7,11 @@ import { PrismaModule } from './prisma/prisma.module';
import { AuthModule } from './auth/auth.module';
import { SongModule } from './song/song.module';
import { LessonModule } from './lesson/lesson.module';
import { ArtistController } from './artist/artist.controller';
import { SettingsModule } from './settings/settings.module';
import { ArtistService } from './artist/artist.service';
import { GenreModule } from './genre/genre.module';
import { ArtistModule } from './artist/artist.module';
import { AlbumModule } from './album/album.module';
import { SearchController } from './search/search.controller';
import { SearchService } from './search/search.service';
import { SearchModule } from './search/search.module';
import { HistoryModule } from './history/history.module';
@@ -28,6 +26,7 @@ import { HistoryModule } from './history/history.module';
ArtistModule,
AlbumModule,
SearchModule,
SettingsModule,
HistoryModule,
],
controllers: [AppController],

View File

@@ -8,6 +8,10 @@ import {
Delete,
BadRequestException,
HttpCode,
Put,
InternalServerErrorException,
Patch,
NotFoundException,
} from '@nestjs/common';
import { AuthService } from './auth.service';
import { JwtAuthGuard } from './jwt-auth.guard';
@@ -25,6 +29,10 @@ import {
import { User } from '../models/user';
import { JwtToken } from './models/jwt';
import { LoginDto } from './dto/login.dto';
import { Profile } from './dto/profile.dto';
import { Setting } from 'src/models/setting';
import { UpdateSettingDto } from 'src/settings/dto/update-setting.dto';
import { SettingsService } from 'src/settings/settings.service';
@ApiTags('auth')
@Controller('auth')
@@ -32,12 +40,15 @@ export class AuthController {
constructor(
private authService: AuthService,
private usersService: UsersService,
private settingsService: SettingsService,
) {}
@Post('register')
async register(@Body() registerDto: RegisterDto): Promise<void> {
try {
await this.usersService.createUser(registerDto);
await this.usersService.createUser(registerDto).then((user) => {
this.settingsService.createUserSetting(user.id);
});
} catch {
throw new BadRequestException();
}
@@ -51,13 +62,47 @@ export class AuthController {
return this.authService.login(req.user);
}
@HttpCode(200)
@Post('guest')
async guest(): Promise<JwtToken> {
const user = await this.usersService.createGuest();
return this.authService.login(user);
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'Successfully logged in', type: User })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Get('me')
getProfile(@Request() req: any): User {
return req.user;
async getProfile(@Request() req: any): Promise<User> {
const user = await this.usersService.user({ id: req.user.id });
if (!user) throw new InternalServerErrorException();
return user;
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ description: 'Successfully edited profile', type: User })
@ApiUnauthorizedResponse({ description: 'Invalid token' })
@Put('me')
editProfile(
@Request() req: any,
@Body() profile: Partial<Profile>,
): Promise<User> {
return this.usersService.updateUser({
where: { id: req.user.id },
data: {
// If every field is present, the account is no longuer a guest profile.
// TODO: Add some condition to change a guest account to a normal account, like require a subscription or something like that.
isGuest:
profile.email && profile.username && profile.password
? false
: undefined,
username: profile.username,
password: profile.password,
email: profile.email,
},
});
}
@UseGuards(JwtAuthGuard)
@@ -68,4 +113,29 @@ export class AuthController {
deleteSelf(@Request() req: any): Promise<User> {
return this.usersService.deleteUser({ id: req.user.id });
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({description: 'Successfully edited settings', type: Setting})
@ApiUnauthorizedResponse({description: 'Invalid token'})
@Patch('me/settings')
udpateSettings(
@Request() req: any,
@Body() settingUserDto: UpdateSettingDto): Promise<Setting> {
return this.settingsService.updateUserSettings({
where: { userId: +req.user.id},
data: settingUserDto,
});
}
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({description: 'Successfully edited settings', type: Setting})
@ApiUnauthorizedResponse({description: 'Invalid token'})
@Get('me/settings')
async getSettings(@Request() req: any): Promise<Setting> {
const result = await this.settingsService.getUserSetting({ userId: +req.user.id });
if (!result) throw new NotFoundException();
return result;
}
}

View File

@@ -8,11 +8,13 @@ import { JwtModule } from '@nestjs/jwt';
import { ConfigModule } from '@nestjs/config';
import { ConfigService } from '@nestjs/config';
import { JwtStrategy } from './jwt.strategy';
import { SettingsModule } from 'src/settings/settings.module';
@Module({
imports: [
ConfigModule,
UsersModule,
SettingsModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],

View File

@@ -0,0 +1,16 @@
import { IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';
export class Profile {
@ApiProperty()
@IsNotEmpty()
username: string;
@ApiProperty()
@IsNotEmpty()
password: string;
@ApiProperty()
@IsNotEmpty()
email: string;
}

View File

@@ -6,9 +6,22 @@ import { SongHistoryDto } from './dto/SongHistoryDto';
@Injectable()
export class HistoryService {
constructor(private prisma: PrismaService) { }
constructor(private prisma: PrismaService) {}
async createSongHistoryRecord({ songID, userID, score, difficulties }: SongHistoryDto): Promise<SongHistory> {
async createSongHistoryRecord({
songID,
userID,
score,
difficulties,
}: SongHistoryDto): Promise<SongHistory> {
await this.prisma.user.update({
where: { id: userID },
data: {
partyPlayed: {
increment: 1,
},
},
});
return this.prisma.songHistory.create({
data: {
score,
@@ -23,19 +36,26 @@ export class HistoryService {
id: userID,
},
},
}
},
});
}
async getHistory(playerId: number, { skip, take }: { skip?: number, take?: number }): Promise<SongHistory[]> {
async getHistory(
playerId: number,
{ skip, take }: { skip?: number; take?: number },
): Promise<SongHistory[]> {
return this.prisma.songHistory.findMany({
where: { user: { id: playerId } },
skip,
take,
})
});
}
async createSearchHistoryRecord({ userID, query, type }: SearchHistoryDto): Promise<SearchHistory> {
async createSearchHistoryRecord({
userID,
query,
type,
}: SearchHistoryDto): Promise<SearchHistory> {
return this.prisma.searchHistory.create({
data: {
query,
@@ -45,15 +65,18 @@ export class HistoryService {
id: userID,
},
},
}
},
});
}
async getSearchHistory(playerId: number, { skip, take }: { skip?: number, take?: number }): Promise<SearchHistory[]> {
async getSearchHistory(
playerId: number,
{ skip, take }: { skip?: number; take?: number },
): Promise<SearchHistory[]> {
return this.prisma.searchHistory.findMany({
where: { user: { id: playerId } },
skip,
take,
})
});
}
}

View File

@@ -0,0 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
export class Setting {
@ApiProperty()
id: number;
@ApiProperty()
userId: number;
@ApiProperty()
pushNotification: boolean;
@ApiProperty()
emailNotification: boolean;
@ApiProperty()
trainingNotification: boolean;
@ApiProperty()
newSongNotification: boolean;
@ApiProperty()
recommendations: boolean;
@ApiProperty()
weeklyReport: boolean;
@ApiProperty()
leaderBoard: boolean;
@ApiProperty()
showActivity: boolean;
}

View File

@@ -6,7 +6,9 @@ export class User {
@ApiProperty()
username: string;
@ApiProperty()
password: string;
@ApiProperty()
email: string;
@ApiProperty()
isGuest: boolean;
@ApiProperty()
partyPlayed: number;
}

View File

@@ -0,0 +1,20 @@
import { ApiProperty } from '@nestjs/swagger';
export class UpdateSettingDto {
@ApiProperty()
pushNotification?: boolean;
@ApiProperty()
emailNotification?: boolean;
@ApiProperty()
trainingNotification?: boolean;
@ApiProperty()
newSongNotification?: boolean;
@ApiProperty()
recommendations?: boolean;
@ApiProperty()
weeklyReport?: boolean;
@ApiProperty()
leaderBoard?: boolean;
@ApiProperty()
showActivity?: boolean;
}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SettingsController } from './settings.controller';
describe('SettingsController', () => {
let controller: SettingsController;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [SettingsController],
}).compile();
controller = module.get<SettingsController>(SettingsController);
});
it('should be defined', () => {
expect(controller).toBeDefined();
});
});

View File

@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { SettingsService } from './settings.service';
import { PrismaModule } from 'src/prisma/prisma.module';
@Module({
imports: [PrismaModule],
providers: [SettingsService],
exports: [SettingsService],
})
export class SettingsModule {}

View File

@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SettingsService } from './settings.service';
describe('SettingsService', () => {
let service: SettingsService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SettingsService],
}).compile();
service = module.get<SettingsService>(SettingsService);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
});

View File

@@ -0,0 +1,45 @@
import { Injectable } from '@nestjs/common';
import { Prisma, UserSettings } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
@Injectable()
export class SettingsService {
constructor(private prisma: PrismaService) {}
async getUserSetting(
settingWhereUniqueInput: Prisma.UserSettingsWhereUniqueInput,
): Promise<UserSettings | null> {
return this.prisma.userSettings.findUnique({
where: settingWhereUniqueInput,
});
}
async createUserSetting(userId: number): Promise<UserSettings> {
return this.prisma.userSettings.create({
data: {
user: {
connect: {
id: userId,
}
}
}
})
}
async updateUserSettings(params: {
where: Prisma.UserSettingsWhereUniqueInput;
data: Prisma.UserSettingsUpdateInput;
}): Promise<UserSettings> {
const { where, data } = params;
return this.prisma.userSettings.update({
data,
where,
});
}
async deleteUserSettings(where: Prisma.UserSettingsWhereUniqueInput): Promise<UserSettings> {
return this.prisma.userSettings.delete({
where,
});
}
}

View File

@@ -20,7 +20,7 @@ import { CreateSongDto } from './dto/create-song.dto';
import { SongService } from './song.service';
import { Request } from 'express';
import { Prisma, Song } from '@prisma/client';
import { createReadStream } from 'fs';
import { createReadStream, lstat, promises } from 'fs';
import { ApiTags } from '@nestjs/swagger';
@Controller('song')
@@ -46,7 +46,7 @@ export class SongController {
const song = await this.songService.song({ id });
if (!song) throw new NotFoundException('Song not found');
const file = createReadStream(song.midiPath);
const file = createReadStream(song.musicXmlPath, { encoding: 'binary' });
return new StreamableFile(file);
}

View File

@@ -3,25 +3,28 @@ import {
Get,
Post,
Body,
Patch,
Param,
Delete,
NotFoundException,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { SettingsService } from 'src/settings/settings.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiNotFoundResponse, ApiTags } from '@nestjs/swagger';
import { User } from 'src/models/user';
import { resolve } from 'path';
@ApiTags('users')
@Controller('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}
constructor(private readonly usersService: UsersService, private readonly settingsService: SettingsService) {}
@Post()
create(@Body() createUserDto: CreateUserDto): Promise<User> {
return this.usersService.createUser(createUserDto);
return this.usersService.createUser(createUserDto).then((user) => {
this.settingsService.createUserSetting(user.id);
return user;
}).catch((e) => e);
}
@Get()
@@ -37,17 +40,6 @@ export class UsersController {
return ret;
}
@Patch(':id')
update(
@Param('id') id: string,
@Body() updateUserDto: UpdateUserDto,
): Promise<User> {
return this.usersService.updateUser({
where: { id: +id },
data: updateUserDto,
});
}
@Delete(':id')
remove(@Param('id') id: string): Promise<User> {
return this.usersService.deleteUser({ id: +id });

View File

@@ -2,11 +2,12 @@ import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { PrismaModule } from 'src/prisma/prisma.module';
import { SettingsService } from 'src/settings/settings.service';
@Module({
imports: [PrismaModule],
controllers: [UsersController],
providers: [UsersService],
providers: [UsersService, SettingsService],
exports: [UsersService],
})
export class UsersModule {}

View File

@@ -2,6 +2,7 @@ import { Injectable } from '@nestjs/common';
import { User, Prisma } from '@prisma/client';
import { PrismaService } from 'src/prisma/prisma.service';
import * as bcrypt from 'bcryptjs';
import { randomUUID } from 'crypto';
@Injectable()
export class UsersService {
@@ -39,11 +40,27 @@ export class UsersService {
});
}
async createGuest(): Promise<User> {
return this.prisma.user.create({
data: {
username: `Guest ${randomUUID()}`,
isGuest: true,
// Not realyl clean but better than a separate table or breaking the api by adding nulls.
email: '',
password: '',
},
});
}
async updateUser(params: {
where: Prisma.UserWhereUniqueInput;
data: Prisma.UserUpdateInput;
}): Promise<User> {
const { where, data } = params;
if (typeof data.password === 'string')
data.password = await bcrypt.hash(data.password, 8);
else if (data.password && data.password.set)
data.password = await bcrypt.hash(data.password.set, 8);
return this.prisma.user.update({
data,
where,

View File

@@ -0,0 +1,75 @@
*** Settings ***
Documentation Tests of the /auth route.
... Ensures that the user can authenticate on kyoo.
Resource ../rest.resource
Resource ./auth.resource
*** Test Cases ***
LoginAsGuest
[Documentation] Login as a guest
&{res}= POST /auth/guest
Output
Integer response status 200
String response body access_token
Set Headers {"Authorization": "Bearer ${res.body.access_token}"}
${res}= GET /auth/me
Output
Integer response status 200
Boolean response body isGuest true
Integer response body partyPlayed 0
[Teardown] DELETE /auth/me
TwoGuests
[Documentation] Login as a guest
&{res}= POST /auth/guest
Output
Integer response status 200
String response body access_token
Set Headers {"Authorization": "Bearer ${res.body.access_token}"}
GET /auth/me
Output
Integer response status 200
Boolean response body isGuest true
Integer response body partyPlayed 0
&{res2}= POST /auth/guest
Output
Integer response status 200
String response body access_token
Set Headers {"Authorization": "Bearer ${res2.body.access_token}"}
GET /auth/me
Output
Integer response status 200
Boolean response body isGuest true
Integer response body partyPlayed 0
[Teardown] Run Keywords DELETE /auth/me
... AND Set Headers {"Authorization": "Bearer ${res.body.access_token}"}
... AND DELETE /auth/me
GuestToNormal
[Documentation] Login as a guest and convert to a normal account
&{res}= POST /auth/guest
Output
Integer response status 200
String response body access_token
Set Headers {"Authorization": "Bearer ${res.body.access_token}"}
${res}= GET /auth/me
Output
Integer response status 200
Boolean response body isGuest true
${res}= PUT /auth/me { "username": "toto", "password": "toto", "email": "a@b.c"}
Output
Integer response status 200
String response body username "toto"
Boolean response body isGuest false
[Teardown] DELETE /auth/me

View File

@@ -37,6 +37,40 @@ Create and get an history record
[Teardown] Run Keywords DELETE /users/${userID}
... AND DELETE /song/${song.body.id}
Create and get a duplicated history record
[Documentation] Create an history item
&{song}= POST
... /song
... {"name": "Mama mia", "difficulties": {}, "midiPath": "/musics/Beethoven-125-4.midi", "musicXmlPath": "/musics/Beethoven-125-4.mxl"}
Output
${userID}= RegisterLogin wowuser
&{history}= POST
... /history
... { "userID": ${userID}, "songID": ${song.body.id}, "score": 55, "difficulties": {} }
Output
Integer response status 201
&{history2}= POST
... /history
... { "userID": ${userID}, "songID": ${song.body.id}, "score": 65, "difficulties": {} }
Output
Integer response status 201
&{res}= GET /history
Output
Integer response status 200
Array response body
Integer $[0].userID ${userID}
Integer $[0].songID ${song.body.id}
Integer $[0].score 55
Integer $[1].userID ${userID}
Integer $[1].songID ${song.body.id}
Integer $[1].score 65
[Teardown] Run Keywords DELETE /users/${userID}
... AND DELETE /song/${song.body.id}
Create and get a search history record
[Documentation] Create a search history item
${userID}= RegisterLogin historyqueryuser

View File

@@ -0,0 +1,27 @@
*** Settings ***
Documentation Tests of the /settings route.
... Ensures that the settings CRUD works corectly as well as the automation with the user creation.
Resource ../rest.resource
Resource ../auth/auth.resource
*** Test Cases ***
Get settings
[Documentation] Create a user and get associated settings
${userID}= RegisterLogin 2na-min-faranssa-wa-2na-adrus-allu3'at-al3rabia
&{get}= GET /auth/me/settings/
Output
Should Be True ${get.body.emailNotification}
Integer response status 200
[Teardown] DELETE /users/${userID}
Patch settingspushNotification
${userID}= RegisterLogin 2na-min-faranssa-wa-2na-adrus-allu3'at-al3rabia
&{patch}= PATCH
... /auth/me/settings/
... {"pushNotification": true, "emailNotification": true, "trainingNotification": true, "newSongNotification": true, "recommendations": true, "weeklyReport": true, "leaderBoard": false, "showActivity": true}
Output
Should Not Be True ${patch.body.leaderBoard}
Integer response status 200
[Teardown] DELETE /users/${userID}

View File

@@ -40,14 +40,17 @@ services:
ports:
- "5432:5432"
front:
build:
context: ./front
dockerfile: Dockerfile.dev
ports:
- "19006:19006"
volumes:
- ./front:/app
depends_on:
- "back"
env_file:
- .env
build:
context: ./front
dockerfile: Dockerfile.dev
environment:
- SCOROMETER_URL=http://scorometer:6543/
- NGINX_PORT=80
ports:
- "19006:19006"
volumes:
- ./front:/app
depends_on:
- "back"
env_file:
- .env

View File

@@ -33,6 +33,10 @@ services:
front:
image: ghcr.io/chroma-case/front:main
environment:
- API_URL=http://back:3000/
- SCOROMETER_URL=http://scorometer:6543/
- NGINX_PORT=80
ports:
- "80:80"
depends_on:

View File

@@ -39,6 +39,10 @@ services:
args:
- API_URL=${API_URL}
- SCORO_URL=${SCORO_URL}
environment:
- API_URL=http://back:3000/
- SCOROMETER_URL=http://scorometer:6543/
- NGINX_PORT=80
ports:
- "80:80"
depends_on:

View File

@@ -10,16 +10,20 @@ import Constants from "expo-constants";
import store from "./state/Store";
import { Platform } from "react-native";
import { en } from "./i18n/Translations";
import { useQuery, QueryClient } from "react-query";
import { QueryClient } from "react-query";
import UserSettings from "./models/UserSettings";
import { PartialDeep } from "type-fest";
import SearchHistory from "./models/SearchHistory";
type AuthenticationInput = { username: string; password: string };
type RegistrationInput = AuthenticationInput & { email: string };
export type AccessToken = string;
type FetchParams = {
route: string;
body?: Object;
method?: "GET" | "POST" | "DELETE";
method?: "GET" | "POST" | "DELETE" | "PATCH" | "PUT";
// If true, No JSON parsing is done, the raw response's content is returned
raw?: true;
};
@@ -32,9 +36,9 @@ export class APIError extends Error {
constructor(
message: string,
public status: number,
// Set the message to the correct error this is a placeholder
// Set the message to the correct error this is a placeholder
// when the error is only used internally (middleman)
public userMessage : keyof typeof en = "unknownError"
public userMessage: keyof typeof en = "unknownError"
) {
super(message);
}
@@ -53,7 +57,8 @@ const dummyIllustrations = [
"https://upload.wikimedia.org/wikipedia/en/b/ba/David_Guetta_2U.jpg",
];
const getDummyIllustration = () => dummyIllustrations[Math.floor(Math.random() * dummyIllustrations.length)];
const getDummyIllustration = () =>
dummyIllustrations[Math.floor(Math.random() * dummyIllustrations.length)];
// we will need the same thing for the scorometer API url
const baseAPIUrl =
@@ -62,7 +67,7 @@ const baseAPIUrl =
: Constants.manifest?.extra?.apiUrl;
export default class API {
private static async fetch(params: FetchParams) {
public static async fetch(params: FetchParams) {
const jwtToken = store.getState().user.accessToken;
const header = {
"Content-Type": "application/json",
@@ -108,7 +113,8 @@ export default class API {
.catch((e) => {
if (!(e instanceof APIError)) throw e;
if (e.status == 401) throw new APIError("invalidCredentials", 401, "invalidCredentials");
if (e.status == 401)
throw new APIError("invalidCredentials", 401, "invalidCredentials");
throw e;
});
}
@@ -125,24 +131,38 @@ export default class API {
body: registrationInput,
method: "POST",
});
// In the Future we should move autheticate out of this function
// and maybe create a new function to create and login in one go
return API.authenticate({
username: registrationInput.username,
password: registrationInput.password,
});
}
public static async createAndGetGuestAccount(): Promise<AccessToken> {
let response = await API.fetch({
route: "/auth/guest",
method: "POST",
});
if (!response.access_token)
throw new APIError("No access token", response.status);
return response.access_token;
}
public static async transformGuestToUser(registrationInput: RegistrationInput): Promise<void> {
await API.fetch({
route: "/auth/me",
body: registrationInput,
method: "PUT",
});
}
/***
* Retrieve information of the currently authentified user
*/
public static async getUserInfo(): Promise<User> {
let me = await API.fetch({
route: "/auth/me",
});
// /auth/me only returns username and id (it needs to be changed)
let user = await API.fetch({
route: `/users/${me.id}`,
route: "/auth/me",
});
// this a dummy settings object, we will need to fetch the real one from the API
@@ -150,33 +170,55 @@ export default class API {
id: user.id as number,
name: (user.username ?? user.name) as string,
email: user.email as string,
xp: 0,
premium: false,
metrics: {},
settings: {
preferences: {
deviceId: 1,
micVolume: 10,
theme: "system",
lang: "fr",
difficulty: "beg",
colorBlind: false,
},
notifications: {
pushNotif: false,
emailNotif: false,
trainNotif: false,
newSongNotif: false,
},
privacy: {
dataCollection: true,
customAd: true,
recommendation: true,
},
},
isGuest: user.isGuest as boolean,
data: {
gamesPlayed: user.partyPlayed as number,
xp: 0,
createdAt: new Date("2023-04-09T00:00:00.000Z"),
avatar:
"https://imgs.search.brave.com/RnQpFhmAFvuQsN_xTw7V-CN61VeHDBg2tkEXnKRYHAE/rs:fit:768:512:1/g:ce/aHR0cHM6Ly96b29h/c3Ryby5jb20vd3At/Y29udGVudC91cGxv/YWRzLzIwMjEvMDIv/Q2FzdG9yLTc2OHg1/MTIuanBn",
}
} as User;
}
public static async getUserSettings(): Promise<UserSettings> {
const settings = await API.fetch({
route: "/auth/me/settings",
});
return {
notifications: {
pushNotif: settings.pushNotification,
emailNotif: settings.emailNotification,
trainNotif: settings.trainingNotification,
newSongNotif: settings.newSongNotification
},
recommendations: settings.recommendations,
weeklyReport: settings.weeklyReport,
leaderBoard: settings.leaderBoard,
showActivity: settings.showActivity
};
}
public static async updateUserSettings(settings: PartialDeep<UserSettings>): Promise<void> {
const dto = {
pushNotification: settings.notifications?.pushNotif,
emailNotification: settings.notifications?.emailNotif,
trainingNotification: settings.notifications?.trainNotif,
newSongNotification: settings.notifications?.newSongNotif,
recommendations: settings.recommendations,
weeklyReport: settings.weeklyReport,
leaderBoard: settings.leaderBoard,
showActivity: settings.showActivity,
}
return API.fetch({
method: 'PATCH',
route: '/auth/me/settings',
body: dto
});
}
public static async getUserSkills() {
return {
pedalsCompetency: Math.random() * 100,
@@ -202,16 +244,19 @@ export default class API {
});
// this is a dummy illustration, we will need to fetch the real one from the API
return songs.data.map((song: any) => ({
id: song.id as number,
name: song.name as string,
artistId: song.artistId as number,
albumId: song.albumId as number,
genreId: song.genreId as number,
details: song.difficulties,
cover: getDummyIllustration(),
metrics: {},
} as Song));
return songs.data.map(
(song: any) =>
({
id: song.id as number,
name: song.name as string,
artistId: song.artistId as number,
albumId: song.albumId as number,
genreId: song.genreId as number,
details: song.difficulties,
cover: getDummyIllustration(),
metrics: {},
} as Song)
);
}
/**
@@ -232,14 +277,13 @@ export default class API {
genreId: song.genreId as number,
details: song.difficulties,
cover: getDummyIllustration(),
metrics: {},
} as Song;
}
/**
* Retrive a song's midi partition
* @param songId the id to find the song
*/
public static async getSongMidi(songId: number): Promise<any> {
public static async getSongMidi(songId: number): Promise<ArrayBuffer> {
return API.fetch({
route: `/song/${songId}/midi`,
raw: true,
@@ -250,7 +294,7 @@ export default class API {
* Retrive a song's musicXML partition
* @param songId the id to find the song
*/
public static async getSongMusicXML(songId: number): Promise<any> {
public static async getSongMusicXML(songId: number): Promise<ArrayBuffer> {
return API.fetch({
route: `/song/${songId}/musicXml`,
raw: true,
@@ -288,11 +332,9 @@ export default class API {
* @param songId the id to find the song
*/
public static async getSongHistory(songId: number): Promise<SongHistory[]> {
return [67, 4578, 2, 9990].map((value) => ({
songId: songId,
userId: 1,
score: value,
}));
return API.fetch({
route: `/history`,
}).then((data: SongHistory[]) => data.filter((entry) => entry.songID == songId))
}
/**
@@ -300,7 +342,9 @@ export default class API {
* @param query the string used to find the songs
*/
public static async searchSongs(query: string): Promise<Song[]> {
return Promise.all([1, 5, 2].map(API.getSong));
return API.fetch({
route: `/search/guess/song/${query}`,
});
}
/**
@@ -321,12 +365,16 @@ export default class API {
* Retrieve the authenticated user's search history
* @param lessonId the id to find the lesson
*/
public static async getSearchHistory(): Promise<Song[]> {
const queryClient = new QueryClient();
let songs = await queryClient.fetchQuery(["API", "allsongs"], API.getAllSongs);
const shuffled = [...songs].sort(() => 0.5 - Math.random());
public static async getSearchHistory(): Promise<SearchHistory[]> {
const tmp = await this.fetch({
route: "/history/search",
});
return shuffled.slice(0, 2);
return tmp.map((value: any) => ({
query: value.query,
userID: value.userId,
id: value.id,
}));
}
/**
@@ -340,12 +388,10 @@ export default class API {
/**
* Retrieve the authenticated user's play history
*/
public static async getUserPlayHistory(): Promise<Song[]> {
const queryClient = new QueryClient();
let songs = await queryClient.fetchQuery(["API", "allsongs"], API.getAllSongs);
const shuffled = [...songs].sort(() => 0.5 - Math.random());
return shuffled.slice(0, 3);
public static async getUserPlayHistory(): Promise<SongHistory[]> {
return this.fetch({
route: '/history'
});
}
/**
@@ -414,4 +460,38 @@ export default class API {
],
];
}
public static async updateUserEmail(newEmail: string): Promise<User> {
const rep = await API.fetch({
route: "/auth/me",
method: "PUT",
body: {
email: newEmail,
},
});
if (rep.error) {
throw new Error(rep.error);
}
return rep;
}
public static async updateUserPassword(
oldPassword: string,
newPassword: string
): Promise<User> {
const rep = await API.fetch({
route: "/auth/me",
method: "PUT",
body: {
oldPassword: oldPassword,
password: newPassword,
},
});
if (rep.error) {
throw new Error(rep.error);
}
return rep;
}
}

View File

@@ -3,12 +3,14 @@
# Build the app
FROM node:16-alpine as build
WORKDIR /app
COPY package.json yarn.lock ./
RUN yarn install
# install expo cli
RUN yarn global add expo-cli@6.0.5
# add sharp-cli (^2.1.0) for faster image processing
RUN yarn global add sharp-cli@^2.1.0
COPY package.json yarn.lock ./
RUN yarn install
COPY . .
ARG API_URL
ENV API_URL=$API_URL
@@ -20,4 +22,6 @@ RUN expo build:web
# Serve the app
FROM nginx:1.21-alpine
COPY --from=build /app/web-build /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY nginx.conf.template /etc/nginx/conf.d/default.conf.template
CMD envsubst '$API_URL $SCOROMETER_URL $NGINX_PORT' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'

View File

@@ -1,63 +1,130 @@
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import React from 'react';
import { NativeStackScreenProps, createNativeStackNavigator } from '@react-navigation/native-stack';
import { NavigationProp, ParamListBase, useNavigation as navigationHook } from "@react-navigation/native";
import React, { useEffect } from 'react';
import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
import { RootState, useSelector } from './state/Store';
import { translate } from './i18n/i18n';
import { useDispatch } from 'react-redux';
import { Translate, translate } from './i18n/i18n';
import SongLobbyView from './views/SongLobbyView';
import AuthenticationView from './views/AuthenticationView';
import StartPageView from './views/StartPageView';
import HomeView from './views/HomeView';
import SearchView from './views/SearchView';
import SetttingsNavigator from './views/SettingsView';
import SetttingsNavigator from './views/settings/SettingsView';
import { useQuery } from 'react-query';
import API from './API';
import PlayView from './views/PlayView';
import ScoreView from './views/ScoreView';
import { Center } from 'native-base';
import LoadingComponent from './components/Loading';
import { LoadingView } from './components/Loading';
import ProfileView from './views/ProfileView';
import useColorScheme from './hooks/colorScheme';
import { Button, Center, VStack } from 'native-base';
import { unsetAccessToken } from './state/UserSlice';
import TextButton from './components/TextButton';
const Stack = createNativeStackNavigator();
export const protectedRoutes = <>
<Stack.Screen name="Home" component={HomeView} options={{ title: translate('welcome') }} />
<Stack.Screen name="Settings" component={SetttingsNavigator} options={{ title: 'Settings' }} />
<Stack.Screen name="Song" component={SongLobbyView} options={{ title: translate('play') }} />
<Stack.Screen name="Play" component={PlayView} options={{ title: translate('play') }} />
<Stack.Screen name="Score" component={ScoreView} options={{ title: translate('score') }} />
<Stack.Screen name="Search" component={SearchView} options={{ title: translate('search') }} />
<Stack.Screen name="User" component={ProfileView} options={{ title: translate('user') }} />
</>;
const protectedRoutes = () => ({
Home: { component: HomeView, options: { title: translate('welcome'), headerLeft: null } },
Play: { component: PlayView, options: { title: translate('play') } },
Settings: { component: SetttingsNavigator, options: { title: 'Settings' } },
Song: { component: SongLobbyView, options: { title: translate('play') } },
Score: { component: ScoreView, options: { title: translate('score'), headerLeft: null } },
Search: { component: SearchView, options: { title: translate('search') } },
User: { component: ProfileView, options: { title: translate('user') } },
}) as const;
export const publicRoutes = <React.Fragment>
<Stack.Screen name="Login" component={AuthenticationView} options={{ title: translate('signInBtn')}} />
</React.Fragment>;
const publicRoutes = () => ({
Start: { component: StartPageView, options: { title: "Chromacase", headerShown: false } },
Login: { component: AuthenticationView, options: { title: translate('signInBtn') } },
Oops: { component: ProfileErrorView, options: { title: 'Oops', headerShown: false } },
}) as const;
type Route<Props = any> = {
component: (arg: RouteProps<Props>) => JSX.Element | (() => JSX.Element),
options: any
}
type OmitOrUndefined<T, K extends string> = T extends undefined ? T : Omit<T, K>
type RouteParams<Routes extends Record<string, Route>> = {
[RouteName in keyof Routes]: OmitOrUndefined<Parameters<Routes[RouteName]['component']>[0], keyof NativeStackScreenProps<{}>>;
}
type PrivateRoutesParams = RouteParams<ReturnType<typeof protectedRoutes>>;
type PublicRoutesParams = RouteParams<ReturnType<typeof publicRoutes>>;
type AppRouteParams = PrivateRoutesParams & PublicRoutesParams;
const Stack = createNativeStackNavigator<AppRouteParams & { Loading: never }>();
const RouteToScreen = <T extends {}, >(component: Route<T>['component']) => (props: NativeStackScreenProps<T & ParamListBase>) =>
<>
{component({ ...props.route.params, route: props.route } as Parameters<Route<T>['component']>[0])}
</>
const routesToScreens = (routes: Partial<Record<keyof AppRouteParams, Route>>) => Object.entries(routes)
.map(([name, route], routeIndex) => (
<Stack.Screen
key={'route-' + routeIndex}
name={name as keyof AppRouteParams}
options={route.options}
component={RouteToScreen(route.component)}
/>
))
const ProfileErrorView = (props: { onTryAgain: () => any }) => {
const dispatch = useDispatch();
return <Center style={{ flexGrow: 1 }}>
<VStack space={3}>
<Translate translationKey='userProfileFetchError'/>
<Button onPress={props.onTryAgain}><Translate translationKey='tryAgain'/></Button>
<TextButton onPress={() => dispatch(unsetAccessToken())}
colorScheme="error" variant='outline'
translate={{ translationKey: 'signOutBtn' }}
/>
</VStack>
</Center>
}
export const Router = () => {
const dispatch = useDispatch();
const accessToken = useSelector((state: RootState) => state.user.accessToken);
const userProfile = useQuery(['user', 'me', accessToken], () => API.getUserInfo(), {
retry: 1,
refetchOnWindowFocus: false
refetchOnWindowFocus: false,
onError: (err) => {
if (err.status === 401) {
dispatch(unsetAccessToken());
}
},
});
const colorScheme = useColorScheme();
useEffect(() => {
if (accessToken) {
userProfile.refetch();
}
}, [accessToken]);
return (
<NavigationContainer theme={colorScheme == 'light'
? DefaultTheme
: DarkTheme
}>
<Stack.Navigator>
{ userProfile.isLoading && !userProfile.data ?
<Stack.Screen name="Loading" component={() =>
<Center style={{ flexGrow: 1 }}>
<LoadingComponent/>
</Center>
}/>
: userProfile.isSuccess && accessToken
? protectedRoutes
: publicRoutes
{ userProfile.isError && accessToken && !userProfile.isLoading
? <Stack.Screen name="Oops" component={RouteToScreen(() => <ProfileErrorView onTryAgain={() => userProfile.refetch()}/>)}/>
: userProfile.isLoading && !userProfile.data ?
<Stack.Screen name="Loading" component={RouteToScreen(LoadingView)}/>
: routesToScreens(userProfile.isSuccess && accessToken
? protectedRoutes()
: publicRoutes())
}
</Stack.Navigator>
</NavigationContainer>
);
}
export type RouteProps<T> = T & Pick<NativeStackScreenProps<T & ParamListBase>, 'route'>;
export const useNavigation = () => navigationHook<NavigationProp<AppRouteParams>>();

View File

@@ -0,0 +1,149 @@
import React from "react";
import {
Box,
Center,
Heading,
View,
Image,
Text,
Pressable,
useBreakpointValue,
Icon,
Row,
PresenceTransition,
} from "native-base";
import { StyleProp, ViewStyle } from "react-native";
import useColorScheme from "../hooks/colorScheme";
type BigActionButtonProps = {
title: string;
subtitle: string;
image: string;
style?: StyleProp<ViewStyle>;
iconName?: string;
iconProvider?: any;
onPress: () => void;
};
const BigActionButton = ({
title,
subtitle,
image,
style,
iconName,
iconProvider,
onPress,
}: BigActionButtonProps) => {
const screenSize = useBreakpointValue({ base: "small", md: "big" });
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
return (
<Pressable onPress={onPress} style={style}>
{({ isHovered, isPressed }) => {
return (
<Box
style={{
width: "100%",
height: "100%",
position: "relative",
borderRadius: 10,
overflow: "hidden",
}}
>
<PresenceTransition
style={{
width: "100%",
height: "100%",
}}
visible={isHovered}
initial={{
scale: 1,
}}
animate={{
scale: 1.1,
}}
>
<Image
source={{ uri: image }}
alt="image"
style={{
width: "100%",
height: "100%",
resizeMode: "cover",
}}
/>
</PresenceTransition>
<PresenceTransition
style={{
height: "100%",
}}
visible={isHovered}
initial={{
translateY: -40,
opacity: 0.8,
}}
animate={{
translateY: -85,
opacity: 1,
}}
>
<Box
style={{
position: "absolute",
left: "0",
width: "100%",
height: "100%",
backgroundColor: isDark ? "black" : "white",
padding: "10px",
}}
>
<Row>
<Icon
as={iconProvider}
name={iconName}
size={screenSize === "small" ? "sm" : "md"}
color={isDark ? "white" : "black"}
marginRight="10px"
/>
<Heading
fontSize={screenSize === "small" ? "md" : "xl"}
isTruncated
>
{title}
</Heading>
</Row>
{isHovered && (
<PresenceTransition
visible={isHovered}
initial={{
opacity: 0,
translateY: 10,
}}
animate={{
opacity: 1,
translateY: 0,
}}
>
<Text
fontSize={screenSize === "small" ? "sm" : "md"}
isTruncated
noOfLines={2}
>
{subtitle}
</Text>
</PresenceTransition>
)}
</Box>
</PresenceTransition>
</Box>
);
}}
{/* The text should be visible on the bottom left corner and when hovering the
button the image will darken and the subtitle will be show in a transition */}
</Pressable>
);
};
export default BigActionButton;

View File

@@ -2,7 +2,6 @@ import { useTheme, Box, Pressable } from 'native-base';
import React from 'react';
import { useColorScheme } from 'react-native';
import { useSelector } from 'react-redux';
import { SettingsState } from '../state/SettingsSlice';
import { RootState } from '../state/Store';
export const CardBorderRadius = 10;
@@ -15,7 +14,7 @@ const cardBorder = (theme: ReturnType<typeof useTheme>) => ({
const Card = (props: Parameters<typeof Box>[0] & { onPress: () => void }) => {
const theme = useTheme();
const colorScheme: SettingsState['colorScheme'] = useSelector((state: RootState) => state.settings.settings.colorScheme);
const colorScheme = useSelector((state: RootState) => state.settings.local.colorScheme);
const systemColorMode = useColorScheme();
return <Pressable onPress={props.onPress}>

View File

@@ -1,4 +1,4 @@
import { useNavigation } from "@react-navigation/core";
import { useNavigation } from "../Navigation";
import { HStack, VStack, Text, Progress } from "native-base";
import { translate } from "../i18n/i18n";
import Card from './Card';

View File

@@ -0,0 +1,30 @@
import React from "react";
import { ElementProps } from "./ElementList";
import { RawElement } from "./RawElement";
import { Pressable } from "native-base";
export const Element = (props: ElementProps) => {
let actionFunction = null as null | Function;
switch (props.type) {
case "text":
actionFunction = props.data?.onPress;
break;
case "toggle":
actionFunction = props.data?.onToggle;
break;
default:
break;
}
if (!props?.disabled && actionFunction) {
return (
<Pressable onPress={actionFunction}>
{({ isHovered }) => {
return <RawElement element={props} isHovered={isHovered} />;
}}
</Pressable>
);
}
return <RawElement element={props} />;
};

View File

@@ -0,0 +1,63 @@
import React from "react";
import { StyleProp, ViewStyle } from "react-native";
import { Element } from "./Element";
import useColorScheme from "../../hooks/colorScheme";
import {
ElementTextProps,
ElementToggleProps,
ElementDropdownProps,
ElementRangeProps,
ElementType,
} from "./ElementTypes";
import {
Box,
Column,
Divider,
} from "native-base";
export type ElementProps = {
title: string;
icon?: React.ReactNode;
type?: ElementType;
helperText?: string;
description?: string;
disabled?: boolean;
data?:
| ElementTextProps
| ElementToggleProps
| ElementDropdownProps
| ElementRangeProps
| React.ReactNode;
};
type ElementListProps = {
elements: ElementProps[];
style?: StyleProp<ViewStyle>;
};
const ElementList = ({ elements, style }: ElementListProps) => {
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const elementStyle = {
borderRadius: 10,
boxShadow: isDark ? "0px 0px 3px 0px rgba(255,255,255,0.6)" : "0px 0px 3px 0px rgba(0,0,0,0.4)",
overflow: "hidden",
};
return (
<Column style={[style, elementStyle]}>
{elements.map((element, index, __) => (
<Box key={element.title}>
<Element {...element} />
{ index < elements.length - 1 &&
<Divider />
}
</Box>
))}
</Column>
);
};
export default ElementList;

View File

@@ -0,0 +1,137 @@
import { Select, Switch, Text, Icon, Row, Slider } from "native-base";
import { MaterialIcons } from "@expo/vector-icons";
export type ElementType =
| "custom"
| "default"
| "text"
| "toggle"
| "dropdown"
| "range";
export type DropdownOption = {
label: string;
value: string;
};
export type ElementTextProps = {
text: string;
onPress?: () => void;
};
export type ElementToggleProps = {
onToggle: () => void;
value: boolean;
defaultValue?: boolean;
};
export type ElementDropdownProps = {
options: DropdownOption[];
onSelect: (value: string) => void;
value: string;
defaultValue?: string;
};
export type ElementRangeProps = {
onChange: (value: number) => void;
value: number;
defaultValue?: number;
min: number;
max: number;
step?: number;
};
export const getElementTextNode = (
{ text, onPress }: ElementTextProps,
disabled: boolean
) => {
return (
<Row
style={{
alignItems: "center",
}}
>
<Text
style={{
opacity: disabled ? 0.4 : 0.6,
}}
>
{text}
</Text>
{onPress && (
<Icon
as={MaterialIcons}
name="keyboard-arrow-right"
size="xl"
style={{
opacity: disabled ? 0.4 : 0.6,
}}
/>
)}
</Row>
);
};
export const getElementToggleNode = (
{ onToggle, value, defaultValue }: ElementToggleProps,
disabled: boolean
) => {
return (
<Switch
// the callback is called by the Pressable component wrapping the entire row
isChecked={value ?? false}
defaultIsChecked={defaultValue}
disabled={disabled}
/>
);
};
export const getElementDropdownNode = (
{ options, onSelect, value, defaultValue }: ElementDropdownProps,
disabled: boolean
) => {
return (
<Select
selectedValue={value}
onValueChange={onSelect}
defaultValue={defaultValue}
variant="filled"
isDisabled={disabled}
>
{options.map((option) => (
<Select.Item
key={option.label}
label={option.label}
value={option.value}
/>
))}
</Select>
);
};
export const getElementRangeNode = (
{ onChange, value, defaultValue, min, max, step }: ElementRangeProps,
disabled: boolean,
title: string
) => {
return (
<Slider
// this is a hot fix for now but ideally this input should be managed
// by the value prop and not the defaultValue prop but it requires the
// caller to manage the state of the continuous value which is not ideal
defaultValue={value}
// defaultValue={defaultValue}
minValue={min}
maxValue={max}
step={step}
isDisabled={disabled}
onChangeEnd={onChange}
accessibilityLabel={`Slider for ${title}`}
width="200"
>
<Slider.Track>
<Slider.FilledTrack />
</Slider.Track>
<Slider.Thumb />
</Slider>
);
};

View File

@@ -0,0 +1,154 @@
import React from "react";
import {
Box,
Button,
Column,
Divider,
Icon,
Popover,
Row,
Text,
useBreakpointValue,
} from "native-base";
import useColorScheme from "../../hooks/colorScheme";
import { Ionicons } from "@expo/vector-icons";
import { ElementProps } from "./ElementList";
import {
getElementDropdownNode,
getElementTextNode,
getElementToggleNode,
getElementRangeNode,
ElementDropdownProps,
ElementTextProps,
ElementToggleProps,
ElementRangeProps,
} from "./ElementTypes";
type RawElementProps = {
element: ElementProps;
isHovered?: boolean;
};
export const RawElement = ({ element, isHovered }: RawElementProps) => {
const { title, icon, type, helperText, description, disabled, data } =
element;
const colorScheme = useColorScheme();
const isDark = colorScheme === "dark";
const screenSize = useBreakpointValue({ base: "small", md: "big" });
const isSmallScreen = screenSize === "small";
return (
<Row
style={{
width: "100%",
height: 45,
padding: 15,
justifyContent: "space-between",
alignContent: "stretch",
alignItems: "center",
backgroundColor: isHovered
? isDark
? "rgba(255, 255, 255, 0.1)"
: "rgba(0, 0, 0, 0.05)"
: undefined,
}}
>
<Box
style={{
flexGrow: 1,
flexShrink: 1,
opacity: disabled ? 0.6 : 1,
}}
>
{icon}
<Column maxW={"90%"}>
<Text isTruncated maxW={"100%"}>
{title}
</Text>
{description && (
<Text
isTruncated
maxW={"100%"}
style={{
opacity: 0.6,
fontSize: 12,
}}
>
{description}
</Text>
)}
</Column>
</Box>
<Box
style={{
flexGrow: 0,
flexShrink: 0,
}}
>
<Row
style={{
alignItems: "center",
marginRight: 3,
}}
>
{helperText && (
<Popover
trigger={(triggerProps) => (
<Button
{...triggerProps}
color="gray.500"
leftIcon={
<Icon
as={Ionicons}
size={"md"}
name="help-circle-outline"
/>
}
variant="ghost"
/>
)}
>
<Popover.Content
accessibilityLabel={`Additionnal information for ${title}`}
style={{
maxWidth: isSmallScreen ? "90vw" : "20vw",
}}
>
<Popover.Arrow />
<Popover.Body>{helperText}</Popover.Body>
</Popover.Content>
</Popover>
)}
{(() => {
switch (type) {
case "text":
return getElementTextNode(
data as ElementTextProps,
disabled ?? false
);
case "toggle":
return getElementToggleNode(
data as ElementToggleProps,
disabled ?? false
);
case "dropdown":
return getElementDropdownNode(
data as ElementDropdownProps,
disabled ?? false
);
case "range":
return getElementRangeNode(
data as ElementRangeProps,
disabled ?? false,
title
);
case "custom":
return data as React.ReactNode;
default:
return <Text>Unknown type</Text>;
}
})()}
</Row>
</Box>
</Row>
);
};

View File

@@ -0,0 +1,12 @@
import { Box, Button } from "native-base";
type IconButtonProps = {
icon: Parameters<typeof Button>[0]['leftIcon']
} & Omit<Parameters<typeof Button>[0], 'leftIcon' | 'rightIcon'>;
// Wrapper around Button for IconButton as Native's one sucks <3
const IconButton = (props: IconButtonProps) => {
return <Box><Button {...props} leftIcon={props.icon} width='fit-content' rounded='sm'/></Box>
}
export default IconButton;

View File

@@ -1,7 +1,15 @@
import { useTheme } from "native-base";
import { Spinner } from "native-base";
import { Center, Spinner } from "native-base";
const LoadingComponent = () => {
const theme = useTheme();
return <Spinner color={theme.colors.primary[500]}/>
}
const LoadingView = () => {
return <Center style={{ flexGrow: 1 }}>
<LoadingComponent/>
</Center>
}
export default LoadingComponent;
export { LoadingView }

View File

@@ -0,0 +1,136 @@
// Inspired from OSMD example project
// https://github.com/opensheetmusicdisplay/react-opensheetmusicdisplay/blob/master/src/lib/OpenSheetMusicDisplay.jsx
import React, { useEffect, useState } from 'react';
import { CursorType, Fraction, OpenSheetMusicDisplay as OSMD, IOSMDOptions, Note, Pitch } from 'opensheetmusicdisplay';
import useColorScheme from '../hooks/colorScheme';
import { useWindowDimensions } from 'react-native';
import SoundFont from 'soundfont-player';
type PartitionViewProps = {
// The Buffer of the MusicXML file retreived from the API
file: string;
onPartitionReady: () => void;
onEndReached: () => void;
// Timestamp of the play session, in milisecond
timestamp: number;
}
const PartitionView = (props: PartitionViewProps) => {
const [osmd, setOsmd] = useState<OSMD>();
const [soundPlayer, setSoundPlayer] = useState<SoundFont.Player>();
const AudioContext = window.AudioContext || window.webkitAudioContext || false;
const audioContext = new AudioContext();
const [wholeNoteLength, setWholeNoteLength] = useState(0); // Length of Whole note, in ms (?)
const colorScheme = useColorScheme();
const dimensions = useWindowDimensions();
const OSMD_DIV_ID = 'osmd-div';
const options: IOSMDOptions = {
darkMode: colorScheme == 'dark',
drawComposer: false,
drawCredits: false,
drawLyrics: false,
drawPartNames: false,
followCursor: false,
renderSingleHorizontalStaffline: true,
cursorsOptions: [{ type: CursorType.Standard, color: 'green', alpha: 0.5, follow: false }],
autoResize: false,
}
// Turns note.Length or timestamp in ms
const timestampToMs = (timestamp: Fraction) => {
return timestamp.RealValue * wholeNoteLength;
}
const getActualNoteLength = (note: Note) => {
let duration = timestampToMs(note.Length)
if (note.NoteTie) {
const firstNote = note.NoteTie.Notes.at(1)
if (Object.is(note.NoteTie.StartNote, note) && firstNote) {
duration += timestampToMs(firstNote.Length);
} else {
duration = 0;
}
}
return duration;
}
const playNotesUnderCursor = () => {
osmd!.cursor.NotesUnderCursor()
.filter((note) => note.isRest() == false)
.filter((note) => note.Pitch) // Pitch Can be null, avoiding them
.forEach((note) => {
// Put your hands together for https://github.com/jimutt/osmd-audio-player/blob/master/src/internals/noteHelpers.ts
const fixedKey = note.ParentVoiceEntry.ParentVoice.Parent.SubInstruments.at(0)?.fixedKey ?? 0;
const midiNumber = note.halfTone - fixedKey * 12;
let duration = getActualNoteLength(note);
const gain = note.ParentVoiceEntry.ParentVoice.Volume;
soundPlayer!.play(midiNumber, audioContext.currentTime, { duration, gain })
});
}
const getShortedNoteUnderCursor = () => {
return osmd.cursor.NotesUnderCursor().sort((n1, n2) => n1.Length.CompareTo(n2.Length)).at(0);
}
useEffect(() => {
const _osmd = new OSMD(OSMD_DIV_ID, options);
Promise.all([
SoundFont.instrument(audioContext, 'electric_piano_1'),
_osmd.load(props.file)
]).then(([player, __]) => {
setSoundPlayer(player);
_osmd.render();
_osmd.cursor.hide();
// Ty https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL77C12-L77C63
const bpm = _osmd.Sheet.HasBPMInfo ? _osmd.Sheet.getExpressionsStartTempoInBPM() : 60;
setWholeNoteLength(Math.round((60 / bpm) * 4000))
props.onPartitionReady();
// Do not show cursor before actuall start
});
setOsmd(_osmd);
}, []);
// Re-render manually (otherwise done by 'autoResize' option), to fix disappearing cursor
useEffect(() => {
if (osmd && osmd.IsReadyToRender()) {
osmd.render();
if (!osmd.cursor.hidden) {
osmd.cursor.show();
}
}
}, [dimensions])
useEffect(() => {
if (!osmd || !soundPlayer) {
return;
}
if (props.timestamp > 0 && osmd.cursor.hidden && !osmd.cursor.iterator.EndReached) {
osmd.cursor.show();
playNotesUnderCursor();
return;
}
let previousCursorPosition = -1;
let currentCursorPosition = osmd.cursor.cursorElement.offsetLeft;
let shortestNote = getShortedNoteUnderCursor();
while(!osmd.cursor.iterator.EndReached && (shortestNote?.isRest
? timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) +
timestampToMs(shortestNote?.Length ?? new Fraction(-1)) < props.timestamp
: timestampToMs(shortestNote?.getAbsoluteTimestamp() ?? new Fraction(-1)) < props.timestamp)
) {
previousCursorPosition = currentCursorPosition;
osmd.cursor.next();
if (osmd.cursor.iterator.EndReached) {
osmd.cursor.hide(); // Lousy fix for https://github.com/opensheetmusicdisplay/opensheetmusicdisplay/issues/1338
soundPlayer.stop();
props.onEndReached();
} else {
// Shamelessly stolen from https://github.com/jimutt/osmd-audio-player/blob/ec205a6e46ee50002c1fa8f5999389447bba7bbf/src/PlaybackEngine.ts#LL223C7-L224C1
playNotesUnderCursor();
currentCursorPosition = osmd.cursor.cursorElement.offsetLeft;
document.getElementById(OSMD_DIV_ID).scrollBy(currentCursorPosition - previousCursorPosition, 0)
shortestNote = getShortedNoteUnderCursor();
}
}
}, [props.timestamp]);
return (<div id={OSMD_DIV_ID} style={{ width: '100%', overflow: 'hidden' }} />);
}
export default PartitionView;

View File

@@ -1,7 +1,7 @@
import { useTheme, Box, Center } from "native-base";
import React from "react";
import { useQuery } from "react-query";
import LoadingComponent from "../Loading";
import LoadingComponent, { LoadingView } from "../Loading";
import SlideView from "./SlideView";
import API from "../../API";
@@ -13,11 +13,7 @@ const PartitionVisualizer = ({ songId }: PartitionVisualizerProps) => {
if (!partitionRessources.data) {
return (
<Center style={{ flexGrow: 1 }}>
<LoadingComponent />
</Center>
);
return <LoadingView/>;
}
return (

View File

@@ -8,6 +8,7 @@ import {
Button,
Icon,
} from "native-base";
import IconButton from "../IconButton";
import { MotiView, useDynamicAnimation } from "moti";
import { abs, Easing } from "react-native-reanimated";
import React from "react";
@@ -97,8 +98,8 @@ const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
</MotiView>
</Box>
<Button.Group margin={3}>
<Button
leftIcon={<Icon as={FontAwesome5} name="play" size="sm" />}
<IconButton
icon={<Icon as={FontAwesome5} name="play" size="sm" />}
onPress={() => {
animation.animateTo({
translateX: range(-totalWidth, 0, stepSize)
@@ -112,20 +113,20 @@ const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
});
}}
/>
<Button
leftIcon={<Icon as={FontAwesome5} name="pause" size="sm" />}
<IconButton
icon={<Icon as={FontAwesome5} name="pause" size="sm" />}
onPress={() => {
animation.animateTo({});
}}
/>
<Button
leftIcon={
<IconButton
icon={
<Icon as={MaterialCommunityIcons} name="rewind-10" size="sm" />
}
onPress={() => jumpAt(-200, false)}
/>
<Button
leftIcon={
<IconButton
icon={
<Icon
as={MaterialCommunityIcons}
name="fast-forward-10"
@@ -134,8 +135,8 @@ const SlideView = ({ sources, speed, startAt }: ImgSlideViewProps) => {
}
onPress={() => jumpAt(200, false)}
/>
<Button
leftIcon={<Icon as={FontAwesome5} name="stop" size="sm" />}
<IconButton
icon={<Icon as={FontAwesome5} name="stop" size="sm" />}
onPress={() => {
stepCount = 0;
animation.animateTo({

View File

@@ -1,7 +1,7 @@
import React from "react";
import { translate } from "../i18n/i18n";
import { Box, useBreakpointValue, Text, VStack, Progress } from 'native-base';
import { useNavigation } from "@react-navigation/native";
import { Box, useBreakpointValue, Text, VStack, Progress, Stack, AspectRatio } from 'native-base';
import { useNavigation } from "../Navigation";
import { Pressable, Image } from "native-base";
import Card from "../components/Card";
@@ -12,27 +12,23 @@ const ProgressBar = ({ xp }: { xp: number}) => {
const progessValue = 100 * xp / nextLevelThreshold;
const nav = useNavigation();
const flexDirection = useBreakpointValue({ base: 'column', xl: "row"});
return (
<Card w="90%" maxW='500' style={{flexDirection}}
onPress={() => nav.navigate('User')}
>
<Box w="20%" paddingRight={2} paddingLeft={2} paddingY={2}>
<Image borderRadius={100} source={{
uri: "https://wallpaperaccess.com/full/317501.jpg" // TODO : put the actual profile pic
}} alt="Profile picture" size="sm"
/>
</Box>
<Box w='80%' paddingY={4}>
<VStack alignItems={'center'}>
<Card w="100%" onPress={() => nav.navigate('User')} >
<Stack padding={4} space={2} direction="row">
<AspectRatio ratio={1}>
<Image position="relative" borderRadius={100} source={{
uri: "https://wallpaperaccess.com/full/317501.jpg" // TODO : put the actual profile pic
}} alt="Profile picture" zIndex={0}/>
</AspectRatio>
<VStack alignItems={'center'} flexGrow={1} space={2}>
<Text>{`${translate('level')} ${level}`}</Text>
<Box w="100%">
<Progress value={progessValue} mx="4" />
</Box>
<Text>{xp} / {nextLevelThreshold} {translate('levelProgress')}</Text>
</VStack>
</Box>
</Stack>
</Card>
);
}

View File

@@ -12,6 +12,7 @@ import {
} from "native-base";
import React from "react";
import { Ionicons } from "@expo/vector-icons";
import useColorScheme from "../hooks/colorScheme";
export enum SuggestionType {
TEXT,
@@ -57,20 +58,19 @@ const IllustratedSuggestion = ({
imageSrc,
onPress,
}: IllustratedSuggestionProps) => {
const colorScheme = useColorScheme();
return (
<Pressable
onPress={onPress}
margin={2}
padding={2}
bg={"white"}
_hover={{
bg: "primary.200",
}}
_pressed={{
bg: "primary.300",
}}
>
<HStack alignItems="center" space={4}>
>{({ isHovered, isPressed }) => (
<HStack alignItems="center" space={4}
bg={colorScheme == 'dark'
? (isHovered || isPressed) ? 'gray.800' : undefined
: (isHovered || isPressed) ? 'primary.100' : undefined
}
>
<Square size={"sm"}>
<Image
source={{ uri: imageSrc }}
@@ -86,31 +86,30 @@ const IllustratedSuggestion = ({
</Text>
</VStack>
</HStack>
</Pressable>
)}</Pressable>
);
};
const TextSuggestion = ({ text, onPress }: SuggestionProps) => {
const colorScheme = useColorScheme();
return (
<Pressable
onPress={onPress}
margin={2}
padding={2}
bg={"white"}
_hover={{
bg: "primary.200",
}}
_pressed={{
bg: "primary.300",
}}
>
<Row alignItems="center" space={4}>
>{({ isHovered, isPressed }) => (
<Row alignItems="center" space={4}
bg={colorScheme == 'dark'
? (isHovered || isPressed) ? 'gray.800' : undefined
: (isHovered || isPressed) ? 'primary.100' : undefined
}
>
<Square size={"sm"}>
<Icon size={"md"} as={Ionicons} name="search" />
</Square>
<Text fontSize="md">{text}</Text>
</Row>
</Pressable>
)}</Pressable>
);
};

View File

@@ -1,7 +1,7 @@
import React from "react";
import Card, { CardBorderRadius } from './Card';
import { VStack, Text, Image, Pressable } from 'native-base';
import { useNavigation } from "@react-navigation/core";
import { useNavigation } from "../Navigation";
type SongCardProps = {
albumCover: string;
songTitle: string;
@@ -15,22 +15,22 @@ const SongCard = (props: SongCardProps) => {
return (
<Card
shadow={3}
flexDirection='column'
alignContent='space-around'
onPress={() => navigation.navigate('Song', { songId })}
>
<Image
style={{ zIndex: 0, aspectRatio: 1, margin: 5, borderRadius: CardBorderRadius}}
source={{ uri: albumCover }}
alt={[props.songTitle, props.artistName].join('-')}
/>
<VStack padding={3}>
<Text isTruncated bold fontSize='md' noOfLines={2} height={50}>
{songTitle}
</Text>
<Text isTruncated >
{artistName}
</Text>
<VStack m={1.5} space={3}>
<Image
style={{ zIndex: 0, aspectRatio: 1, borderRadius: CardBorderRadius}}
source={{ uri: albumCover }}
alt={[props.songTitle, props.artistName].join('-')}
/>
<VStack>
<Text isTruncated bold fontSize='md' noOfLines={2} height={50}>
{songTitle}
</Text>
<Text isTruncated >
{artistName}
</Text>
</VStack>
</VStack>
</Card>
)

View File

@@ -5,18 +5,17 @@ import { Heading, VStack } from 'native-base';
type SongCardGrid = {
songs: Parameters<typeof SongCard>[0][];
maxItemsPerRow?: number,
itemDimension?: number,
heading?: JSX.Element,
maxItemsPerRow?: number,
style?: Parameters<typeof FlatGrid>[0]['additionalRowStyle']
}
const SongCardGrid = (props: SongCardGrid) => {
return <VStack>
return <VStack space={5}>
<Heading>{props.heading}</Heading>
<FlatGrid
maxItemsPerRow={props.maxItemsPerRow}
itemDimension={props.itemDimension ?? (props.maxItemsPerRow ? undefined : 150)}
additionalRowStyle={{ justifyContent: 'flex-start' }}
additionalRowStyle={props.style ?? { justifyContent: 'flex-start' }}
data={props.songs}
renderItem={({ item }) => <SongCard {...item} /> }
spacing={10}

View File

@@ -0,0 +1,163 @@
import {
Note,
PianoKey,
NoteNameBehavior,
octaveKeys,
isAccidental,
HighlightedKey,
} from "../../models/Piano";
import { Box, Row, Text } from "native-base";
import PianoKeyComp from "./PianoKeyComp";
type OctaveProps = Parameters<typeof Box>[0] & {
number: number;
startNote: Note;
endNote: Note;
showNoteNames: NoteNameBehavior;
showOctaveNumber: boolean;
whiteKeyBg: string;
whiteKeyBgPressed: string;
whiteKeyBgHovered: string;
blackKeyBg: string;
blackKeyBgPressed: string;
blackKeyBgHovered: string;
highlightedNotes: Array<HighlightedKey>;
defaultHighlightColor: string;
onNoteDown: (note: PianoKey) => void;
onNoteUp: (note: PianoKey) => void;
};
const Octave = (props: OctaveProps) => {
const {
number,
startNote,
endNote,
showNoteNames,
showOctaveNumber,
whiteKeyBg,
whiteKeyBgPressed,
whiteKeyBgHovered,
blackKeyBg,
blackKeyBgPressed,
blackKeyBgHovered,
highlightedNotes,
defaultHighlightColor,
onNoteDown,
onNoteUp,
} = props;
const oK: PianoKey[] = octaveKeys.map((k) => {
return new PianoKey(k.note, number);
});
const notesArray = oK.map((k) => k.note);
const startNoteIndex = notesArray.indexOf(startNote);
const endNoteIndex = notesArray.indexOf(endNote);
const keys = oK.slice(startNoteIndex, endNoteIndex + 1);
const whiteKeys = keys.filter((k) => !isAccidental(k));
const blackKeys = keys.filter(isAccidental);
const whiteKeyWidthExpr = "calc(100% / 7)";
const whiteKeyHeightExpr = "100%";
const blackKeyWidthExpr = "calc(100% / 13)";
const blackKeyHeightExpr = "calc(100% / 1.5)";
return (
<Box {...props}>
<Row height={"100%"} width={"100%"}>
{whiteKeys.map((key, i) => {
const highlightedKey = highlightedNotes.find(
(h) => h.key.note === key.note
);
const isHighlighted = highlightedKey !== undefined;
const highlightColor =
highlightedKey?.bgColor ?? defaultHighlightColor;
return (
<PianoKeyComp
pianoKey={key}
showNoteName={showNoteNames}
bg={isHighlighted ? highlightColor : whiteKeyBg}
bgPressed={isHighlighted ? highlightColor : whiteKeyBgPressed}
bgHovered={isHighlighted ? highlightColor : whiteKeyBgHovered}
onKeyDown={() => onNoteDown(key)}
onKeyUp={() => onNoteUp(key)}
style={{
width: whiteKeyWidthExpr,
height: whiteKeyHeightExpr,
}}
/>
);
})}
{blackKeys.map((key, i) => {
const highlightedKey = highlightedNotes.find(
(h) => h.key.note === key.note
);
const isHighlighted = highlightedKey !== undefined;
const highlightColor =
highlightedKey?.bgColor ?? defaultHighlightColor;
return (
<PianoKeyComp
pianoKey={key}
showNoteName={showNoteNames}
bg={isHighlighted ? highlightColor : blackKeyBg}
bgPressed={isHighlighted ? highlightColor : blackKeyBgPressed}
bgHovered={isHighlighted ? highlightColor : blackKeyBgHovered}
onKeyDown={() => onNoteDown(key)}
onKeyUp={() => onNoteUp(key)}
style={{
width: blackKeyWidthExpr,
height: blackKeyHeightExpr,
position: "absolute",
left: `calc(calc(${whiteKeyWidthExpr} * ${
i + ((i > 1) as unknown as number) + 1
}) - calc(${blackKeyWidthExpr} / 2))`,
top: "0px",
}}
text={{
color: "white",
fontSize: "xs",
}}
/>
);
})}
</Row>
{showOctaveNumber && (
<Text
style={{
userSelect: "none",
WebkitUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
}}
fontSize="2xs"
color="black"
position="absolute"
bottom="0px"
left="2px"
m="2px"
>
{number}
</Text>
)}
</Box>
);
};
Octave.defaultProps = {
startNote: "C",
endNote: "B",
showNoteNames: "onpress",
showOctaveNumber: false,
whiteKeyBg: "white",
whiteKeyBgPressed: "gray.200",
whiteKeyBgHovered: "gray.100",
blackKeyBg: "black",
blackKeyBgPressed: "gray.600",
blackKeyBgHovered: "gray.700",
highlightedNotes: [],
defaultHighlightColor: "#FF0000",
onNoteDown: () => {},
onNoteUp: () => {},
};
export default Octave;

View File

@@ -0,0 +1,102 @@
import { Box, Pressable, Text } from "native-base";
import { StyleProp, ViewStyle } from "react-native";
import {
PianoKey,
NoteNameBehavior,
octaveKeys,
keyToStr,
} from "../../models/Piano";
type PianoKeyProps = {
pianoKey: PianoKey;
showNoteName: NoteNameBehavior;
bg: string;
bgPressed: string;
bgHovered: string;
onKeyDown: () => void;
onKeyUp: () => void;
text: Parameters<typeof Text>[0];
style: StyleProp<ViewStyle>;
};
const isNoteVisible = (
noteNameBehavior: NoteNameBehavior,
isPressed: boolean,
isHovered: boolean
) => {
if (noteNameBehavior === NoteNameBehavior.always) return true;
if (noteNameBehavior === NoteNameBehavior.never) return false;
if (noteNameBehavior === NoteNameBehavior.onpress) {
return isPressed;
} else if (noteNameBehavior === NoteNameBehavior.onhover) {
return isHovered;
}
return false;
};
const PianoKeyComp = ({
pianoKey,
showNoteName,
bg,
bgPressed,
bgHovered,
onKeyDown,
onKeyUp,
text,
style,
}: PianoKeyProps) => {
const textDefaultProps = {
style: {
userSelect: "none",
WebkitUserSelect: "none",
MozUserSelect: "none",
msUserSelect: "none",
},
fontSize: "xl",
color: "black",
} as Parameters<typeof Text>[0];
const textProps = { ...textDefaultProps, ...text };
return (
<Pressable
onPressIn={onKeyDown}
onPressOut={onKeyUp}
style={style}
>
{({ isHovered, isPressed }) => (
<Box
bg={(() => {
if (isPressed) return bgPressed;
if (isHovered) return bgHovered;
return bg;
})()}
w="100%"
h="100%"
borderWidth="1px"
borderColor="black"
justifyContent="flex-end"
alignItems="center"
>
{isNoteVisible(showNoteName, isPressed, isHovered) && (
<Text {...textProps}>{keyToStr(pianoKey, false)}</Text>
)}
</Box>
)}
</Pressable>
);
};
PianoKeyComp.defaultProps = {
key: octaveKeys[0],
showNoteNames: NoteNameBehavior.onhover,
keyBg: "white",
keyBgPressed: "gray.200",
keyBgHovered: "gray.100",
onKeyDown: () => {},
onKeyUp: () => {},
text: {},
style: {},
};
export default PianoKeyComp;

View File

@@ -0,0 +1,96 @@
import { Row, Box } from "native-base";
import React, { useState, useEffect } from "react";
import Octave from "./Octave";
import { StyleProp, ViewStyle } from "react-native";
import {
Note,
PianoKey,
NoteNameBehavior,
KeyPressStyle,
keyToStr,
strToKey,
HighlightedKey,
} from "../../models/Piano";
type VirtualPianoProps = Parameters<typeof Row>[0] & {
onNoteDown: (note: PianoKey) => void;
onNoteUp: (note: PianoKey) => void;
startOctave: number;
startNote: Note;
endOctave: number;
endNote: Note;
showNoteNames: NoteNameBehavior; // default "onpress"
highlightedNotes: Array<HighlightedKey>;
showOctaveNumbers: boolean;
style: StyleProp<ViewStyle>;
};
const VirtualPiano = ({
onNoteDown,
onNoteUp,
startOctave,
startNote,
endOctave,
endNote,
showNoteNames,
highlightedNotes,
showOctaveNumbers,
style,
}: VirtualPianoProps) => {
const notesList: Array<Note> = [
Note.C,
Note.D,
Note.E,
Note.F,
Note.G,
Note.A,
Note.B,
];
const octaveList = [];
for (let octaveNum = startOctave; octaveNum <= endOctave; octaveNum++) {
octaveList.push(octaveNum);
}
const octaveWidthExpr = `calc(100% / ${octaveList.length})`;
return (
<Row style={style}>
{octaveList.map((octaveNum) => {
return (
<Octave
style={{ width: octaveWidthExpr, height: "100%" }}
key={octaveNum}
number={octaveNum}
showNoteNames={showNoteNames}
showOctaveNumber={showOctaveNumbers}
highlightedNotes={highlightedNotes.filter((n) =>
n.key.octave ? n.key.octave == octaveNum : true
)}
startNote={octaveNum == startOctave ? startNote : notesList[0]}
endNote={
octaveNum == endOctave ? endNote : notesList[notesList.length - 1]
}
onNoteDown={onNoteDown}
onNoteUp={onNoteUp}
/>
);
})}
</Row>
);
};
VirtualPiano.defaultProps = {
onNoteDown: (_n: PianoKey) => {},
onNoteUp: (_n: PianoKey) => {},
startOctave: 2,
startNote: Note.C,
endOctave: 6,
endNote: Note.C,
showNoteNames: NoteNameBehavior.onpress,
highlightedNotes: [],
showOctaveNumbers: true,
style: {},
};
export default VirtualPiano;

View File

@@ -0,0 +1,118 @@
import React from "react";
import { translate } from "../../i18n/i18n";
import { string } from "yup";
import {
FormControl,
Input,
Stack,
WarningOutlineIcon,
Box,
Button,
useToast,
} from "native-base";
interface ChangeEmailFormProps {
onSubmit: (
oldEmail: string,
newEmail: string
) => Promise<string>;
}
const validationSchemas = {
email: string().email("Invalid email").required("Email is required"),
};
const ChangeEmailForm = ({ onSubmit }: ChangeEmailFormProps) => {
const [formData, setFormData] = React.useState({
oldEmail: {
value: "",
error: null as string | null,
},
newEmail: {
value: "",
error: null as string | null,
}
});
const [submittingForm, setSubmittingForm] = React.useState(false);
const toast = useToast();
return (
<Box>
<Stack mx="4" style={{ width: '80%', maxWidth: 400 }}>
<FormControl
isRequired
isInvalid={
formData.oldEmail.error !== null ||
formData.newEmail.error !== null
}
>
<FormControl.Label>{translate("oldEmail")}</FormControl.Label>
<Input
isRequired
type="text"
placeholder={translate("oldEmail")}
value={formData.oldEmail.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.email
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, oldEmail: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />} >
{formData.oldEmail.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("newEmail")}</FormControl.Label>
<Input
isRequired
type="text"
placeholder={translate("newEmail")}
value={formData.newEmail.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.email
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, newEmail: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />} >
{formData.oldEmail.error}
</FormControl.ErrorMessage>
<Button
style={{ marginTop: 10 }}
isLoading={submittingForm}
isDisabled={
formData.newEmail.error !== null
}
onPress={async () => {
setSubmittingForm(true);
try {
const resp = await onSubmit(formData.oldEmail.value,
formData.newEmail.value
);
toast.show({ description: resp });
} catch (e) {
toast.show({ description: e as string });
} finally {
setSubmittingForm(false);
}
}}
>
{translate("submitBtn")}
</Button>
</FormControl>
</Stack>
</Box>
);
}
export default ChangeEmailForm;

View File

@@ -0,0 +1,157 @@
import React from "react";
import { translate } from "../../i18n/i18n";
import { string } from "yup";
import {
FormControl,
Input,
Stack,
WarningOutlineIcon,
Box,
Button,
useToast,
} from "native-base";
interface ChangePasswordFormProps {
onSubmit: (
oldPassword: string,
newPassword: string
) => Promise<string>;
}
const ChangePasswordForm = ({ onSubmit }: ChangePasswordFormProps) => {
const [formData, setFormData] = React.useState({
oldPassword: {
value: "",
error: null as string | null,
},
newPassword: {
value: "",
error: null as string | null,
},
confirmNewPassword: {
value: "",
error: null as string | null,
},
});
const [submittingForm, setSubmittingForm] = React.useState(false);
const validationSchemas = {
password: string()
.min(4, translate("passwordTooShort"))
.max(100, translate("passwordTooLong"))
.required("Password is required"),
};
const toast = useToast();
return (
<Box>
<Stack mx="4" style={{ width: '80%', maxWidth: 400 }}>
<FormControl
isRequired
isInvalid={
formData.oldPassword.error !== null ||
formData.newPassword.error !== null ||
formData.confirmNewPassword.error !== null}
>
<FormControl.Label>{translate("oldPassword")}</FormControl.Label>
<Input
isRequired
type="password"
placeholder={translate("oldPassword")}
value={formData.oldPassword.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, oldPassword: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />} >
{formData.oldPassword.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("newPassword")}</FormControl.Label>
<Input
isRequired
type="password"
placeholder={translate("newPassword")}
value={formData.newPassword.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
.finally(() => {
setFormData({ ...formData, newPassword: { value: t, error } });
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />} >
{formData.newPassword.error}
</FormControl.ErrorMessage>
<FormControl.Label>{translate("confirmNewPassword")}</FormControl.Label>
<Input
isRequired
type="password"
placeholder={translate("confirmNewPassword")}
value={formData.confirmNewPassword.value}
onChangeText={(t) => {
let error: null | string = null;
validationSchemas.password
.validate(t)
.catch((e) => (error = e.message))
if (!error && t !== formData.newPassword.value) {
error = translate("passwordsDontMatch");
}
setFormData({
...formData,
confirmNewPassword: { value: t, error },
});
}}
/>
<FormControl.ErrorMessage leftIcon={<WarningOutlineIcon size="xs" />} >
{formData.confirmNewPassword.error}
</FormControl.ErrorMessage>
<Button
style={{ marginTop: 10 }}
isLoading={submittingForm}
isDisabled={
formData.oldPassword.error !== null ||
formData.newPassword.error !== null ||
formData.confirmNewPassword.error !== null ||
formData.oldPassword.value === "" ||
formData.newPassword.value === "" ||
formData.confirmNewPassword.value === ""
}
onPress={async () => {
setSubmittingForm(true);
try {
const resp = await onSubmit(
formData.oldPassword.value,
formData.newPassword.value
);
toast.show({ description: resp });
} catch (e) {
toast.show({ description: e as string });
} finally {
setSubmittingForm(false);
}
}}
>
{translate("submitBtn")}
</Button>
</FormControl>
</Stack>
</Box>
);
}
export default ChangePasswordForm;

View File

@@ -21,7 +21,7 @@ interface SignupFormProps {
) => Promise<string>;
}
const LoginForm = ({ onSubmit }: SignupFormProps) => {
const SignUpForm = ({ onSubmit }: SignupFormProps) => {
const [formData, setFormData] = React.useState({
username: {
value: "",
@@ -210,4 +210,4 @@ const LoginForm = ({ onSubmit }: SignupFormProps) => {
);
};
export default LoginForm;
export default SignUpForm;

View File

@@ -0,0 +1,225 @@
import * as React from "react";
import { StyleProp, ViewStyle, StyleSheet } from "react-native";
import { Ionicons } from "@expo/vector-icons";
import {
View,
Text,
Pressable,
Box,
Row,
Icon,
Button,
useBreakpointValue,
} from "native-base";
import {
createNavigatorFactory,
DefaultNavigatorOptions,
ParamListBase,
CommonActions,
TabActionHelpers,
TabNavigationState,
TabRouter,
TabRouterOptions,
useNavigationBuilder,
} from "@react-navigation/native";
import IconButton from "../IconButton";
const TabRowNavigatorInitialComponentName = "TabIndex";
export {TabRowNavigatorInitialComponentName};
// Props accepted by the view
type TabNavigationConfig = {
tabBarStyle: StyleProp<ViewStyle>;
contentStyle: StyleProp<ViewStyle>;
};
// Supported screen options
type TabNavigationOptions = {
title?: string;
iconProvider?: any;
iconName?: string;
};
// Map of event name and the type of data (in event.data)
//
// canPreventDefault: true adds the defaultPrevented property to the
// emitted events.
type TabNavigationEventMap = {
tabPress: {
data: { isAlreadyFocused: boolean };
canPreventDefault: true;
};
};
// The props accepted by the component is a combination of 3 things
type Props = DefaultNavigatorOptions<
ParamListBase,
TabNavigationState<ParamListBase>,
TabNavigationOptions,
TabNavigationEventMap
> &
TabRouterOptions &
TabNavigationConfig;
function TabNavigator({
initialRouteName,
children,
screenOptions,
tabBarStyle,
contentStyle,
}: Props) {
const { state, navigation, descriptors, NavigationContent } =
useNavigationBuilder<
TabNavigationState<ParamListBase>,
TabRouterOptions,
TabActionHelpers<ParamListBase>,
TabNavigationOptions,
TabNavigationEventMap
>(TabRouter, {
children,
screenOptions,
initialRouteName,
});
const screenSize = useBreakpointValue({ base: "small", md: "big" });
const [isPanelView, setIsPanelView] = React.useState(false);
const isMobileView = screenSize == "small";
React.useEffect(() => {
if (state.index === 0) {
if (isMobileView) {
setIsPanelView(true);
} else {
navigation.reset(
{
...state,
index: 1,
}
);
}
}
}, [state.index]);
React.useEffect(() => {
navigation.setOptions({
headerShown: !isMobileView || isPanelView,
});
}, [isMobileView, isPanelView]);
return (
<NavigationContent>
<Row height={"100%"}>
{(!isMobileView || isPanelView) && (
<View
style={[
{
display: "flex",
flexDirection: "column",
justifyContent: "flex-start",
borderRightWidth: 1,
borderRightColor: "lightgray",
overflow: "scroll",
width: isMobileView ? "100%" : "clamp(200px, 20%, 300px)",
},
tabBarStyle,
]}
>
{state.routes.map((route, idx) => {
if (idx === 0) {
return null;
}
const isSelected = route.key === state.routes[state.index]?.key;
const { options } = descriptors[route.key];
return (
<Button
variant={"ghost"}
key={route.key}
onPress={() => {
const event = navigation.emit({
type: "tabPress",
target: route.key,
canPreventDefault: true,
data: {
isAlreadyFocused: isSelected,
},
});
if (!event.defaultPrevented) {
navigation.dispatch({
...CommonActions.navigate(route),
target: state.key,
});
}
if (isMobileView) {
setIsPanelView(false);
}
}}
bgColor={isSelected && (!isMobileView || !isPanelView) ? "primary.300" : undefined}
style={{
justifyContent: "flex-start",
padding: "10px",
height: "50px",
width: "100%",
}}
leftIcon={
options.iconProvider && options.iconName ? (
<Icon
as={options.iconProvider}
name={options.iconName}
size="xl"
mr="2"
/>
) : undefined
}
>
<Text fontSize="lg" isTruncated w="100%">
{options.title || route.name}
</Text>
</Button>
);
})}
</View>
)}
{(!isMobileView || !isPanelView) && (
<View
style={[
{ flex: 1, width: isMobileView ? "100%" : "700px" },
contentStyle,
]}
>
{isMobileView && (
<Button
style={{
position: "absolute",
top: "10px",
left: "10px",
zIndex: 100,
}}
onPress={() => setIsPanelView(true)}
leftIcon={
<Icon
as={Ionicons}
name="arrow-back"
size="xl"
color="black"
borderRadius="full"
/>
}
/>
)}
{descriptors[state.routes[state.index]?.key]?.render()}
</View>
)}
</Row>
</NavigationContent>
);
}
export default createNavigatorFactory<
TabNavigationState<ParamListBase>,
TabNavigationOptions,
TabNavigationEventMap,
typeof TabNavigator
>(TabNavigator);

View File

@@ -1,10 +1,9 @@
import { Appearance } from "react-native";
import { useSelector } from "react-redux";
import { SettingsState } from "../state/SettingsSlice";
import { RootState } from "../state/Store";
const useColorScheme = (): 'light' | 'dark' => {
const colorScheme: SettingsState['colorScheme'] = useSelector((state: RootState) => state.settings.settings.colorScheme);
const colorScheme = useSelector((state: RootState) => state.settings.local.colorScheme);
const systemColorScheme = Appearance.getColorScheme();
if (colorScheme == 'system') {

View File

@@ -0,0 +1,13 @@
import { useQuery } from "react-query"
import API from "../API"
const useUserSettings = () => {
const queryKey = ['settings'];
const settings = useQuery(queryKey, () => API.getUserSettings())
const updateSettings = (...params: Parameters<typeof API.updateUserSettings>) => API
.updateUserSettings(...params)
.then(() => settings.refetch());
return { settings, updateSettings }
}
export default useUserSettings;

View File

@@ -5,8 +5,14 @@ export const en = {
signInBtn: 'Sign in',
signUpBtn: 'Sign up',
changeLanguageBtn: 'Change language',
search: 'Search',
login: 'Login',
signUp: 'Sign up',
signIn: 'Sign in',
searchBtn: 'Search',
play: 'Play',
playBtn: 'Play',
practiceBtn: 'Practice',
playAgain: 'Play Again',
songPageBtn: 'Go to song page',
level: 'Level',
@@ -91,7 +97,74 @@ export const en = {
unknownError: 'Unknown error',
errAlrdExst: 'Already exist',
errIncrrct: 'Incorrect Credentials',
userProfileFetchError: 'An error occured while fetching your profile',
tryAgain: 'Try Again',
// Playback messages
missed: 'Missed note',
perfect: 'Perfect',
great: 'Great',
good: 'Good',
wrong: 'Wrong',
short: 'A little too short',
long: 'A little too long',
tooLong: 'Too Long',
tooShort: 'Too Short',
changePassword: 'Change password',
oldPassword: 'Old password',
newPassword: 'New password',
confirmNewPassword: 'Confirm new password',
submitBtn: 'Submit',
changeEmail: 'Change email',
oldEmail: 'Old email',
newEmail: 'New email',
passwordUpdated: 'Password updated',
emailUpdated: 'Email updated',
SettingsCategoryProfile: 'Profile',
SettingsCategoryPreferences: 'Preferences',
SettingsCategoryNotifications: 'Notifications',
SettingsCategoryPrivacy: 'Privacy',
SettingsCategorySecurity: 'Security',
SettingsCategoryEmail: 'Email',
SettingsCategoryGoogle: 'Google',
SettingsCategoryPiano: 'Piano',
SettingsCategoryGuest: 'Guest',
transformGuestToUserExplanations: 'You can transform your guest account to a user account by providing a username and a password. You will then be able to save your progress and access your profile.',
SettingsNotificationsPushNotifications: 'Push',
SettingsNotificationsEmailNotifications: 'Email',
SettingsNotificationsTrainingReminder: 'Training reminder',
SettingsNotificationsReleaseAlert: 'Release alert',
dataCollection: 'Data collection',
customAds: 'Custom ads',
recommendations: 'Recommendations',
SettingsPreferencesTheme: 'Theme',
SettingsPreferencesLanguage: 'Language',
SettingsPreferencesDifficulty: 'Difficulty',
SettingsPreferencesColorblindMode: 'Colorblind mode',
SettingsPreferencesMicVolume: 'Mic volume',
SettingsPreferencesDevice: 'Device',
NoAssociatedEmail: 'No associated email',
nbGamesPlayed: 'Games played',
XPDescription: 'XP is a measure of your progress. You earn XP by playing songs and completing challenges.',
userCreatedAt: 'Creation date',
premiumAccount: "Premium account",
yes: 'Yes',
no: 'No',
Attention: 'Attention',
YouAreCurrentlyConnectedWithAGuestAccountWarning: "You are currently connected with a guest account. Disconneting will result in your data being lost. If you want to save your progress, you need to create an account.",
recentSearches: 'Recent searches',
noRecentSearches: 'No recent searches',
};
export const fr: typeof en = {
@@ -102,6 +175,7 @@ export const fr: typeof en = {
changeLanguageBtn: 'Changer la langue',
searchBtn: 'Rechercher',
playBtn: 'Jouer',
practiceBtn: 'S\'entrainer',
playAgain: 'Rejouer',
songPageBtn: 'Aller sur la page de la chanson',
level: 'Niveau',
@@ -110,15 +184,14 @@ export const fr: typeof en = {
lastScore: 'Dernier Score',
langBtn: 'Langage',
backBtn: 'Retour',
settingsBtn: 'Réglages',
prefBtn: 'Préférences',
notifBtn: 'Notifications',
privBtn: 'Confidentialité',
goNextStep: 'Prochaine Etape',
mySkillsToImprove: 'Mes Skills',
recentlyPlayed: 'Joués récemment',
lastSearched: 'Dernières recherches',
levelProgress: 'Niveau',
play: 'Jouer',
changeEmail: 'Changer d\'email',
newEmail: 'Nouvel email',
oldEmail: 'Ancien email',
// profile page
user: 'Profil',
@@ -127,7 +200,7 @@ export const fr: typeof en = {
mostPlayedSong: 'Chanson la plus jouée : ',
goodNotesPlayed: 'Bonnes notes jouées : ',
longestCombo: 'Combo le plus long : ',
favoriteGenre: 'Genre favorit : ',
favoriteGenre: 'Genre favori : ',
// Difficulty settings
diffBtn: 'Difficulté',
@@ -139,7 +212,21 @@ export const fr: typeof en = {
dark: 'Foncé',
system: 'Système',
light: 'Clair',
settingsBtn: "Réglages",
goNextStep: "Prochaine Etape",
mySkillsToImprove: "Mes Skills",
recentlyPlayed: "Joués récemment",
search: "Rechercher",
lastSearched: "Dernières recherches",
levelProgress: "Niveau",
login: 'Se connecter',
signUp: "S'inscrire",
signIn: "Se connecter",
oldPassword: 'Ancien mot de passe',
newPassword: 'Nouveau mot de passe',
confirmNewPassword: 'Confirmer le nouveau mot de passe',
submitBtn: 'Soumettre',
// competencies
pedalsCompetency: 'Pédales',
rightHandCompetency: 'Main droite',
@@ -170,11 +257,12 @@ export const fr: typeof en = {
email: 'Email',
repeatPassword: 'Confirmer',
score: 'Score',
changePassword: 'Modification du mot de passe',
precisionScore: 'Précision',
goodNotesInARow: 'Bonnes notes à la suite',
songsToGetBetter: 'Recommendations',
goodNotes: 'bonnes notes',
changepasswdBtn: 'Changer le mot de pass',
changepasswdBtn: 'Changer le mot de passe',
changeemailBtn: 'Changer l\'email',
googleacctBtn: 'Compte Google',
forgottenPassword: 'Mot de passe oublié',
@@ -185,10 +273,65 @@ export const fr: typeof en = {
errAlrdExst: "Utilisateur existe déjà",
unknownError: 'Erreur inconnue',
errIncrrct: 'Identifiant incorrect',
userProfileFetchError: 'Une erreur est survenue lors de la récupération du profil',
tryAgain: 'Réessayer',
// Playback messages
missed: 'Raté',
perfect: 'Parfait',
great: 'Super',
good: 'Bien',
wrong: 'Oups',
short: 'Un peu court',
long: 'Un peu long',
tooLong: 'Trop long',
tooShort: 'Trop court',
passwordUpdated: 'Mot de passe mis à jour',
emailUpdated: 'Email mis à jour',
SettingsCategoryProfile: 'Profil',
SettingsCategoryPreferences: 'Préférences',
SettingsCategoryNotifications: 'Notifications',
SettingsCategoryPrivacy: 'Confidentialité',
SettingsCategorySecurity: 'Sécurité',
SettingsCategoryEmail: 'Email',
SettingsCategoryGoogle: 'Google',
SettingsCategoryPiano: 'Piano',
transformGuestToUserExplanations: 'Vous êtes actuellement connecté en tant qu\'invité. Vous pouvez créer un compte pour sauvegarder vos données et profiter de toutes les fonctionnalités de Chromacase.',
SettingsCategoryGuest: 'Invité',
SettingsNotificationsEmailNotifications: 'Email',
SettingsNotificationsPushNotifications: 'Notifications push',
SettingsNotificationsReleaseAlert: 'Alertes de nouvelles Sorties',
SettingsNotificationsTrainingReminder: 'Rappel d\'entrainement',
SettingsPreferencesColorblindMode: 'Mode daltonien',
SettingsPreferencesDevice: 'Appareil',
SettingsPreferencesDifficulty: 'Difficulté',
SettingsPreferencesLanguage: 'Langue',
SettingsPreferencesTheme: 'Thème',
SettingsPreferencesMicVolume: 'Volume du micro',
dataCollection: 'Collecte de données',
recommendations: 'Recommandations',
customAds: 'Publicités personnalisées',
NoAssociatedEmail: 'Aucun email associé',
nbGamesPlayed: 'Parties jouées',
XPDescription: 'L\'XP est gagnée en jouant des chansons. Plus vous jouez, plus vous gagnez d\'XP. Plus vous avez d\'XP, plus vous montez de niveau.',
userCreatedAt: 'Compte créé le',
premiumAccount: 'Compte premium',
yes: 'Oui',
no: 'Non',
Attention: 'Attention',
YouAreCurrentlyConnectedWithAGuestAccountWarning: 'Vous êtes actuellement connecté en tant qu\'invité. La déconnexion résultera en une perte de données. Vous pouvez créer un compte pour sauvegarder vos données.',
recentSearches: 'Recherches récentes',
noRecentSearches: 'Aucune recherche récente',
};
export const sp: typeof en = {
welcome: 'Benvenido a Chromacase',
welcomeMessage: 'Benvenido',
signOutBtn: 'Desconectarse',
signInBtn: 'Connectarse',
@@ -197,10 +340,19 @@ export const sp: typeof en = {
googleacctBtn: 'Cuenta Google',
goodNotes: 'buenas notas',
search: 'Buscar',
login: 'Iniciar sesión',
signUp: 'Registrarse',
signIn: 'Iniciar sesión',
changeEmail: 'Cambiar el correo electrónico',
newEmail: 'Nuevo correo electrónico',
oldEmail: 'Correo electrónico anterior',
// competencies
changeLanguageBtn: 'Cambiar el idioma',
searchBtn: 'Buscar',
playBtn: 'reproducir',
practiceBtn: 'Práctica',
playAgain: 'Repetición',
precisionScore: 'Précision',
songPageBtn: 'canción',
@@ -208,9 +360,6 @@ export const sp: typeof en = {
chapters: 'Capítulos',
bestScore: 'Mejor puntuación',
lastScore: 'Ùltima puntuación',
langBtn: 'idioma',
backBtn: 'Volver',
settingsBtn: 'Ajustes',
prefBtn: 'Preferencias',
notifBtn: 'Notificaciones',
privBtn: 'Privacidad',
@@ -219,6 +368,12 @@ export const sp: typeof en = {
recentlyPlayed: 'Recientemente jugado',
lastSearched: 'Ultimas búsquedas',
welcome: 'Benvenido a Chromacase',
langBtn: 'Langua',
backBtn: 'Volver',
settingsBtn: 'Ajustes',
play: 'Jugar',
// profile page
user: 'Perfil',
medals: 'Medallas',
@@ -279,5 +434,68 @@ export const sp: typeof en = {
//errors
unknownError: 'Error desconocido',
errAlrdExst: "Ya existe",
errIncrrct: "credenciales incorrectas"
errIncrrct: "credenciales incorrectas",
userProfileFetchError: 'Ocurrió un error al obtener su perfil',
tryAgain: 'intentar otra vez',
// Playback messages
missed: 'Te perdiste una nota',
perfect: 'Perfecto',
great: 'Excelente',
good: 'Bueno',
wrong: 'Equivocado',
short: 'Un poco demasiado corto',
long: 'Un poco demasiado largo',
tooLong: 'Demasiado largo',
tooShort: 'Demasiado corto',
changePassword: 'Cambio de contraseña',
oldPassword: 'Contraseña anterior',
newPassword: 'Nueva contraseña',
confirmNewPassword: 'Confirmar nueva contraseña',
submitBtn: 'Enviar',
passwordUpdated: 'Contraseña actualizada',
emailUpdated: 'Email actualizado',
SettingsCategoryProfile: 'Perfil',
SettingsCategoryPreferences: 'Preferencias',
SettingsCategoryNotifications: 'Notificaciones',
SettingsCategoryPrivacy: 'Privacidad',
SettingsCategorySecurity: 'Seguridad',
SettingsCategoryEmail: 'Email',
SettingsCategoryGoogle: 'Google',
SettingsCategoryPiano: 'Piano',
transformGuestToUserExplanations: 'Actualmente estás conectado como invitado. Puedes crear una cuenta para guardar tus datos y disfrutar de todas las funciones de Chromacase.',
SettingsCategoryGuest: 'Invitado',
SettingsNotificationsEmailNotifications: 'Email',
SettingsNotificationsPushNotifications: 'Notificaciones push',
SettingsNotificationsReleaseAlert: 'Alertas de nuevas Sorties',
SettingsNotificationsTrainingReminder: 'Recordatorio de entrenamiento',
SettingsPreferencesColorblindMode: 'Modo daltoniano',
SettingsPreferencesDevice: 'Dispositivo',
SettingsPreferencesDifficulty: 'Dificultad',
SettingsPreferencesLanguage: 'Idioma',
SettingsPreferencesTheme: 'Tema',
SettingsPreferencesMicVolume: 'Volumen del micrófono',
dataCollection: 'Recopilación de datos',
recommendations: 'Recomendaciones',
customAds: 'Anuncios personalizados',
NoAssociatedEmail: 'No hay correo electrónico asociado',
nbGamesPlayed: 'Partidos jugados',
XPDescription: 'XP se gana jugando canciones. Cuanto más juegas, más XP ganas. Cuanto más XP tienes, más subes de nivel.',
userCreatedAt: 'Cuenta creada el',
premiumAccount: 'Cuenta premium',
yes: 'Sí',
no: 'No',
Attention: 'Atención',
YouAreCurrentlyConnectedWithAGuestAccountWarning: 'Actualmente estás conectado como invitado. La desconexión resultará en la pérdida de datos. Puedes crear una cuenta para guardar tus datos.',
recentSearches: 'Búsquedas recientes',
noRecentSearches: 'No hay búsquedas recientes',
};

View File

@@ -0,0 +1,10 @@
interface LocalSettings {
deviceId: number,
micVolume: number,
colorScheme: 'light' | 'dark' | 'system',
lang: 'fr' | 'en' | 'sp',
difficulty: 'beg' | 'inter' | 'pro',
colorBlind: boolean,
customAds: boolean,
dataCollection: boolean
}

View File

@@ -1,5 +0,0 @@
interface Metrics {
}
export default Metrics;

170
front/models/Piano.ts Normal file
View File

@@ -0,0 +1,170 @@
export enum Note {
"C",
"C#",
"D",
"D#",
"E",
"F",
"F#",
"G",
"G#",
"A",
"A#",
"B",
}
export enum NoteNameBehavior {
"always",
"onpress",
"onhighlight",
"onhover",
"never",
}
export enum KeyPressStyle {
"subtle",
"vivid",
}
export type HighlightedKey = {
key: PianoKey;
// if not specified, the default color for highlighted notes will be used
bgColor?: string;
};
export class PianoKey {
public note: Note;
public octave?: number;
constructor(note: Note, octave?: number) {
this.note = note;
this.octave = octave;
}
public toString = () => {
return (this.note as unknown as string) + (this.octave || "");
};
}
export const strToKey = (str: string): PianoKey => {
let note: Note;
const isSimpleNote = str[1]! >= "0" && str[1]! <= "9";
// later we need to support different annotations
switch (isSimpleNote ? str[0] : str.substring(0, 2)) {
case "E":
note = Note.E;
break;
case "B":
note = Note.B;
break;
case "C":
note = Note.C;
break;
case "D":
note = Note.D;
break;
case "F":
note = Note.F;
break;
case "G":
note = Note.G;
break;
case "A":
note = Note.A;
break;
case "C#":
note = Note["C#"];
break;
case "D#":
note = Note["D#"];
break;
case "F#":
note = Note["F#"];
break;
case "G#":
note = Note["G#"];
break;
case "A#":
note = Note["A#"];
break;
default:
throw new Error("Invalid note name");
}
if ((isSimpleNote && !str[1]) || (!isSimpleNote && str.length < 3)) {
return new PianoKey(note);
}
const octave = parseInt(str.substring(isSimpleNote ? 1 : 2));
return new PianoKey(note, octave);
};
export const keyToStr = (key: PianoKey, showOctave: boolean = true): string => {
let s = "";
switch (key.note) {
case Note.C:
s += "C";
break;
case Note.D:
s += "D";
break;
case Note.E:
s += "E";
break;
case Note.F:
s += "F";
break;
case Note.G:
s += "G";
break;
case Note.A:
s += "A";
break;
case Note.B:
s += "B";
break;
case Note["C#"]:
s += "C#";
break;
case Note["D#"]:
s += "D#";
break;
case Note["F#"]:
s += "F#";
break;
case Note["G#"]:
s += "G#";
break;
case Note["A#"]:
s += "A#";
break;
default:
throw new Error("Invalid note name");
}
if (showOctave && key.octave) {
s += key.octave;
}
return s;
};
export const isAccidental = (key: PianoKey): boolean => {
return (
key.note === Note["C#"] ||
key.note === Note["D#"] ||
key.note === Note["F#"] ||
key.note === Note["G#"] ||
key.note === Note["A#"]
);
};
export const octaveKeys: Array<PianoKey> = [
new PianoKey(Note.C),
new PianoKey(Note["C#"]),
new PianoKey(Note.D),
new PianoKey(Note["D#"]),
new PianoKey(Note.E),
new PianoKey(Note.F),
new PianoKey(Note["F#"]),
new PianoKey(Note.G),
new PianoKey(Note["G#"]),
new PianoKey(Note.A),
new PianoKey(Note["A#"]),
new PianoKey(Note.B),
];

View File

@@ -0,0 +1,7 @@
interface SearchHistory {
query: string;
userID: number;
id: number;
}
export default SearchHistory;

View File

@@ -1,4 +1,3 @@
import Metrics from "./Metrics";
import Model from "./Model";
import SongDetails from "./SongDetails";
@@ -8,7 +7,6 @@ interface Song extends Model {
albumId: number | null
genreId: number | null;
cover: string;
metrics: Metrics;
details: SongDetails;
}

View File

@@ -1,7 +1,8 @@
interface LessonHistory {
songId: number;
userId: number;
interface SongHistory {
songID: number;
userID: number;
score: number;
difficulties: JSON;
}
export default LessonHistory;
export default SongHistory;

View File

@@ -1,13 +1,13 @@
import Metrics from "./Metrics";
import UserData from "./UserData";
import Model from "./Model";
import UserSettings from "./UserSettings";
interface User extends Model {
name: string;
email: string;
xp: number;
isGuest: boolean;
premium: boolean;
metrics: Metrics;
data: UserData;
settings: UserSettings;
}

8
front/models/UserData.ts Normal file
View File

@@ -0,0 +1,8 @@
interface UserData {
gamesPlayed: number;
xp: number;
avatar: string | undefined;
createdAt: Date;
}
export default UserData;

View File

@@ -1,23 +1,14 @@
interface UserSettings {
preferences: {
deviceId: number,
micVolume: number,
theme: 'light' | 'dark' | 'system',
lang: 'fr' | 'en' | 'sp',
difficulty: 'beg' | 'inter' | 'pro',
colorBlind: boolean
},
notifications: {
pushNotif: boolean,
emailNotif: boolean,
trainNotif: boolean,
newSongNotif: boolean
},
privacy: {
dataCollection: boolean,
customAd: boolean,
recommendation: boolean
}
notifications: {
pushNotif: boolean,
emailNotif: boolean,
trainNotif: boolean,
newSongNotif: boolean
},
weeklyReport: boolean,
leaderBoard: boolean,
showActivity: boolean,
recommendations: boolean
}
export default UserSettings

View File

@@ -1,22 +0,0 @@
server {
listen 80;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://back:3000/;
}
location /ws/ {
proxy_pass http://scorometer:6543/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
}
}

25
front/nginx.conf.template Normal file
View File

@@ -0,0 +1,25 @@
server {
listen ${NGINX_PORT};
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass ${API_URL};
}
location /ws {
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Scheme $scheme;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $remote_addr;
proxy_pass ${SCOROMETER_URL};
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $http_connection;
proxy_http_version 1.1;
}
}

View File

@@ -42,6 +42,7 @@
"midi-player-js": "^2.0.16",
"moti": "^0.22.0",
"native-base": "^3.4.17",
"opensheetmusicdisplay": "^1.7.5",
"react": "18.1.0",
"react-dom": "18.1.0",
"react-i18next": "^11.18.3",
@@ -56,6 +57,7 @@
"react-native-web": "~0.18.7",
"react-redux": "^8.0.2",
"react-timer-hook": "^3.0.5",
"react-use-precision-timer": "^3.3.1",
"redux-persist": "^6.0.0",
"soundfont-player": "^0.12.0",
"type-fest": "^3.6.0",

View File

@@ -1,34 +1,22 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit";
export type SettingsState = {
colorScheme: "dark" | "light" | "system",
enablePushNotifications: boolean,
enableMailNotifications: boolean,
enableLessongsReminders: boolean,
enableReleaseAlerts: boolean,
preferedLevel: 'easy' | 'medium' | 'hard',
colorBlind: boolean,
micLevel: number,
preferedInputName?: string
}
export const settingsSlice = createSlice({
name: 'settings',
initialState: {
settings: <SettingsState>{
enablePushNotifications: true,
enableMailNotifications: true,
enableLessongsReminders: true,
enableReleaseAlerts: true,
preferedLevel: 'easy',
local: <LocalSettings>{
deviceId: 0,
micVolume: 0,
colorScheme: 'system',
lang: 'en',
difficulty: 'beg',
colorBlind: false,
micLevel: 50,
colorScheme: "system"
customAds: true,
dataCollection: true
},
},
reducers: {
updateSettings: (state, action: PayloadAction<Partial<SettingsState>>) => {
state.settings = { ...state.settings, ...action.payload };
updateSettings: (state, action: PayloadAction<Partial<LocalSettings>>) => {
state.local = { ...state.local, ...action.payload };
}
}
});

View File

@@ -1,24 +1,29 @@
import userReducer from '../state/UserSlice';
import settingsReduder from './SettingsSlice';
import { configureStore } from '@reduxjs/toolkit';
import { StateFromReducersMapObject, configureStore } from '@reduxjs/toolkit';
import languageReducer from './LanguageSlice';
import { TypedUseSelectorHook, useDispatch as reduxDispatch, useSelector as reduxSelector } from 'react-redux'
import { persistStore, persistCombineReducers, FLUSH, PAUSE, PERSIST, PURGE, REGISTER, REHYDRATE } from "redux-persist";
import AsyncStorage from '@react-native-async-storage/async-storage';
import { CurriedGetDefaultMiddleware } from '@reduxjs/toolkit/dist/getDefaultMiddleware';
import { PersistPartial } from 'redux-persist/es/persistReducer';
const persistConfig = {
key: 'root',
storage: AsyncStorage
}
const reducers = {
user: userReducer,
language: languageReducer,
settings: settingsReduder
}
type State = StateFromReducersMapObject<typeof reducers>;
let store = configureStore({
reducer: persistCombineReducers(persistConfig, {
user: userReducer,
language: languageReducer,
settings: settingsReduder
}),
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
reducer: persistCombineReducers(persistConfig, reducers),
middleware: (getDefaultMiddleware: CurriedGetDefaultMiddleware<State & PersistPartial>) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},

View File

@@ -7,6 +7,7 @@ import { Center, Button, Text } from 'native-base';
import SigninForm from "../components/forms/signinform";
import SignupForm from "../components/forms/signupform";
import TextButton from "../components/TextButton";
import { RouteProps } from "../Navigation";
const hanldeSignin = async (username: string, password: string, apiSetter: (accessToken: string) => void): Promise<string> => {
try {
@@ -32,9 +33,13 @@ const handleSignup = async (username: string, password: string, email: string, a
}
};
const AuthenticationView = () => {
type AuthenticationViewProps = {
isSignup: boolean;
}
const AuthenticationView = ({ isSignup }: RouteProps<AuthenticationViewProps>) => {
const dispatch = useDispatch();
const [mode, setMode] = React.useState("signin" as "signin" | "signup");
const [mode, setMode] = React.useState<"signin" | "signup">(isSignup ? "signup" : "signin");
return (
<Center style={{ flex: 1 }}>

View File

@@ -1,116 +1,165 @@
import React from "react";
import { useQueries, useQuery } from "react-query";
import API from "../API";
import LoadingComponent from "../components/Loading";
import { Box, ScrollView, Flex, useBreakpointValue, Text, VStack, Button, Heading } from 'native-base';
import { useNavigation } from "@react-navigation/native";
import SongCardGrid from '../components/SongCardGrid';
import CompetenciesTable from '../components/CompetenciesTable'
import { LoadingView } from "../components/Loading";
import {
Center,
Box,
ScrollView,
Flex,
useBreakpointValue,
Stack,
Heading,
Container,
VStack,
HStack,
Column,
Button,
Text,
useTheme
} from "native-base";
import { useNavigation } from "../Navigation";
import SongCardGrid from "../components/SongCardGrid";
import CompetenciesTable from "../components/CompetenciesTable";
import ProgressBar from "../components/ProgressBar";
import Translate from "../components/Translate";
import TextButton from "../components/TextButton";
import Song from "../models/Song";
import { FontAwesome5 } from "@expo/vector-icons";
const HomeView = () => {
const theme = useTheme();
const navigation = useNavigation();
const screenSize = useBreakpointValue({ base: 'small', md: "big"});
const flexDirection = useBreakpointValue({ base: 'column', xl: "row"});
const userQuery = useQuery(['user'], () => API.getUserInfo());
const playHistoryQuery = useQuery(['history', 'play'], () => API.getUserPlayHistory());
const searchHistoryQuery = useQuery(['history', 'search'], () => API.getSearchHistory());
const skillsQuery = useQuery(['skills'], () => API.getUserSkills());
const nextStepQuery = useQuery(['user', 'recommendations'], () => API.getUserRecommendations());
const artistsQueries = useQueries((playHistoryQuery.data?.concat(searchHistoryQuery.data ?? []).concat(nextStepQuery.data ?? []) ?? []).map((song) => (
{ queryKey: ['artist', song.id], queryFn: () => API.getArtist(song.id) }
)));
const songHistory = useQueries(
playHistoryQuery.data?.map(({ songID }) => ({
queryKey: ['song', songID],
queryFn: () => API.getSong(songID)
})) ?? []
);
const artistsQueries = useQueries((songHistory
.map((entry) => entry.data)
.concat(nextStepQuery.data ?? [])
.filter((s): s is Song => s !== undefined))
.map((song) => (
{ queryKey: ['artist', song.id], queryFn: () => API.getArtist(song.id) }
))
);
if (!userQuery.data || !skillsQuery.data || !searchHistoryQuery.data || !playHistoryQuery.data) {
return <Box style={{ flexGrow: 1, justifyContent: 'center' }}>
<LoadingComponent/>
</Box>
return <LoadingView/>
}
return <ScrollView>
<Box style={{ display: 'flex', padding: 30 }}>
<Box textAlign={ screenSize == 'small' ? 'center' : undefined } style={{ flexDirection, justifyContent: 'center', display: 'flex' }}>
<Text fontSize="xl" flex={screenSize == 'small' ? 1 : 2}>
<Translate translationKey="welcome" format={(welcome) => `${welcome} ${userQuery.data.name}!`}/>
</Text>
return <ScrollView p={10}>
<Flex>
<Stack space={4}
display={{ base: 'block', md: 'flex' }}
direction={{ base: 'column', md: 'row' }}
textAlign={{ base: 'center', md: 'inherit' }}
justifyContent="space-evenly"
>
<Translate fontSize="xl" flex={2}
translationKey="welcome" format={(welcome) => `${welcome} ${userQuery.data.name}!`}
/>
<Box flex={1}>
<ProgressBar xp={userQuery.data.xp}/>
<ProgressBar xp={userQuery.data.data.xp}/>
</Box>
</Box>
<Box paddingY={5} style={{ flexDirection }}>
<Box flex={2}>
<SongCardGrid
heading={<Translate translationKey='goNextStep'/>}
itemDimension={screenSize == 'small' ? 250 : 200}
songs={nextStepQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
albumCover: song.cover,
songTitle: song.name,
songId: song.id,
artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name
})) ?? []
}
/>
<Flex style={{ flexDirection }}>
<Box flex={1} paddingY={5}>
<Heading><Translate translationKey='mySkillsToImprove'/></Heading>
<Box padding={5}>
<CompetenciesTable {...skillsQuery.data}/>
</Box>
</Stack>
</Flex>
<Stack direction={{ base: 'column', lg: 'row' }} height="100%" space={5} paddingTop={5}>
<VStack flex={{ lg: 2 }} space={5}>
<SongCardGrid
heading={<Translate translationKey='goNextStep'/>}
songs={nextStepQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
albumCover: song.cover,
songTitle: song.name,
songId: song.id,
artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name
})) ?? []
}
/>
<Stack direction={{ base: 'column', lg: 'row' }}>
<Box flex={{ lg: 1 }}>
<Heading><Translate translationKey='mySkillsToImprove'/></Heading>
<Box padding={5}>
<CompetenciesTable {...skillsQuery.data}/>
</Box>
<Box flex={1} padding={5}>
<SongCardGrid
heading={<Translate translationKey='recentlyPlayed'/>}
itemDimension={screenSize == 'small' ? 200 : 170}
songs={playHistoryQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
albumCover: song.cover,
songTitle: song.name,
songId: song.id,
artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name
})) ?? []
}
/>
</Box>
</Box>
<Box flex={{ lg: 1 }}>
<SongCardGrid
heading={<Translate translationKey='recentlyPlayed'/>}
songs={songHistory
.filter((songQuery) => songQuery.data)
.map(({ data }) => data)
.filter((song, i, array) => array.map((s) => s.id).findIndex((id) => id == song.id) == i)
.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
albumCover: song.cover,
songTitle: song.name,
songId: song.id,
artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name
})) ?? []
}
/>
</Box>
</Stack>
</VStack>
<VStack flex={{ lg: 1 }} height={{ lg: '100%' }} alignItems="center">
<HStack width="100%" justifyContent="space-evenly" p={5} space={5}>
<TextButton
translate={{ translationKey: 'searchBtn' }}
colorScheme='secondary' size="sm"
onPress={() => navigation.navigate('Search')}
/>
<TextButton translate={{ translationKey: 'settingsBtn' }}
colorScheme='gray' size="sm"
onPress={() => navigation.navigate('Settings')}
/>
</HStack>
<Box style={{ width: '100%' }}>
<Heading><Translate translationKey='recentSearches'/></Heading>
<Flex padding={3} style={{
width: '100%',
alignItems: 'flex-start',
alignContent: 'flex-start',
flexDirection: 'row',
flexWrap: 'wrap',
}}>
{
searchHistoryQuery.data?.length === 0 && <Translate translationKey='noRecentSearches'/>
}
{
[...(new Set(searchHistoryQuery.data.map((x) => x.query)))].reverse().slice(0, 5).map((query) => (
<Button
leftIcon={
<FontAwesome5 name="search" size={16} />
}
style={{
margin: 2,
}}
key={ query }
variant="solid"
size="xs"
colorScheme="primary"
onPress={() => navigation.navigate('Search', { query: query })}
>
<Text fontSize={"xs"} isTruncated maxW={"150px"}>
{ query }
</Text>
</Button>
))
}
</Flex>
</Box>
</VStack>
</Stack>
<VStack padding={5} flex={1} space={10}>
<Box style={{flexDirection: 'row'}}>
<Box flex="2" padding={5}>
<Box style={{ flexDirection: 'row', justifyContent:'center' }}>
<TextButton
translate={{ translationKey: 'search' }}
colorScheme='secondary' size="sm"
onPress={() => navigation.navigate('Search')}
/>
</Box>
<SongCardGrid
itemDimension={screenSize == 'small' ? 150 : 120}
heading={<Translate translationKey='lastSearched'/>}
songs={searchHistoryQuery.data?.filter((song) => artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId))
.map((song) => ({
albumCover: song.cover,
songTitle: song.name,
songId: song.id,
artistName: artistsQueries.find((artistQuery) => artistQuery.data?.id === song.artistId)!.data!.name
})) ?? []
}
/>
</Box>
</Box>
<Box style={{ flexDirection: 'row', justifyContent:'center' }}>
<TextButton translate={{ translationKey: 'settingsBtn' }}
size="sm" onPress={() => navigation.navigate('Settings')}
/>
</Box>
</VStack>
</Box>
</Box>
</ScrollView>
}

View File

@@ -1,20 +1,26 @@
import React, { useEffect, useRef, useState } from 'react';
import { SafeAreaView, Text, Platform } from 'react-native';
import { SafeAreaView, Platform } from 'react-native';
import * as ScreenOrientation from 'expo-screen-orientation';
import { Box, Center, Column, IconButton, Progress, Row, View, useToast } from 'native-base';
import { Ionicons } from "@expo/vector-icons";
import { useNavigation } from '@react-navigation/native';
import { useQuery, useQueryClient } from 'react-query';
import { Box, Center, Column, Progress, Text, Row, View, useToast, Icon } from 'native-base';
import IconButton from '../components/IconButton';
import { Ionicons, MaterialCommunityIcons } from "@expo/vector-icons";
import { RouteProps, useNavigation } from "../Navigation";
import { useQuery } from 'react-query';
import API from '../API';
import LoadingComponent from '../components/Loading';
import LoadingComponent, { LoadingView } from '../components/Loading';
import Constants from 'expo-constants';
import { useStopwatch } from 'react-timer-hook';
import SlideView from '../components/PartitionVisualizer/SlideView';
import MidiPlayer from 'midi-player-js';
import SoundFont from 'soundfont-player';
import VirtualPiano from '../components/VirtualPiano/VirtualPiano';
import { strToKey, keyToStr, Note } from '../models/Piano';
import { useSelector } from 'react-redux';
import { RootState } from '../state/Store';
import { translate } from '../i18n/i18n';
import { ColorSchemeType } from 'native-base/lib/typescript/components/types';
import { useStopwatch } from "react-use-precision-timer";
import PartitionView from '../components/PartitionView';
type PlayViewProps = {
songId: number
songId: number,
type: 'practice' | 'normal'
}
@@ -29,44 +35,52 @@ if (process.env.NODE_ENV != 'development' && Platform.OS === 'web') {
}
}
const PlayView = () => {
const songId = 1;
const PlayView = ({ songId, type, route }: RouteProps<PlayViewProps>) => {
const accessToken = useSelector((state: RootState) => state.user.accessToken);
const navigation = useNavigation();
const queryClient = useQueryClient();
const song = useQuery(['song', songId], () => API.getSong(songId));
const song = useQuery(['song', songId], () => API.getSong(songId), { staleTime: Infinity });
const toast = useToast();
const webSocket = useRef<WebSocket>();
const timer = useStopwatch({ autoStart: false });
const [paused, setPause] = useState<boolean>(true);
const [midiPlayer, setMidiPlayer] = useState<MidiPlayer.Player>();
const partitionRessources = useQuery(["partition", songId], () =>
API.getPartitionRessources(songId)
const stopwatch = useStopwatch();
const [isVirtualPianoVisible, setVirtualPianoVisible] = useState<boolean>(false);
const [time, setTime] = useState(0);
const [partitionRendered, setPartitionRendered] = useState(false); // Used to know when partitionview can render
const [score, setScore] = useState(0); // Between 0 and 100
const musixml = useQuery(["musixml", songId], () =>
API.getSongMusicXML(songId).then((data) => new TextDecoder().decode(data)),
{ staleTime: Infinity }
);
const [midiKeyboardFound, setMidiKeyboardFound] = useState<boolean>();
const onPause = () => {
timer.pause();
midiPlayer?.pause();
stopwatch.pause();
setPause(true);
webSocket.current?.send(JSON.stringify({
type: "pause",
paused: true,
time: Date.now()
time: time
}));
}
const onResume = () => {
if (stopwatch.isStarted()) {
stopwatch.resume();
} else {
stopwatch.start();
}
setPause(false);
midiPlayer?.play();
timer.start();
webSocket.current?.send(JSON.stringify({
type: "pause",
paused: false,
time: Date.now()
time: time
}));
}
const onEnd = () => {
webSocket.current?.send(JSON.stringify({
type: "end"
}));
stopwatch.stop();
webSocket.current?.close();
midiPlayer?.pause();
}
const onMIDISuccess = (access) => {
@@ -76,25 +90,61 @@ const PlayView = () => {
toast.show({ description: 'No MIDI Keyboard found' });
return;
}
toast.show({ description: `MIDI ready!`, placement: 'top' });
setMidiKeyboardFound(true);
let inputIndex = 0;
webSocket.current = new WebSocket(scoroBaseApiUrl);
webSocket.current.onopen = () => {
webSocket.current!.send(JSON.stringify({
type: "start",
name: "clair-de-lune" /*song.data.id*/,
id: song.data!.id,
mode: type,
bearer: accessToken
}));
};
webSocket.current.onmessage = (message) => {
try {
const data = JSON.parse(message.data);
if (data.type == 'end') {
navigation.navigate('Score');
} else if (data.song_launched == undefined) {
toast.show({ description: data, placement: 'top', colorScheme: 'secondary' });
navigation.navigate('Score', { songId: song.data!.id });
return;
}
} catch {
const points = data.info.score;
const maxPoints = data.info.maxScore || 1;
setScore(Math.floor(Math.max(points, 0) / maxPoints) * 100);
let formattedMessage = '';
let messageColor: ColorSchemeType | undefined;
if (data.type == 'miss') {
formattedMessage = translate('missed');
messageColor = 'black';
} else if (data.type == 'timing' || data.type == 'duration') {
formattedMessage = translate(data[data.type]);
switch (data[data.type]) {
case 'perfect':
messageColor = 'fuchsia';
break;
case 'great':
messageColor = 'green';
break;
case 'short':
case 'long':
case 'good':
messageColor = 'lightBlue';
break;
case 'too short':
case 'too long':
case 'wrong':
messageColor = 'grey';
break;
default:
break;
}
}
toast.show({ description: formattedMessage, placement: 'top', colorScheme: messageColor ?? 'secondary' });
} catch (e) {
console.log(e);
}
}
inputs.forEach((input) => {
@@ -106,84 +156,133 @@ const PlayView = () => {
const keyCode = message.data[1];
webSocket.current?.send(JSON.stringify({
type: keyIsPressed ? "note_on" : "note_off",
node: keyCode,
intensity: null,
time: Date.now()
note: keyCode,
id: song.data!.id,
time: time
}))
}
inputIndex++;
});
Promise.all([
queryClient.fetchQuery(['song', songId, 'midi'], () => API.getSongMidi(songId)),
SoundFont.instrument(new AudioContext(), 'electric_piano_1'),
]).then(([midiFile, audioController]) => {
const player = new MidiPlayer.Player((event) => {
if (event['noteName']) {
console.log(event);
audioController.play(event['noteName']);
}
});
player.loadArrayBuffer(midiFile);
setMidiPlayer(player);
});
}
const onMIDIFailure = () => {
toast.show({ description: `Failed to get MIDI access` });
}
useEffect(() => {
ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.LANDSCAPE).catch(() => {});
navigator.requestMIDIAccess().then(onMIDISuccess, onMIDIFailure);
let interval = setInterval(() => {
setTime(() => stopwatch.getElapsedRunningTime() - 3000) // Countdown
}, 1);
return () => {
ScreenOrientation.unlockAsync().catch(() => {});
clearInterval(timer);
onEnd();
clearInterval(interval);
}
}, [])
const score = 20;
}, []);
useEffect(() => {
// Song.data is updated on navigation.navigate (do not know why)
// Hotfix to prevent midi setup process from reruning on game end
if (navigation.getState().routes.at(-1)?.name != route.name) {
return;
}
if (song.data && !webSocket.current && partitionRendered) {
navigator.requestMIDIAccess().then(onMIDISuccess, onMIDIFailure);
}
}, [song.data, partitionRendered]);
if (!song.data || !partitionRessources.data) {
return <Center style={{ flexGrow: 1 }}>
<LoadingComponent/>
</Center>
if (!song.data || !musixml.data) {
return <LoadingView/>;
}
return (
<SafeAreaView style={{ flexGrow: 1, flexDirection: 'column' }}>
<View style={{ flexGrow: 1 }}>
<SlideView sources={partitionRessources.data} speed={200} startAt={0} />
<View style={{ flexGrow: 1, justifyContent: 'center' }}>
<PartitionView file={musixml.data}
onPartitionReady={() => setPartitionRendered(true)}
timestamp={Math.max(0, time)}
onEndReached={() => {
onEnd();
navigation.navigate('Score', { songId: song.data.id });
}}
/>
{ !partitionRendered && <LoadingComponent/> }
</View>
<Box shadow={4} style={{ height: '12%', width:'100%', borderWidth: 0.5, margin: 5 }}>
{isVirtualPianoVisible && <Column
style={{
display: 'flex',
justifyContent: "flex-end",
alignItems: "center",
height: '20%',
width: '100%',
}}
>
<VirtualPiano
onNoteDown={(note: any) => {
console.log("On note down", keyToStr(note));
}}
onNoteUp={(note: any) => {
console.log("On note up", keyToStr(note));
}}
showOctaveNumbers={true}
startNote={Note.C}
endNote={Note.B}
startOctave={2}
endOctave={5}
style={{
width: '80%',
height: '100%',
}}
highlightedNotes={
[
{ key: strToKey("D3") },
{ key: strToKey("A#"), bgColor: "#00FF00" },
]
}
/>
</Column>}
<Box shadow={4} style={{ height: '12%', width:'100%', borderWidth: 0.5, margin: 5, display: !partitionRendered ? 'none' : undefined }}>
<Row justifyContent='space-between' style={{ flexGrow: 1, alignItems: 'center' }} >
<Column space={2} style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<Text style={{ fontWeight: 'bold' }}>Score: {score}%</Text>
<Progress value={score} style={{ width: '90%' }}/>
</Column>
<Center style={{ flex: 1, alignItems: 'center' }}>
<Text style={{ fontWeight: '700' }}>Rolling in the Deep</Text>
<Text style={{ fontWeight: '700' }}>{song.data.name}</Text>
</Center>
<Row style={{ flex: 1, height: '100%', justifyContent: 'space-evenly', alignItems: 'center' }}>
<IconButton size='sm' colorScheme='secondary' variant='solid' _icon={{
as: Ionicons,
name: "play-back"
}}/>
<IconButton size='sm' variant='solid' _icon={{
as: Ionicons,
name: paused ? "play" : "pause"
}} onPress={() => {
{midiKeyboardFound && <>
<IconButton size='sm' variant='solid' icon={
<Icon as={Ionicons} name={paused ? "play" : "pause"}/>
} onPress={() => {
if (paused) {
onResume();
} else {
onPause();
}
}}/>
<Text>{timer.minutes}:{timer.seconds.toString().padStart(2, '0')}</Text>
<IconButton size='sm' colorScheme='coolGray' variant='solid' _icon={{
as: Ionicons,
name: "stop"
}} onPress={() => {
onEnd();
navigation.navigate('Score')
<IconButton size='sm' colorScheme='coolGray' variant='solid' icon={
<Icon as={MaterialCommunityIcons}
name={ isVirtualPianoVisible ? "piano-off" : "piano"} />
} onPress={() => {
setVirtualPianoVisible(!isVirtualPianoVisible);
}}/>
<Text>
{ time < 0
? paused
? '0:00'
: Math.floor((time % 60000) / 1000).toFixed(0).toString()
: `${Math.floor(time / 60000)}:${Math.floor((time % 60000) / 1000).toFixed(0).toString().padStart(2, '0')}`
}
</Text>
<IconButton size='sm' colorScheme='coolGray' variant='solid' icon={
<Icon as={Ionicons} name="stop"/>
} onPress={() => {
onEnd();
navigation.navigate('Score', { songId: song.data.id });
}}/>
</>}
</Row>
</Row>
</Box>

View File

@@ -2,7 +2,7 @@ import React from 'react';
import { Dimensions, View } from 'react-native';
import { Box, Image, Heading, HStack, Card, Button, Spacer, Text } from 'native-base';
import Translate from '../components/Translate';
import { useNavigation } from '@react-navigation/native';
import { useNavigation } from "../Navigation";
import TextButton from '../components/TextButton';
const UserMedals = () => {
@@ -89,7 +89,7 @@ const ProfileView = () => {
<PlayerStats/>
<Box w="10%" paddingY={10} paddingLeft={5} paddingRight={50} zIndex={1}>
<TextButton
onPress={() => navigation.navigate('Settings')}
onPress={() => navigation.navigate('Settings', {screen: 'Profile'})}
style={{margin: 10}}
translate={{ translationKey: 'settingsBtn' }}
/>

View File

@@ -1,73 +1,105 @@
import { Box, Button, Card, Column, Image, Progress, Row, Text, View, useTheme } from "native-base"
import { Card, Column, Image, Row, Text, useTheme, ScrollView, Center, VStack } from "native-base"
import Translate from "../components/Translate";
import SongCardGrid from "../components/SongCardGrid";
import { useNavigation } from "@react-navigation/native";
import { RouteProps, useNavigation } from "../Navigation";
import { CardBorderRadius } from "../components/Card";
import TextButton from "../components/TextButton";
import API from '../API';
import { useQueries, useQuery } from "react-query";
import { LoadingView } from "../components/Loading";
const ScoreView = (/*{ songId }, { songId: number }*/) => {
type ScoreViewProps = { songId: number }
const ScoreView = ({ songId, route }: RouteProps<ScoreViewProps>) => {
const theme = useTheme();
const navigation = useNavigation();
// const songQuery = useQuery(['song', props.songId], () => API.getSong(props.songId));
// const songScoreQuery = useQuery(['song', props.songId, 'score', 'latest'], () => API.getLastSongPerformanceScore(props.songId));
const songQuery = useQuery(['song', songId], () => API.getSong(songId));
const artistQuery = useQuery(['song', songId],
() => API.getArtist(songQuery.data!.artistId!),
{ enabled: songQuery.data != undefined }
);
const songHistoryQuery = useQuery(["song", "history"], () => API.getUserPlayHistory());
// const perfoamnceRecommandationsQuery = useQuery(['song', props.songId, 'score', 'latest', 'recommendations'], () => API.getLastSongPerformanceScore(props.songId));
return <Column style={{ flexGrow: 1, justifyContent: 'space-evenly', alignItems: 'center', padding: 10 }}>
<Text bold fontSize='lg'>Rolling in the Deep</Text>
<Text bold>Adele - 3:45</Text>
<Row style={{ flexGrow: 0.5, justifyContent: 'center' }}>
<Card shadow={3} style={{ aspectRatio: 1 }}>
<Image
style={{ zIndex: 0, aspectRatio: 1, margin: 5, borderRadius: CardBorderRadius}}
source={{ uri: 'https://imgs.search.brave.com/AinqAz0knOSOt0V3rcv7ps7aMVCo0QQfZ-1NTdwVjK0/rs:fit:1200:1200:1/g:ce/aHR0cDovLzEuYnAu/YmxvZ3Nwb3QuY29t/Ly0xTmZtZTdKbDVk/US9UaHd0Y3pieEVa/SS9BQUFBQUFBQUFP/TS9QdGx6ZWtWd2Zt/ay9zMTYwMC9BZGVs/ZSstKzIxKyUyNTI4/T2ZmaWNpYWwrQWxi/dW0rQ292ZXIlMjUy/OS5qcGc' }}
/>
</Card>
<Card shadow={3} style={{ aspectRatio: 1 }}>
<Column style={{ justifyContent: 'space-evenly', flexGrow: 1 }}>
<Row style={{ alignItems: 'center' }}>
<Text bold fontSize='xl'>
80
</Text>
<Translate translationKey='goodNotes' format={(t) => ' ' + t}/>
</Row>
<Row style={{ alignItems: 'center' }}>
<Text bold fontSize='xl'>
80
</Text>
<Translate translationKey='goodNotesInARow' format={(t) => ' ' + t}/>
</Row>
<Row style={{ alignItems: 'center' }}>
<Translate translationKey='precisionScore' format={(t) => t + ' : '}/>
<Text bold fontSize='xl'>
{"80" + "%"}
</Text>
</Row>
</Column>
{/* Precision */}
</Card>
</Row>
<SongCardGrid
heading={<Text fontSize='sm'>
<Translate translationKey="songsToGetBetter"/>
</Text>}
maxItemPerRow={5}
songs={Array.of(1, 2, 3, 4, 5).map((i) => ({
albumCover: "",
songTitle: 'Song ' + i,
artistName: "Artist",
songId: i
}))}
/>
<Row space={3} style={{ width: '100%', justifyContent: 'center' }}>
<TextButton backgroundColor='gray.300'
const recommendations = useQuery(['song', 'recommendations'], () => API.getUserRecommendations());
const artistRecommendations = useQueries(recommendations.data
?.filter(({ artistId }) => artistId !== null)
.map((song) => ({
queryKey: ['artist', song.artistId],
queryFn: () => API.getArtist(song.artistId!)
})) ?? []
)
if (!recommendations.data || artistRecommendations.find(({ data }) => !data) || !songHistoryQuery.data || !songQuery.data || (songQuery.data.artistId && !artistQuery.data)) {
return <LoadingView/>;
}
const songScore = songHistoryQuery.data.find((history) => history.songID == songId);
if (!songScore) {
return <Center>
<Translate translationKey="unknownError"/>
<TextButton
translate={{ translationKey: 'backBtn' }}
onPress={() => navigation.navigate('Home')}
/>
<TextButton
onPress={() => navigation.navigate('Song', { songId: 1 })}
translate={{ translationKey: 'playAgain' }}
</Center>;
}
return <ScrollView p={8} contentContainerStyle={{ alignItems: 'center' }}>
<VStack width={{ base: '100%', lg: '50%' }} textAlign='center'>
<Text bold fontSize='lg'>{songQuery.data.name}</Text>
<Text bold>{artistQuery.data?.name}</Text>
<Row style={{ justifyContent: 'center', display: 'flex' }}>
<Card shadow={3} style={{ flex: 1 }}>
<Image
style={{ zIndex: 0, aspectRatio: 1, margin: 5, borderRadius: CardBorderRadius}}
source={{ uri: songQuery.data.cover }}
/>
</Card>
<Card shadow={3} style={{ flex: 1 }}>
<Column style={{ justifyContent: 'space-evenly', flexGrow: 1 }}>
{/*<Row style={{ alignItems: 'center' }}>
<Text bold fontSize='xl'>
</Text>
<Translate translationKey='goodNotes' format={(t) => ' ' + t}/>
</Row>
<Row style={{ alignItems: 'center' }}>
<Text bold fontSize='xl'>
80
</Text>
<Translate translationKey='goodNotesInARow' format={(t) => ' ' + t}/>
</Row>*/}
<Row style={{ alignItems: 'center' }}>
<Translate translationKey='score' format={(t) => t + ' : '}/>
<Text bold fontSize='xl'>
{songScore.score + "pts"}
</Text>
</Row>
</Column>
</Card>
</Row>
<SongCardGrid
style={{ justifyContent: "space-evenly" }}
heading={<Text fontSize='sm'>
<Translate translationKey="songsToGetBetter"/>
</Text>}
songs={recommendations.data.map((i) => ({
albumCover: i.cover,
songTitle: i.name ,
artistName: artistRecommendations.find(({ data }) => data?.id == i.artistId)?.data?.name ?? "",
songId: i.id
}))}
/>
</Row>
</Column>
<Row space={3} style={{ width: '100%', justifyContent: 'center' }}>
<TextButton colorScheme='gray'
translate={{ translationKey: 'backBtn' }}
onPress={() => navigation.navigate('Home')}
/>
<TextButton
onPress={() => navigation.navigate('Song', { songId })}
translate={{ translationKey: 'playAgain' }}
/>
</Row>
</VStack>
</ScrollView>
}
export default ScoreView;

View File

@@ -1,6 +1,6 @@
import React, { useState } from "react";
import { Box } from "native-base";
import { useNavigation } from "@react-navigation/native";
import { useNavigation } from "../Navigation";
import SearchBarSuggestions from "../components/SearchBarSuggestions";
import { useQueries, useQuery } from "react-query";
import { SuggestionType } from "../components/SearchBar";

View File

@@ -1,249 +0,0 @@
import React from 'react';
import { View } from 'react-native';
import { Center, Button, Text, Switch, Slider, Select, Heading } from "native-base";
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { unsetAccessToken } from '../state/UserSlice';
import { useDispatch } from "react-redux";
import { RootState, useSelector } from '../state/Store';
import { useLanguage } from "../state/LanguageSlice";
import { SettingsState, updateSettings } from '../state/SettingsSlice';
import { AvailableLanguages, translate, Translate } from "../i18n/i18n";
import TextButton from '../components/TextButton';
const SettingsStack = createNativeStackNavigator();
export const MainView = ({navigation}) => {
const dispatch = useDispatch();
return (
<Center style={{ flex: 1}}>
<Button variant='ghost' onPress={() => navigation.navigate('Preferences')}>
<Translate translationKey='prefBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('Notifications')}>
<Translate translationKey='notifBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('Privacy')}>
<Translate translationKey='privBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('ChangePassword')}>
<Translate translationKey='changepasswdBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('ChangeEmail')}>
<Translate translationKey='changeemailBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('GoogleAccount')}>
<Translate translationKey='googleacctBtn'/>
</Button>
<Button variant='ghost' onPress={() => dispatch(unsetAccessToken())} >
<Translate translationKey='signOutBtn'/>
</Button>
</Center>
)
}
export const PreferencesView = ({navigation}) => {
const dispatch = useDispatch();
const language: AvailableLanguages = useSelector((state: RootState) => state.language.value);
const settings = useSelector((state: RootState) => (state.settings.settings as SettingsState));
return (
<Center style={{ flex: 1}}>
<Heading style={{ textAlign: "center" }}>
<Translate translationKey='prefBtn'/>
</Heading>
<TextButton
onPress={() => navigation.navigate('Main')} style={{ margin: 10 }}
translate={{ translationKey: 'backBtn' }}
/>
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
<Select selectedValue={settings.colorScheme}
placeholder={'Theme'}
style={{ alignSelf: 'center'}}
onValueChange={(newColorScheme) => {
dispatch(updateSettings({ colorScheme: newColorScheme as any }))
}}
>
<Select.Item label={ translate('dark') } value='dark'/>
<Select.Item label={ translate('light') } value='light'/>
<Select.Item label={ translate('system') } value='system'/>
</Select>
</View>
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
<Select selectedValue={language}
placeholder={translate('langBtn')}
style={{ alignSelf: 'center'}}
onValueChange={(itemValue) => {
dispatch(useLanguage(itemValue as AvailableLanguages));
}}>
<Select.Item label='Français' value='fr'/>
<Select.Item label='English' value='en'/>
<Select.Item label='Italiano' value='it'/>
<Select.Item label='Espanol' value='sp'/>
</Select>
</View>
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
<Select selectedValue={settings.preferedLevel}
placeholder={ translate('diffBtn') }
style={{ height: 50, width: 150, alignSelf: 'center'}}
onValueChange={(itemValue) => {
dispatch(updateSettings({ preferedLevel: itemValue as any }));
}}>
<Select.Item label={ translate('easy') } value='easy'/>
<Select.Item label={ translate('medium') } value='medium'/>
<Select.Item label={ translate('hard') } value='hard'/>
</Select>
</View>
<View style={{margin: 20}}>
<Text style={{ textAlign: 'center' }}>Color blind mode</Text>
<Switch style={{ alignSelf: 'center'}} value={settings.colorBlind} colorScheme="primary"
onValueChange={(enabled) => { dispatch(updateSettings({ colorBlind: enabled })) }}
/>
</View>
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
<Text style={{ textAlign: "center" }}>Mic volume</Text>
<Slider defaultValue={settings.micLevel} minValue={0} maxValue={1000} accessibilityLabel="hello world" step={10}
onChangeEnd={(value) => { dispatch(updateSettings({ micLevel: value })) }}
>
<Slider.Track>
<Slider.FilledTrack/>
</Slider.Track>
<Slider.Thumb/>
</Slider>
</View>
<View style={{margin: 20, maxHeight: 100, maxWidth: 500, width: '80%'}}>
<Select selectedValue={settings.preferedInputName}
placeholder={'Device'}
style={{ height: 50, width: 150, alignSelf: 'center'}}
onValueChange={(itemValue: string) => { dispatch(updateSettings({ preferedInputName: itemValue })) }}
>
<Select.Item label='Mic_0' value='0'/>
<Select.Item label='Mic_1' value='1'/>
<Select.Item label='Mic_2' value='2'/>
</Select>
</View>
</Center>
)
}
const NotificationsView = ({navigation}) => {
const dispatch = useDispatch();
const settings: SettingsState = useSelector((state: RootState) => state.settings);
return (
<Center style={{ flex: 1, justifyContent: 'center' }}>
<Heading style={{ textAlign: "center" }}>
<Translate translationKey='notifBtn'/>
</Heading>
<Button style={{ margin: 10}} onPress={() => navigation.navigate('Main')} >
<Translate translationKey='backBtn'/>
</Button>
<View style={{margin: 20}} >
<Text style={{ textAlign: "center" }}>Push notifications</Text>
<Switch value={settings.enablePushNotifications} style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"
onValueChange={(value) => { dispatch(updateSettings({ enablePushNotifications: value })) }}
/>
</View>
<View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>Email notifications</Text>
<Switch value={settings.enableMailNotifications} style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"
onValueChange={(value) => { dispatch(updateSettings({ enableMailNotifications: value })) }}
/>
</View>
<View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>Training reminder</Text>
<Switch value={settings.enableLessongsReminders} style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"
onValueChange={(value) => { dispatch(updateSettings({ enableLessongsReminders: value })) }}
/>
</View>
<View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>New songs</Text>
<Switch value={settings.enableReleaseAlerts} style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"
onValueChange={(value) => { dispatch(updateSettings({ enableReleaseAlerts: value })) }}
/>
</View>
</Center>
)
}
export const PrivacyView = ({navigation}) => {
return (
<Center style={{ flex: 1}}>
<Heading style={{ textAlign: "center" }}>
<Translate translationKey='privBtn'/>
</Heading>
<Button onPress={() => navigation.navigate('Main')} style={{ margin: 10 }}>
<Translate translationKey='backBtn'/>
</Button>
<View style={{margin: 20}} >
<Text style={{ textAlign: "center" }}>Data Collection</Text>
<Switch style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"/>
</View>
<View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>Custom Adds</Text>
<Switch style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"/>
</View>
<View style={{margin: 20}}>
<Text style={{ textAlign: "center" }}>Recommendations</Text>
<Switch style={{ alignSelf: 'center', margin: 10 }} colorScheme="primary"/>
</View>
</Center>
)
}
export const ChangePasswordView = ({navigation}) => {
return (
<Center style={{ flex: 1}}>
<Button onPress={() => navigation.navigate('Main')}>Back</Button>
<Text>ChangePassword</Text>
</Center>
)
}
export const ChangeEmailView = ({navigation}) => {
return (
<Center style={{ flex: 1}}>
<Button onPress={() => navigation.navigate('Main')}>Back</Button>
<Text>ChangeEmail</Text>
</Center>
)
}
export const GoogleAccountView = ({navigation}) => {
return (
<Center style={{ flex: 1}}>
<Button onPress={() => navigation.navigate('Main')}>Back</Button>
<Text>GoogleAccount</Text>
</Center>
)
}
const SetttingsNavigator = () => {
return (
<SettingsStack.Navigator initialRouteName='Main' screenOptions={{headerShown: false}}>
<SettingsStack.Screen name='Main' component={MainView} />
<SettingsStack.Screen name='Preferences' component={PreferencesView} />
<SettingsStack.Screen name='Notifications' component={NotificationsView} />
<SettingsStack.Screen name='Privacy' component={PrivacyView} />
<SettingsStack.Screen name='ChangePassword' component={ChangePasswordView} />
<SettingsStack.Screen name='ChangeEmail' component={ChangeEmailView} />
<SettingsStack.Screen name='GoogleAccount' component={GoogleAccountView} />
</SettingsStack.Navigator>
)
}
export default SetttingsNavigator;

View File

@@ -1,23 +1,21 @@
import { useNavigation, useRoute } from "@react-navigation/native";
import { Button, Divider, Box, Center, Image, Text, VStack, PresenceTransition, Icon } from "native-base";
import { Divider, Box, Center, Image, Text, VStack, PresenceTransition, Icon, Stack } from "native-base";
import { useQuery } from 'react-query';
import LoadingComponent from "../components/Loading";
import LoadingComponent, { LoadingView } from "../components/Loading";
import React, { useEffect, useState } from "react";
import { Translate, translate } from "../i18n/i18n";
import formatDuration from "format-duration";
import { Ionicons } from '@expo/vector-icons';
import API from "../API";
import TextButton from "../components/TextButton";
import { RouteProps, useNavigation } from "../Navigation";
interface SongLobbyProps {
// The unique identifier to find a song
songId: number;
}
const SongLobbyView = () => {
const route = useRoute();
const SongLobbyView = (props: RouteProps<SongLobbyProps>) => {
const navigation = useNavigation();
const props: SongLobbyProps = route.params as any;
const songQuery = useQuery(['song', props.songId], () => API.getSong(props.songId));
const chaptersQuery = useQuery(['song', props.songId, 'chapters'], () => API.getSongChapters(props.songId));
const scoresQuery = useQuery(['song', props.songId, 'scores'], () => API.getSongHistory(props.songId));
@@ -28,9 +26,7 @@ const SongLobbyView = () => {
}, [chaptersOpen]);
useEffect(() => {}, [songQuery.isLoading]);
if (songQuery.isLoading || scoresQuery.isLoading)
return <Center style={{ flexGrow: 1 }}>
<LoadingComponent/>
</Center>
return <LoadingView/>;
return (
<Box style={{ padding: 30, flexDirection: 'column' }}>
<Box style={{ flexDirection: 'row', height: '30%'}}>
@@ -39,7 +35,7 @@ const SongLobbyView = () => {
</Box>
<Box style={{ flex: 0.5 }}/>
<Box style={{ flex: 3, padding: 10, flexDirection: 'column', justifyContent: 'space-between' }}>
<Box flex={1}>
<Stack flex={1} space={3}>
<Text bold isTruncated numberOfLines={2} fontSize='lg'>{songQuery.data!.name}</Text>
<Text>
<Translate translationKey='level'
@@ -47,10 +43,15 @@ const SongLobbyView = () => {
/>
</Text>
<TextButton translate={{ translationKey: 'playBtn' }} width='auto'
onPress={() => navigation.navigate('Play', { songId: songQuery.data?.id })}
onPress={() => navigation.navigate('Play', { songId: songQuery.data!.id, type: 'normal' })}
rightIcon={<Icon as={Ionicons} name="play-outline"/>}
/>
</Box>
<TextButton translate={{ translationKey: 'practiceBtn' }} width='auto'
onPress={() => navigation.navigate('Play', { songId: songQuery.data!.id, type: 'practice' })}
rightIcon={<Icon as={Ionicons} name="play-outline"/>}
colorScheme='secondary'
/>
</Stack>
</Box>
</Box>
<Box style={{ flexDirection: 'row', justifyContent: 'space-between', padding: 30}}>
@@ -58,13 +59,13 @@ const SongLobbyView = () => {
<Text bold fontSize='lg'>
<Translate translationKey='bestScore'/>
</Text>
<Text>{scoresQuery.data!.sort()[0]?.score}</Text>
<Text>{scoresQuery.data!.sort()[0]?.score ?? 0}</Text>
</Box>
<Box style={{ flexDirection: 'column', alignItems: 'center' }}>
<Text bold fontSize='lg'>
<Translate translationKey='lastScore'/>
</Text>
<Text>{scoresQuery.data!.slice(-1)[0]!.score}</Text>
<Text>{scoresQuery.data!.slice(-1)[0]?.score ?? 0}</Text>
</Box>
</Box>
{/* <Text style={{ paddingBottom: 10 }}>{songQuery.data!.description}</Text> */}

View File

@@ -0,0 +1,227 @@
import React from "react";
import { useNavigation } from "../Navigation";
import {
View,
Text,
Stack,
Box,
useToast,
AspectRatio,
Column,
useBreakpointValue,
Image,
Link,
Center,
Row,
Heading,
Icon,
} from "native-base";
import { FontAwesome5 } from "@expo/vector-icons";
import BigActionButton from "../components/BigActionButton";
import API, { APIError } from "../API";
import { setAccessToken } from "../state/UserSlice";
import { useDispatch } from "../state/Store";
import { translate } from "../i18n/i18n";
const handleGuestLogin = async (
apiSetter: (accessToken: string) => void
): Promise<string> => {
const apiAccess = await API.createAndGetGuestAccount();
apiSetter(apiAccess);
return translate("loggedIn");
};
const imgLogin =
"https://media.discordapp.net/attachments/717080637038788731/1095980610981478470/Octopus_a_moder_style_image_of_a_musician_showing_a_member_card_c0b9072c-d834-40d5-bc83-796501e1382c.png?width=657&height=657";
const imgGuest =
"https://media.discordapp.net/attachments/717080637038788731/1095996800835539014/Chromacase_guest_2.png?width=865&height=657";
const imgRegister =
"https://media.discordapp.net/attachments/717080637038788731/1095991220267929641/chromacase_register.png?width=1440&height=511";
const imgBanner =
"https://chromacase.studio/wp-content/uploads/2023/03/music-sheet-music-color-2462438.jpg";
const imgLogo =
"https://chromacase.studio/wp-content/uploads/2023/03/cropped-cropped-splashLogo-280x300.png";
const StartPageView = () => {
const navigation = useNavigation();
const screenSize = useBreakpointValue({ base: "small", md: "big" });
const isSmallScreen = screenSize === "small";
const dispatch = useDispatch();
const toast = useToast();
return (
<View
style={{
width: "100%",
height: "100%",
}}
>
<Center>
<Row
style={{
alignItems: "center",
justifyContent: "center",
marginTop: 20,
}}
>
<Icon
as={
<Image
alt="Chromacase logo"
source={{
uri: imgLogo,
}}
/>
}
size={isSmallScreen ? "5xl" : "6xl"}
/>
<Heading fontSize={isSmallScreen ? "3xl" : "5xl"}>Chromacase</Heading>
</Row>
</Center>
<Stack
direction={screenSize === "small" ? "column" : "row"}
style={{
width: "100%",
justifyContent: "center",
alignItems: "center",
}}
>
<BigActionButton
title="Authenticate"
subtitle="Save and resume your learning at anytime on all devices"
image={imgLogin}
iconName="user"
iconProvider={FontAwesome5}
onPress={() => navigation.navigate("Login", { isSignup: false })}
style={{
width: isSmallScreen ? "90%" : "clamp(100px, 33.3%, 600px)",
height: "300px",
margin: "clamp(10px, 2%, 50px)",
}}
/>
<BigActionButton
title="Test Chromacase"
subtitle="Use a guest account to see around but your progression won't be saved"
image={imgGuest}
iconName="user-clock"
iconProvider={FontAwesome5}
onPress={() => {
try {
handleGuestLogin((accessToken: string) => {
dispatch(setAccessToken(accessToken));
});
} catch (error) {
if (error instanceof APIError) {
toast.show({ description: translate(error.userMessage) });
return;
}
toast.show({ description: error as string });
}
}}
style={{
width: isSmallScreen ? "90%" : "clamp(100px, 33.3%, 600px)",
height: "300px",
margin: "clamp(10px, 2%, 50px)",
}}
/>
</Stack>
<Center>
<BigActionButton
title="Register"
image={imgRegister}
subtitle="Create an account to save your progress"
iconProvider={FontAwesome5}
iconName="user-plus"
onPress={() => navigation.navigate("Login", { isSignup: true })}
style={{
height: "150px",
width: isSmallScreen ? "90%" : "clamp(150px, 50%, 600px)",
}}
/>
</Center>
<Column
style={{
width: "100%",
marginTop: 40,
display: "flex",
alignItems: "center",
}}
>
<Box
style={{
maxWidth: "90%",
}}
>
<Heading fontSize="4xl" style={{ textAlign: "center" }}>
What is Chromacase?
</Heading>
<Text fontSize={"xl"}>
Chromacase is a free and open source project that aims to provide a
complete learning experience for anyone willing to learn piano.
</Text>
</Box>
<Box
style={{
width: "90%",
marginTop: 20,
}}
>
<Box
style={{
width: "100%",
height: "100%",
display: "flex",
alignItems: "center",
}}
>
<Link
href="https://chromacase.studio"
isExternal
style={{
width: "clamp(200px, 100%, 700px)",
position: "relative",
overflow: "hidden",
borderRadius: 10,
}}
>
<AspectRatio ratio={40 / 9} style={{ width: "100%" }}>
<Image
alt="Chromacase Banner"
source={{ uri: imgBanner }}
resizeMode="cover"
/>
</AspectRatio>
<Box
style={{
position: "absolute",
top: 0,
left: 0,
width: "100%",
height: "100%",
backgroundColor: "rgba(0,0,0,0.5)",
}}
></Box>
<Heading
fontSize="2xl"
style={{
textAlign: "center",
position: "absolute",
top: "40%",
left: 20,
color: "white",
}}
>
Click here for more infos
</Heading>
</Link>
</Box>
</Box>
</Column>
</View>
);
};
export default StartPageView;

View File

@@ -0,0 +1,40 @@
import React from "react";
import SignUpForm from "../../components/forms/signupform";
import { Center, Heading, Text } from "native-base";
import API, { APIError } from "../../API";
import { translate } from "../../i18n/i18n";
const handleSubmit = async (
username: string,
password: string,
email: string
) => {
try {
await API.transformGuestToUser({ username, password, email });
} catch (error) {
if (error instanceof APIError) return translate(error.userMessage);
if (error instanceof Error) return error.message;
return translate("unknownError");
}
return translate("loggedIn");
};
const GuestToUserView = () => {
return (
<Center flex={1} justifyContent={"center"}>
<Center width="90%" justifyContent={"center"}>
<Heading>{translate("signUp")}</Heading>
<Text mt={5} mb={10}>
{translate("transformGuestToUserExplanations")}
</Text>
<SignUpForm
onSubmit={(username, password, email) =>
handleSubmit(username, password, email)
}
/>
</Center>
</Center>
);
};
export default GuestToUserView;

View File

@@ -0,0 +1,80 @@
import React from "react";
import { Center, Heading } from "native-base";
import { translate, Translate } from "../../i18n/i18n";
import ElementList from "../../components/GtkUI/ElementList";
import useUserSettings from "../../hooks/userSettings";
import { LoadingView } from "../../components/Loading";
const NotificationsView = () => {
const { settings, updateSettings } = useUserSettings();
if (!settings.data) {
return <LoadingView/>
}
return (
<Center style={{ flex: 1, justifyContent: "center" }}>
<Heading style={{ textAlign: "center" }}>
<Translate translationKey="notifBtn" />
</Heading>
<ElementList
style={{
marginTop: 20,
width: "90%",
maxWidth: 850,
}}
elements={[
{
type: "toggle",
title: translate("SettingsNotificationsPushNotifications"),
data: {
value: settings.data.notifications.pushNotif,
onToggle: () => {
updateSettings({
notifications: { pushNotif: !settings.data.notifications.pushNotif },
});
},
},
},
{
type: "toggle",
title: translate("SettingsNotificationsEmailNotifications"),
data: {
value: settings.data.notifications.emailNotif,
onToggle: () => {
updateSettings({
notifications: { emailNotif: !settings.data.notifications.emailNotif },
});
},
},
},
{
type: "toggle",
title: translate("SettingsNotificationsTrainingReminder"),
data: {
value: settings.data.notifications.trainNotif,
onToggle: () => {
updateSettings({
notifications: { trainNotif: !settings.data.notifications.trainNotif },
});
},
},
},
{
type: "toggle",
title: translate("SettingsNotificationsReleaseAlert"),
data: {
value: settings.data.notifications.newSongNotif,
onToggle: () => {
updateSettings({
notifications: { newSongNotif: !settings.data.notifications.newSongNotif },
});
},
},
},
]}
/>
</Center>
);
};
export default NotificationsView;

View File

@@ -0,0 +1,151 @@
import React from "react";
import { useDispatch } from "react-redux";
import {
Center,
Heading,
} from "native-base";
import { useLanguage } from "../../state/LanguageSlice";
import {
AvailableLanguages,
DefaultLanguage,
translate,
Translate,
} from "../../i18n/i18n";
import { RootState, useSelector } from "../../state/Store";
import { updateSettings } from "../../state/SettingsSlice";
import ElementList from "../../components/GtkUI/ElementList";
const PreferencesView = () => {
const dispatch = useDispatch();
const language: AvailableLanguages = useSelector(
(state: RootState) => state.language.value
);
const settings = useSelector(
(state: RootState) => state.settings.local
);
return (
<Center style={{ flex: 1 }}>
<Heading style={{ textAlign: "center" }}>
<Translate translationKey="prefBtn" />
</Heading>
<ElementList
style={{
marginTop: 20,
width: "90%",
maxWidth: 850,
}}
elements={[
{
type: "dropdown",
title: translate("SettingsPreferencesTheme"),
data: {
value: settings.colorScheme,
defaultValue: "system",
onSelect: (newColorScheme) => {
dispatch(
updateSettings({ colorScheme: newColorScheme as any })
);
},
options: [
{ label: translate("dark"), value: "dark" },
{ label: translate("light"), value: "light" },
{ label: translate("system"), value: "system" },
],
},
},
{
type: "dropdown",
title: translate("SettingsPreferencesLanguage"),
data: {
value: language,
defaultValue: DefaultLanguage,
onSelect: (itemValue) => {
dispatch(useLanguage(itemValue as AvailableLanguages));
},
options: [
{ label: "Français", value: "fr" },
{ label: "English", value: "en" },
{ label: "Espanol", value: "sp" },
],
},
},
{
type: "dropdown",
title: translate("SettingsPreferencesDifficulty"),
data: {
value: settings.difficulty,
defaultValue: "medium",
onSelect: (itemValue) => {
dispatch(updateSettings({ difficulty: itemValue as any }));
},
options: [
{ label: translate("easy"), value: "beg" },
{ label: translate("medium"), value: "inter" },
{ label: translate("hard"), value: "pro" },
],
},
},
]}
/>
<ElementList
style={{
marginTop: 20,
width: "90%",
maxWidth: 850,
}}
elements={[
{
type: "toggle",
title: translate("SettingsPreferencesColorblindMode"),
data: {
value: settings.colorBlind,
onToggle: () => {
dispatch(updateSettings({ colorBlind: !settings.colorBlind }));
},
},
},
]}
/>
<ElementList
style={{
marginTop: 20,
width: "90%",
maxWidth: 850,
}}
elements={[
{
type: "range",
title: translate("SettingsPreferencesMicVolume"),
data: {
value: settings.micVolume,
min: 0,
max: 1000,
step: 10,
onChange: (value) => {
dispatch(updateSettings({ micVolume: value }));
},
},
},
/*{
type: "dropdown",
title: translate("SettingsPreferencesDevice"),
data: {
value: settings.preferedInputName || "0",
defaultValue: "0",
onSelect: (itemValue: string) => {
dispatch(updateSettings({ preferedInputName: itemValue }));
},
options: [
{ label: "Mic_0", value: "0" },
{ label: "Mic_1", value: "1" },
{ label: "Mic_2", value: "2" },
],
},
},*/
]}
/>
</Center>
);
};
export default PreferencesView;

View File

@@ -0,0 +1,65 @@
import React from "react";
import { Center, Heading } from "native-base";
import { translate } from "../../i18n/i18n";
import ElementList from "../../components/GtkUI/ElementList";
import { useDispatch } from "react-redux";
import { RootState, useSelector } from "../../state/Store";
import { updateSettings } from "../../state/SettingsSlice";
import useUserSettings from "../../hooks/userSettings";
import { LoadingView } from "../../components/Loading";
const PrivacyView = () => {
const dispatch = useDispatch();
const settings = useSelector((state: RootState) => state.settings.local);
const { settings: userSettings, updateSettings: updateUserSettings } = useUserSettings();
if (!userSettings.data) {
return <LoadingView/>;
}
return (
<Center style={{ flex: 1 }}>
<Heading style={{ textAlign: "center" }}>{translate("privBtn")}</Heading>
<ElementList
style={{
marginTop: 20,
width: "90%",
maxWidth: 850,
}}
elements={[
{
type: "toggle",
title: translate("dataCollection"),
data: {
value: settings.dataCollection,
onToggle: () =>
dispatch(
updateSettings({ dataCollection: !settings.dataCollection })
),
},
},
{
type: "toggle",
title: translate("customAds"),
data: {
value: settings.customAds,
onToggle: () =>
dispatch(updateSettings({ customAds: !settings.customAds })),
},
},
{
type: "toggle",
title: translate("recommendations"),
data: {
value: userSettings.data.recommendations,
onToggle: () =>
updateUserSettings({ recommendations: !userSettings.data.recommendations })
},
},
]}
/>
</Center>
);
};
export default PrivacyView;

View File

@@ -0,0 +1,225 @@
import API from "../../API";
import { useDispatch } from "react-redux";
import { unsetAccessToken } from "../../state/UserSlice";
import React from "react";
import {
Column,
Text,
Button,
Box,
Flex,
Center,
Heading,
Avatar,
Popover,
} from "native-base";
import TextButton from "../../components/TextButton";
import { LoadingView } from "../../components/Loading";
import ElementList from "../../components/GtkUI/ElementList";
import { translate } from "../../i18n/i18n";
import { useQuery } from "react-query";
const getInitials = (name: string) => {
return name.split(" ").map((n) => n[0]).join("");
};
const ProfileSettings = ({ navigation }: { navigation: any }) => {
const userQuery = useQuery(['user'], () => API.getUserInfo());
const dispatch = useDispatch();
if (!userQuery.data || userQuery.isLoading) {
return <LoadingView/>
}
const user = userQuery.data;
return (
<Flex
style={{
flex: 1,
alignItems: "center",
paddingTop: 40,
}}
>
<Column
style={{
width: "100%",
alignItems: "center",
}}
>
<Center>
<Avatar size="2xl" source={{ uri: user.data.avatar }}>
{getInitials(user.name)}
</Avatar>
</Center>
<ElementList
style={{
marginTop: 20,
width: "90%",
maxWidth: 850,
}}
elements={[
{
type: "text",
title: translate("email"),
data: {
text: user.email || translate("NoAssociatedEmail"),
onPress: () => {
navigation.navigate("ChangeEmail");
},
},
},
]}
/>
<ElementList
style={{
marginTop: 20,
width: "90%",
maxWidth: 850,
}}
elements={[
{
type: "text",
title: translate("username"),
data: {
text: user.name,
},
},
{
type: "text",
title: "ID",
helperText: "This is your unique ID, be proud of it!",
data: {
text: user.id.toString(),
},
},
{
type: "text",
title: translate("nbGamesPlayed"),
data: {
text: user.data.gamesPlayed.toString(),
},
},
{
type: "text",
title: "XP",
description: translate("XPDescription"),
data: {
text: user.data.xp.toString(),
},
},
{
type: "text",
title: translate("userCreatedAt"),
helperText:
"La date de création est actuellement arbitraire car le serveur ne retourne pas cette information",
data: {
text: user.data.createdAt.toLocaleDateString(),
},
},
{
type: "text",
title: translate("premiumAccount"),
data: {
text: translate(user.premium ? "yes" : "no"),
},
},
]}
/>
<Heading fontSize="20" mt="7">
Fonctionnalités premium
</Heading>
<ElementList
style={{
marginTop: 10,
width: "90%",
maxWidth: 850,
}}
elements={[
{
type: "toggle",
title: "Piano Magique",
description:
"Fait apparaître de la lumière sur le piano pendant les parties",
helperText:
"Vous devez posséder le module physique lumineux Chromacase pour pouvoir utiliser cette fonctionnalité",
disabled: true,
data: {
value: false,
onToggle: () => {},
},
},
{
type: "dropdown",
title: "Thème de piano",
disabled: true,
data: {
value: "default",
onValueChange: () => {},
options: [
{
label: "Default",
value: "default",
},
{
label: "Catpuccino",
value: "catpuccino",
},
],
},
},
]}
/>
</Column>
<Box mt={10}>
{!user.isGuest && (
<TextButton
onPress={() => dispatch(unsetAccessToken())}
translate={{
translationKey: "signOutBtn",
}}
/>
)}
{user.isGuest && (
<Popover
trigger={(triggerProps) => (
<Button {...triggerProps}>{translate("signOutBtn")}</Button>
)}
>
<Popover.Content>
<Popover.Arrow />
<Popover.Body>
<Heading size="md" mb={2}>
{translate("Attention")}
</Heading>
<Text>
{translate(
"YouAreCurrentlyConnectedWithAGuestAccountWarning"
)}
</Text>
<Button.Group variant="ghost" space={2}>
<Button
onPress={() => dispatch(unsetAccessToken())}
colorScheme="red"
>
{translate("signOutBtn")}
</Button>
<Button
onPress={() => {
navigation.navigate("GuestToUser");
}}
colorScheme="green"
>
{translate("signUpBtn")}
</Button>
</Button.Group>
</Popover.Body>
</Popover.Content>
</Popover>
)}
</Box>
</Flex>
);
};
export default ProfileSettings;

View File

@@ -0,0 +1,184 @@
import React from 'react';
import { Center, Button, Text, Heading, Box } from "native-base";
import { createNativeStackNavigator } from '@react-navigation/native-stack';
import { unsetAccessToken } from '../../state/UserSlice';
import { useDispatch } from "react-redux";
import { translate, Translate } from "../../i18n/i18n";
import createTabRowNavigator from '../../components/navigators/TabRowNavigator';
import { MaterialCommunityIcons, FontAwesome5 } from '@expo/vector-icons';
import ChangePasswordForm from '../../components/forms/changePasswordForm';
import ChangeEmailForm from '../../components/forms/changeEmailForm';
import ProfileSettings from './SettingsProfileView';
import NotificationsView from './NotificationView';
import PrivacyView from './PrivacyView';
import PreferencesView from './PreferencesView';
import GuestToUserView from './GuestToUserView';
import { useQuery } from 'react-query';
import API from '../../API';
const SettingsStack = createNativeStackNavigator();
const handleChangeEmail = async (newEmail: string): Promise<string> => {
try {
let response = await API.updateUserEmail(newEmail);
return translate('emailUpdated');
} catch (e) {
throw e;
}
}
const handleChangePassword = async (oldPassword: string, newPassword: string): Promise<string> => {
try {
let response = await API.updateUserPassword(oldPassword, newPassword);
return translate('passwordUpdated');
} catch (e) {
throw e;
}
}
const MainView = ({navigation}) => {
const dispatch = useDispatch();
return (
<Center style={{ flex: 1}}>
<Button variant='ghost' onPress={() => navigation.navigate('Preferences')}>
<Translate translationKey='prefBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('Notifications')}>
<Translate translationKey='notifBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('Privacy')}>
<Translate translationKey='privBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('ChangePassword')}>
<Translate translationKey='changepasswdBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('ChangeEmail')}>
<Translate translationKey='changeemailBtn'/>
</Button>
<Button variant='ghost' onPress={() => navigation.navigate('GoogleAccount')}>
<Translate translationKey='googleacctBtn'/>
</Button>
<Button variant='ghost' onPress={() => dispatch(unsetAccessToken())} >
<Translate translationKey='signOutBtn'/>
</Button>
</Center>
)
}
export const ChangePasswordView = ({navigation}) => {
return (
<Center style={{ flex: 1}}>
<Heading paddingBottom={'2%'}>{translate('changePassword')}</Heading>
<ChangePasswordForm onSubmit={(oldPassword, newPassword) => handleChangePassword(oldPassword, newPassword)}/>
</Center>
)
}
export const ChangeEmailView = ({navigation}) => {
return (
<Center style={{ flex: 1}}>
<Heading paddingBottom={'2%'}>{translate('changeEmail')}</Heading>
<ChangeEmailForm onSubmit={(oldEmail, newEmail) => handleChangeEmail(newEmail)}/>
</Center>
)
}
export const GoogleAccountView = ({navigation}) => {
return (
<Center style={{ flex: 1}}>
<Text>GoogleAccount</Text>
</Center>
)
}
export const PianoSettingsView = ({navigation}) => {
return (
<Center style={{ flex: 1}}>
<Text>Global settings for the virtual piano</Text>
</Center>
)
}
const TabRow = createTabRowNavigator();
const SetttingsNavigator = () => {
const userQuery = useQuery(['user'], () => API.getUserInfo());
const user = userQuery.data;
if (userQuery.isError) {
user.isGuest = false;
}
if (userQuery.isLoading) {
return (
<Center style={{ flex: 1}}>
<Text>Loading...</Text>
</Center>
)
}
return (
<TabRow.Navigator initialRouteName='InternalDefault'>
{/* I'm doing this to be able to land on the summary of settings when clicking on settings and directly to the
wanted settings page if needed so I need to do special work with the 0 index */}
<TabRow.Screen name='InternalDefault' component={Box} />
{user && user.isGuest &&
<TabRow.Screen name='GuestToUser' component={GuestToUserView} options={{
title: translate('SettingsCategoryGuest'),
iconProvider: FontAwesome5,
iconName: "user-clock"
}} />
}
<TabRow.Screen name='Profile' component={ProfileSettings} options={{
title: translate('SettingsCategoryProfile'),
iconProvider: FontAwesome5,
iconName: "user"
}} />
<TabRow.Screen name='Preferences' component={PreferencesView} options={{
title: translate('SettingsCategoryPreferences'),
iconProvider: FontAwesome5,
iconName: "music"
}} />
<TabRow.Screen name='Notifications' component={NotificationsView} options={{
title: translate('SettingsCategoryNotifications'),
iconProvider: FontAwesome5,
iconName: "bell"
}}/>
<TabRow.Screen name='Privacy' component={PrivacyView} options={{
title: translate('SettingsCategoryPrivacy'),
iconProvider: FontAwesome5,
iconName: "lock"
}} />
<TabRow.Screen name='ChangePassword' component={ChangePasswordView} options={{
title: translate('SettingsCategorySecurity'),
iconProvider: FontAwesome5,
iconName: "key"
}}/>
<TabRow.Screen name='ChangeEmail' component={ChangeEmailView} options={{
title: translate('SettingsCategoryEmail'),
iconProvider: FontAwesome5,
iconName: "envelope"
}} />
<TabRow.Screen name='GoogleAccount' component={GoogleAccountView} options={{
title: translate('SettingsCategoryGoogle'),
iconProvider: FontAwesome5,
iconName: "google"
}} />
<TabRow.Screen name='PianoSettings' component={PianoSettingsView} options={{
title: translate('SettingsCategoryPiano'),
iconProvider: MaterialCommunityIcons,
iconName: "piano"
}} />
</TabRow.Navigator>
)
}
export default SetttingsNavigator;

View File

@@ -1759,7 +1759,7 @@
dependencies:
tslib "^2.4.0"
"@gar/promisify@^1.0.1":
"@gar/promisify@^1.0.1", "@gar/promisify@^1.1.3":
version "1.1.3"
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
integrity sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==
@@ -2267,6 +2267,14 @@
"@gar/promisify" "^1.0.1"
semver "^7.3.5"
"@npmcli/fs@^2.1.0":
version "2.1.2"
resolved "https://registry.yarnpkg.com/@npmcli/fs/-/fs-2.1.2.tgz#a9e2541a4a2fec2e69c29b35e6060973da79b865"
integrity sha512-yOJKRvohFOaLqipNtwYB9WugyZKhC/DZC4VYPmpaCzDBrA8YpK3qHZ8/HGscMnE4GqbkLNuVcCnxkeQEdGt6LQ==
dependencies:
"@gar/promisify" "^1.1.3"
semver "^7.3.5"
"@npmcli/move-file@^1.0.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-1.1.2.tgz#1a82c3e372f7cae9253eb66d72543d6b8685c674"
@@ -2275,6 +2283,14 @@
mkdirp "^1.0.4"
rimraf "^3.0.2"
"@npmcli/move-file@^2.0.0":
version "2.0.1"
resolved "https://registry.yarnpkg.com/@npmcli/move-file/-/move-file-2.0.1.tgz#26f6bdc379d87f75e55739bab89db525b06100e4"
integrity sha512-mJd2Z5TjYWq/ttPLLGqArdtnC74J6bOzg4rMDnN+p1xTacZ2yPRCk2y0oSWQtygLR9YVQXgOcONrwtnk3JupxQ==
dependencies:
mkdirp "^1.0.4"
rimraf "^3.0.2"
"@pmmmwh/react-refresh-webpack-plugin@^0.5.3":
version "0.5.10"
resolved "https://registry.yarnpkg.com/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.10.tgz#2eba163b8e7dbabb4ce3609ab5e32ab63dda3ef8"
@@ -4357,6 +4373,11 @@
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82"
integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==
"@tootallnate/once@2":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf"
integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==
"@types/aria-query@^5.0.1":
version "5.0.1"
resolved "https://registry.yarnpkg.com/@types/aria-query/-/aria-query-5.0.1.tgz#3286741fb8f1e1580ac28784add4c7a1d49bdfbc"
@@ -4716,6 +4737,11 @@
resolved "https://registry.yarnpkg.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz#b6725d5f4af24ace33b36fafd295136e75509f43"
integrity sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==
"@types/vexflow@^1.2.38":
version "1.2.38"
resolved "https://registry.yarnpkg.com/@types/vexflow/-/vexflow-1.2.38.tgz#a09b32956b2005e567cac851d539961c43ce0992"
integrity sha512-OmEfhv07molNFqbOJ/UD2bUHZbeUzKo4aj+jpe21Ce8+xY2ihCXwcUcfSHv0oCVdnw/cpkPxQcIyLh/MCd7e/g==
"@types/webpack-env@^1.16.0", "@types/webpack-env@^1.17.0":
version "1.18.0"
resolved "https://registry.yarnpkg.com/@types/webpack-env/-/webpack-env-1.18.0.tgz#ed6ecaa8e5ed5dfe8b2b3d00181702c9925f13fb"
@@ -5174,6 +5200,11 @@ abab@^2.0.3, abab@^2.0.5:
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
abbrev@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
integrity sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==
abort-controller@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392"
@@ -5252,13 +5283,22 @@ adsr@^1.0.0:
resolved "https://registry.yarnpkg.com/adsr/-/adsr-1.0.1.tgz#a7bc08e5ef8a71e6364abc96fce7df1c44881cc3"
integrity sha512-thr9LK4jxApOzBA33IWOA83bXJFbyfbeozpHXyrMQOIhUni198uRxXqDhobW0S/51iokqty2Yz2WbLZbE6tntQ==
agent-base@6:
agent-base@6, agent-base@^6.0.2:
version "6.0.2"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-6.0.2.tgz#49fff58577cfee3f37176feab4c22e00f86d7f77"
integrity sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==
dependencies:
debug "4"
agentkeepalive@^4.2.1:
version "4.3.0"
resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255"
integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==
dependencies:
debug "^4.1.0"
depd "^2.0.0"
humanize-ms "^1.2.1"
aggregate-error@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a"
@@ -5458,6 +5498,14 @@ are-we-there-yet@^2.0.0:
delegates "^1.0.0"
readable-stream "^3.6.0"
are-we-there-yet@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz#679df222b278c64f2cdba1175cdc00b0d96164bd"
integrity sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==
dependencies:
delegates "^1.0.0"
readable-stream "^3.6.0"
arg@4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.0.tgz#583c518199419e0037abb74062c37f8519e575f0"
@@ -6000,6 +6048,11 @@ binjumper@^0.1.4:
resolved "https://registry.yarnpkg.com/binjumper/-/binjumper-0.1.4.tgz#4acc0566832714bd6508af6d666bd9e5e21fc7f8"
integrity sha512-Gdxhj+U295tIM6cO4bJO1jsvSjBVHNpj2o/OwW7pqDEtaqF6KdOxjtbo93jMMKAkP7+u09+bV8DhSqjIv4qR3w==
bit-twiddle@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/bit-twiddle/-/bit-twiddle-1.0.2.tgz#0c6c1fabe2b23d17173d9a61b7b7093eb9e1769e"
integrity sha512-B9UhK0DKFZhoTFcfvAzhqsjStvGJp9vYWf3+6SNTtdSQnvIgfkHbgHrg/e4+TH71N2GDu8tpmCVoyfrL1d7ntA==
bl@^4.0.3, bl@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a"
@@ -6114,6 +6167,13 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
dependencies:
balanced-match "^1.0.0"
braces@^2.3.1, braces@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/braces/-/braces-2.3.2.tgz#5979fd3f14cd531565e5fa2df1abfff1dfaee729"
@@ -6382,6 +6442,30 @@ cacache@^15.0.5, cacache@^15.3.0:
tar "^6.0.2"
unique-filename "^1.1.1"
cacache@^16.1.0:
version "16.1.3"
resolved "https://registry.yarnpkg.com/cacache/-/cacache-16.1.3.tgz#a02b9f34ecfaf9a78c9f4bc16fceb94d5d67a38e"
integrity sha512-/+Emcj9DAXxX4cwlLmRI9c166RuL3w30zp4R7Joiv2cQTtTtA+jeuCAjH3ZlGnYS3tKENSrKhAzVVP9GVyzeYQ==
dependencies:
"@npmcli/fs" "^2.1.0"
"@npmcli/move-file" "^2.0.0"
chownr "^2.0.0"
fs-minipass "^2.1.0"
glob "^8.0.1"
infer-owner "^1.0.4"
lru-cache "^7.7.1"
minipass "^3.1.6"
minipass-collect "^1.0.2"
minipass-flush "^1.0.5"
minipass-pipeline "^1.2.4"
mkdirp "^1.0.4"
p-map "^4.0.0"
promise-inflight "^1.0.1"
rimraf "^3.0.2"
ssri "^9.0.0"
tar "^6.1.11"
unique-filename "^2.0.0"
cache-base@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
@@ -6846,7 +6930,7 @@ color-string@^1.5.3, color-string@^1.6.0:
color-name "^1.0.0"
simple-swizzle "^0.2.2"
color-support@^1.1.2:
color-support@^1.1.2, color-support@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/color-support/-/color-support-1.1.3.tgz#93834379a1cc9a0c61f82f52f0d04322251bd5a2"
integrity sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==
@@ -7491,7 +7575,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0:
dependencies:
ms "2.0.0"
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
debug@4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4:
version "4.3.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==
@@ -7693,7 +7777,7 @@ denodeify@^1.2.1:
resolved "https://registry.yarnpkg.com/denodeify/-/denodeify-1.2.1.tgz#3a36287f5034e699e7577901052c2e6c94251631"
integrity sha512-KNTihKNmQENUZeKu5fzfpzRqR5S2VMp4gl9RFHiWzj9DfvYQPMJ6XHKNaQxaGCXwPk6y9yme3aUoaiAe+KX+vg==
depd@2.0.0:
depd@2.0.0, depd@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
@@ -7723,6 +7807,11 @@ detab@2.0.4:
dependencies:
repeat-string "^1.5.4"
detect-libc@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-2.0.1.tgz#e1897aa88fa6ad197862937fbc0441ef352ee0cd"
integrity sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==
detect-newline@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/detect-newline/-/detect-newline-3.1.0.tgz#576f5dfc63ae1a192ff192d8ad3af6308991b651"
@@ -8029,6 +8118,13 @@ encodeurl@~1.0.2:
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
encoding@^0.1.13:
version "0.1.13"
resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9"
integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==
dependencies:
iconv-lite "^0.6.2"
end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1:
version "1.4.4"
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
@@ -8084,6 +8180,11 @@ env-editor@^0.4.1:
resolved "https://registry.yarnpkg.com/env-editor/-/env-editor-0.4.2.tgz#4e76568d0bd8f5c2b6d314a9412c8fe9aa3ae861"
integrity sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==
env-paths@^2.2.0:
version "2.2.1"
resolved "https://registry.yarnpkg.com/env-paths/-/env-paths-2.2.1.tgz#420399d416ce1fbe9bc0a07c62fa68d67fd0f8f2"
integrity sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==
envinfo@^7.7.2:
version "7.8.1"
resolved "https://registry.yarnpkg.com/envinfo/-/envinfo-7.8.1.tgz#06377e3e5f4d379fea7ac592d5ad8927e0c4d475"
@@ -8094,6 +8195,11 @@ eol@^0.9.1:
resolved "https://registry.yarnpkg.com/eol/-/eol-0.9.1.tgz#f701912f504074be35c6117a5c4ade49cd547acd"
integrity sha512-Ds/TEoZjwggRoz/Q2O7SE3i4Jm66mqTDfmdHdq/7DKVk3bro9Q8h6WdXKdPqFLMoqxrDK5SVRzHVPOS6uuGtrg==
err-code@^2.0.2:
version "2.0.3"
resolved "https://registry.yarnpkg.com/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9"
integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==
errno@^0.1.3, errno@~0.1.7:
version "0.1.8"
resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.8.tgz#8bb3e9c7d463be4976ff888f76b4809ebc2e811f"
@@ -8422,6 +8528,11 @@ expand-brackets@^2.1.4:
snapdragon "^0.8.1"
to-regex "^3.0.1"
expand-template@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/expand-template/-/expand-template-2.0.3.tgz#6e14b3fcee0f3a6340ecb57d2e8918692052a47c"
integrity sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==
expect@^26.6.2:
version "26.6.2"
resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417"
@@ -9186,7 +9297,7 @@ fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0:
jsonfile "^6.0.1"
universalify "^2.0.0"
fs-minipass@^2.0.0:
fs-minipass@^2.0.0, fs-minipass@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb"
integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==
@@ -9261,6 +9372,20 @@ gauge@^3.0.0:
strip-ansi "^6.0.1"
wide-align "^1.1.2"
gauge@^4.0.3:
version "4.0.4"
resolved "https://registry.yarnpkg.com/gauge/-/gauge-4.0.4.tgz#52ff0652f2bbf607a989793d53b751bef2328dce"
integrity sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==
dependencies:
aproba "^1.0.3 || ^2.0.0"
color-support "^1.1.3"
console-control-strings "^1.1.0"
has-unicode "^2.0.1"
signal-exit "^3.0.7"
string-width "^4.2.3"
strip-ansi "^6.0.1"
wide-align "^1.1.5"
gensync@^1.0.0-beta.1, gensync@^1.0.0-beta.2:
version "1.0.0-beta.2"
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
@@ -9332,11 +9457,29 @@ getenv@^1.0.0:
resolved "https://registry.yarnpkg.com/getenv/-/getenv-1.0.0.tgz#874f2e7544fbca53c7a4738f37de8605c3fcfc31"
integrity sha512-7yetJWqbS9sbn0vIfliPsFgoXMKn/YMF+Wuiog97x+urnSRRRZ7xB+uVkwGKzRgq9CDFfMQnE9ruL5DHv9c6Xg==
github-from-package@0.0.0:
version "0.0.0"
resolved "https://registry.yarnpkg.com/github-from-package/-/github-from-package-0.0.0.tgz#97fb5d96bfde8973313f20e8288ef9a167fa64ce"
integrity sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==
github-slugger@^1.0.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/github-slugger/-/github-slugger-1.5.0.tgz#17891bbc73232051474d68bd867a34625c955f7d"
integrity sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==
gl@^5.0.0:
version "5.0.3"
resolved "https://registry.yarnpkg.com/gl/-/gl-5.0.3.tgz#a10f37c50e48954348cc3e790b83313049bdbe1c"
integrity sha512-toWmb3Rgli5Wl9ygjZeglFBVLDYMOomy+rXlVZVDCoIRV+6mQE5nY4NgQgokYIc5oQzc1pvWY9lQJ0hGn61ZUg==
dependencies:
bindings "^1.5.0"
bit-twiddle "^1.0.2"
glsl-tokenizer "^2.1.5"
nan "^2.16.0"
node-abi "^3.22.0"
node-gyp "^9.0.0"
prebuild-install "^7.1.1"
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@@ -9404,6 +9547,17 @@ glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6:
once "^1.3.0"
path-is-absolute "^1.0.0"
glob@^8.0.1:
version "8.1.0"
resolved "https://registry.yarnpkg.com/glob/-/glob-8.1.0.tgz#d388f656593ef708ee3e34640fdfb99a9fd1c33e"
integrity sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==
dependencies:
fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
minimatch "^5.0.1"
once "^1.3.0"
global-modules@2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/global-modules/-/global-modules-2.0.0.tgz#997605ad2345f27f51539bea26574421215c7780"
@@ -9489,6 +9643,13 @@ globby@^9.2.0:
pify "^4.0.1"
slash "^2.0.0"
glsl-tokenizer@^2.1.5:
version "2.1.5"
resolved "https://registry.yarnpkg.com/glsl-tokenizer/-/glsl-tokenizer-2.1.5.tgz#1c2e78c16589933c274ba278d0a63b370c5fee1a"
integrity sha512-XSZEJ/i4dmz3Pmbnpsy3cKh7cotvFlBiZnDOwnj/05EwNp2XrhQ4XKJxT7/pDt4kp4YcpRSKz8eTV7S+mwV6MA==
dependencies:
through2 "^0.6.3"
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
@@ -9518,6 +9679,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.3
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.10.tgz#147d3a006da4ca3ce14728c7aefc287c367d7a6c"
integrity sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==
graceful-fs@^4.2.6:
version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
grapheme-splitter@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e"
@@ -9944,6 +10110,11 @@ http-cache-semantics@^4.0.0:
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390"
integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ==
http-cache-semantics@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a"
integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==
http-deceiver@^1.2.7:
version "1.2.7"
resolved "https://registry.yarnpkg.com/http-deceiver/-/http-deceiver-1.2.7.tgz#fa7168944ab9a519d337cb0bec7284dc3e723d87"
@@ -9984,6 +10155,15 @@ http-proxy-agent@^4.0.1:
agent-base "6"
debug "4"
http-proxy-agent@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz#5129800203520d434f142bc78ff3c170800f2b43"
integrity sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==
dependencies:
"@tootallnate/once" "2"
agent-base "6"
debug "4"
http-proxy-middleware@0.19.1:
version "0.19.1"
resolved "https://registry.yarnpkg.com/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz#183c7dc4aa1479150306498c210cdaf96080a43a"
@@ -10034,6 +10214,13 @@ human-signals@^2.1.0:
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0"
integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==
humanize-ms@^1.2.1:
version "1.2.1"
resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed"
integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==
dependencies:
ms "^2.0.0"
hyphenate-style-name@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
@@ -10053,6 +10240,13 @@ iconv-lite@0.4.24:
dependencies:
safer-buffer ">= 2.1.2 < 3"
iconv-lite@^0.6.2:
version "0.6.3"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
dependencies:
safer-buffer ">= 2.1.2 < 3.0.0"
icss-utils@^4.0.0, icss-utils@^4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-4.1.1.tgz#21170b53789ee27447c2f47dd683081403f9a467"
@@ -10092,6 +10286,11 @@ image-size@^1.0.0:
dependencies:
queue "6.0.2"
immediate@~3.0.5:
version "3.0.6"
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
immer@8.0.1:
version "8.0.1"
resolved "https://registry.yarnpkg.com/immer/-/immer-8.0.1.tgz#9c73db683e2b3975c424fb0572af5889877ae656"
@@ -10547,6 +10746,11 @@ is-invalid-path@^0.1.0:
dependencies:
is-glob "^2.0.0"
is-lambda@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5"
integrity sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==
is-map@^2.0.1, is-map@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.2.tgz#00922db8c9bf73e81b7a335827bc2a43f2b91127"
@@ -11632,6 +11836,16 @@ jsonfile@^6.0.1:
optionalDependencies:
graceful-fs "^4.1.6"
jszip@3.10.1:
version "3.10.1"
resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
dependencies:
lie "~3.3.0"
pako "~1.0.2"
readable-stream "~2.3.6"
setimmediate "^1.0.5"
junk@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/junk/-/junk-3.1.0.tgz#31499098d902b7e98c5d9b9c80f43457a88abfa1"
@@ -11722,6 +11936,13 @@ levn@~0.3.0:
prelude-ls "~1.1.2"
type-check "~0.3.2"
lie@~3.3.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
dependencies:
immediate "~3.0.5"
lines-and-columns@^1.1.6:
version "1.2.4"
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
@@ -11981,7 +12202,7 @@ logkitty@^0.7.1:
dayjs "^1.8.15"
yargs "^15.1.0"
loglevel@^1.6.8:
loglevel@^1.6.8, loglevel@^1.8.0:
version "1.8.1"
resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.8.1.tgz#5c621f83d5b48c54ae93b6156353f555963377b4"
integrity sha512-tCRIJM51SHjAayKwC+QAg8hT8vg6z7GSgLJKGvzuPb1Wc+hLzqtuVLxp6/HzSPOozuK+8ErAhy7U/sVzw8Dgfg==
@@ -12027,6 +12248,11 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
lru-cache@^7.7.1:
version "7.18.3"
resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-7.18.3.tgz#f793896e0fd0e954a59dfdd82f0773808df6aa89"
integrity sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==
lz-string@^1.4.4:
version "1.4.4"
resolved "https://registry.yarnpkg.com/lz-string/-/lz-string-1.4.4.tgz#c0d8eaf36059f705796e1e344811cf4c498d3a26"
@@ -12047,6 +12273,28 @@ make-dir@^3.0.0, make-dir@^3.0.2, make-dir@^3.1.0:
dependencies:
semver "^6.0.0"
make-fetch-happen@^10.0.3:
version "10.2.1"
resolved "https://registry.yarnpkg.com/make-fetch-happen/-/make-fetch-happen-10.2.1.tgz#f5e3835c5e9817b617f2770870d9492d28678164"
integrity sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w==
dependencies:
agentkeepalive "^4.2.1"
cacache "^16.1.0"
http-cache-semantics "^4.1.0"
http-proxy-agent "^5.0.0"
https-proxy-agent "^5.0.0"
is-lambda "^1.0.1"
lru-cache "^7.7.1"
minipass "^3.1.6"
minipass-collect "^1.0.2"
minipass-fetch "^2.0.3"
minipass-flush "^1.0.5"
minipass-pipeline "^1.2.4"
negotiator "^0.6.3"
promise-retry "^2.0.1"
socks-proxy-agent "^7.0.0"
ssri "^9.0.0"
makeerror@1.0.12:
version "1.0.12"
resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a"
@@ -12676,11 +12924,23 @@ minimatch@3.0.4:
dependencies:
brace-expansion "^1.1.7"
minimatch@^5.0.1:
version "5.1.6"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96"
integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==
dependencies:
brace-expansion "^2.0.1"
minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.7"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.7.tgz#daa1c4d91f507390437c6a8bc01078e7000c4d18"
integrity sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==
minimist@^1.2.3:
version "1.2.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c"
integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==
minipass-collect@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/minipass-collect/-/minipass-collect-1.0.2.tgz#22b813bf745dc6edba2576b940022ad6edc8c617"
@@ -12688,6 +12948,17 @@ minipass-collect@^1.0.2:
dependencies:
minipass "^3.0.0"
minipass-fetch@^2.0.3:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minipass-fetch/-/minipass-fetch-2.1.2.tgz#95560b50c472d81a3bc76f20ede80eaed76d8add"
integrity sha512-LT49Zi2/WMROHYoqGgdlQIZh8mLPZmOrN2NdJjMXxYe4nkN6FUyuPuOAOedNJDrx0IRGg9+4guZewtp8hE6TxA==
dependencies:
minipass "^3.1.6"
minipass-sized "^1.0.3"
minizlib "^2.1.2"
optionalDependencies:
encoding "^0.1.13"
minipass-flush@^1.0.5:
version "1.0.5"
resolved "https://registry.yarnpkg.com/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373"
@@ -12695,13 +12966,20 @@ minipass-flush@^1.0.5:
dependencies:
minipass "^3.0.0"
minipass-pipeline@^1.2.2:
minipass-pipeline@^1.2.2, minipass-pipeline@^1.2.4:
version "1.2.4"
resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c"
integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==
dependencies:
minipass "^3.0.0"
minipass-sized@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70"
integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==
dependencies:
minipass "^3.0.0"
minipass@3.1.6:
version "3.1.6"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.6.tgz#3b8150aa688a711a1521af5e8779c1d3bb4f45ee"
@@ -12709,7 +12987,7 @@ minipass@3.1.6:
dependencies:
yallist "^4.0.0"
minipass@^3.0.0, minipass@^3.1.1:
minipass@^3.0.0, minipass@^3.1.1, minipass@^3.1.6:
version "3.3.6"
resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.3.6.tgz#7bba384db3a1520d18c9c0e5251c3444e95dd94a"
integrity sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==
@@ -12723,7 +13001,7 @@ minipass@^4.0.0:
dependencies:
yallist "^4.0.0"
minizlib@^2.1.1:
minizlib@^2.1.1, minizlib@^2.1.2:
version "2.1.2"
resolved "https://registry.yarnpkg.com/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931"
integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==
@@ -12755,6 +13033,11 @@ mixin-deep@^1.2.0:
for-in "^1.0.2"
is-extendable "^1.0.1"
mkdirp-classic@^0.5.2, mkdirp-classic@^0.5.3:
version "0.5.3"
resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113"
integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==
mkdirp@^0.5.1, mkdirp@^0.5.3, mkdirp@^0.5.6, mkdirp@~0.5.1:
version "0.5.6"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.6.tgz#7def03d2432dcae4ba1d611445c48396062255f6"
@@ -12801,7 +13084,7 @@ ms@2.1.2:
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
ms@2.1.3, ms@^2.1.1:
ms@2.1.3, ms@^2.0.0, ms@^2.1.1:
version "2.1.3"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
@@ -12837,7 +13120,7 @@ mz@^2.7.0:
object-assign "^4.0.1"
thenify-all "^1.0.0"
nan@^2.12.1:
nan@^2.12.1, nan@^2.16.0:
version "2.17.0"
resolved "https://registry.yarnpkg.com/nan/-/nan-2.17.0.tgz#c0150a2368a182f033e9aa5195ec76ea41a199cb"
integrity sha512-2ZTgtl0nJsO0KQCjEpxcIr5D+Yv90plTitZt9JBfQvVJDS5seMl3FOvsh3+9CoYWXf/1l5OaZzzF6nDm4cagaQ==
@@ -12876,6 +13159,11 @@ nanomatch@^1.2.9:
snapdragon "^0.8.1"
to-regex "^3.0.1"
napi-build-utils@^1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/napi-build-utils/-/napi-build-utils-1.0.2.tgz#b1fddc0b2c46e380a0b7a76f984dd47c41a13806"
integrity sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==
native-base@^3.4.17:
version "3.4.25"
resolved "https://registry.yarnpkg.com/native-base/-/native-base-3.4.25.tgz#0b5871855be4c48ef72768e50db002d6a0e1ad23"
@@ -12926,7 +13214,7 @@ ncp@~2.0.0:
resolved "https://registry.yarnpkg.com/ncp/-/ncp-2.0.0.tgz#195a21d6c46e361d2fb1281ba38b91e9df7bdbb3"
integrity sha512-zIdGUrPRFTUELUvr3Gmc7KZ2Sw/h1PiVM0Af/oHB6zgnV1ikqSfRk+TOufi79aHYCW3NiOXmr1BP5nWbzojLaA==
negotiator@0.6.3:
negotiator@0.6.3, negotiator@^0.6.3:
version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
@@ -12964,6 +13252,13 @@ nocache@^3.0.1:
resolved "https://registry.yarnpkg.com/nocache/-/nocache-3.0.4.tgz#5b37a56ec6e09fc7d401dceaed2eab40c8bfdf79"
integrity sha512-WDD0bdg9mbq6F4mRxEYcPWwfA1vxd0mrvKOyxI7Xj/atfRHVeutzuWByG//jfm4uPzp0y4Kj051EORCBSQMycw==
node-abi@^3.22.0, node-abi@^3.3.0:
version "3.40.0"
resolved "https://registry.yarnpkg.com/node-abi/-/node-abi-3.40.0.tgz#51d8ed44534f70ff1357dfbc3a89717b1ceac1b4"
integrity sha512-zNy02qivjjRosswoYmPi8hIKJRr8MpQyeKT6qlcq/OnOgA3Rhoae+IYOqsM9V5+JnHWmxKnWOT2GxvtqdtOCXA==
dependencies:
semver "^7.3.5"
node-dir@^0.1.10, node-dir@^0.1.17:
version "0.1.17"
resolved "https://registry.yarnpkg.com/node-dir/-/node-dir-0.1.17.tgz#5f5665d93351335caabef8f1c554516cf5f1e4e5"
@@ -12995,6 +13290,22 @@ node-forge@^1.2.1, node-forge@^1.3.1:
resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-1.3.1.tgz#be8da2af243b2417d5f646a770663a92b7e9ded3"
integrity sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==
node-gyp@^9.0.0:
version "9.3.1"
resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-9.3.1.tgz#1e19f5f290afcc9c46973d68700cbd21a96192e4"
integrity sha512-4Q16ZCqq3g8awk6UplT7AuxQ35XN4R/yf/+wSAwcBUAjg7l58RTactWaP8fIDTi0FzI7YcVLujwExakZlfWkXg==
dependencies:
env-paths "^2.2.0"
glob "^7.1.4"
graceful-fs "^4.2.6"
make-fetch-happen "^10.0.3"
nopt "^6.0.0"
npmlog "^6.0.0"
rimraf "^3.0.2"
semver "^7.3.5"
tar "^6.1.2"
which "^2.0.2"
node-html-parser@^1.2.12:
version "1.4.9"
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-1.4.9.tgz#3c8f6cac46479fae5800725edb532e9ae8fd816c"
@@ -13071,6 +13382,13 @@ node.extend@^2.0.0:
has "^1.0.3"
is "^3.2.1"
nopt@^6.0.0:
version "6.0.0"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-6.0.0.tgz#245801d8ebf409c6df22ab9d95b65e1309cdb16d"
integrity sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==
dependencies:
abbrev "^1.0.0"
normalize-css-color@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/normalize-css-color/-/normalize-css-color-1.0.2.tgz#02991e97cccec6623fe573afbbf0de6a1f3e9f8d"
@@ -13157,6 +13475,16 @@ npmlog@^5.0.1:
gauge "^3.0.0"
set-blocking "^2.0.0"
npmlog@^6.0.0:
version "6.0.2"
resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-6.0.2.tgz#c8166017a42f2dea92d6453168dd865186a70830"
integrity sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==
dependencies:
are-we-there-yet "^3.0.0"
console-control-strings "^1.1.0"
gauge "^4.0.3"
set-blocking "^2.0.0"
nth-check@^1.0.2:
version "1.0.2"
resolved "https://registry.yarnpkg.com/nth-check/-/nth-check-1.0.2.tgz#b2bd295c37e3dd58a3bf0700376663ba4d9cf05c"
@@ -13375,6 +13703,19 @@ open@^8.0.4, open@^8.3.0, open@^8.4.0:
is-docker "^2.1.1"
is-wsl "^2.2.0"
opensheetmusicdisplay@^1.7.5:
version "1.7.5"
resolved "https://registry.yarnpkg.com/opensheetmusicdisplay/-/opensheetmusicdisplay-1.7.5.tgz#9324a3be2527e584c615626ab0d8913017924ffe"
integrity sha512-DHFWwlbfKYoBSJpf8xfp42yV9G9xBPOqUz0TTq9FmGpONtQBbuqMGpbxP57MBFb6VyKzN+Zbug4L7IYNkRDBkg==
dependencies:
"@types/vexflow" "^1.2.38"
jszip "3.10.1"
loglevel "^1.8.0"
typescript-collections "^1.3.3"
vexflow "1.2.93"
optionalDependencies:
gl "^5.0.0"
opn@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"
@@ -13566,7 +13907,7 @@ packageurl-js@^1.0.0:
resolved "https://registry.yarnpkg.com/packageurl-js/-/packageurl-js-1.0.0.tgz#188ed35688d44a0684476e7af5b6c6835c3c5533"
integrity sha512-06kNFU+yB2pjDf5JyXouQeKfwSScGP8hrZK6VgB+W4SlVy4y5yB4vl+AVmh3R0GBNd+fBt0dEiSx3HKmuchuJQ==
pako@~1.0.5:
pako@~1.0.2, pako@~1.0.5:
version "1.0.11"
resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
@@ -14269,6 +14610,24 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.26, postcss@^7.0.2
picocolors "^0.2.1"
source-map "^0.6.1"
prebuild-install@^7.1.1:
version "7.1.1"
resolved "https://registry.yarnpkg.com/prebuild-install/-/prebuild-install-7.1.1.tgz#de97d5b34a70a0c81334fd24641f2a1702352e45"
integrity sha512-jAXscXWMcCK8GgCoHOfIr0ODh5ai8mj63L2nWrjuAgXE6tDyYGnx4/8o/rCgU+B4JSyZBKbeZqzhtwtC3ovxjw==
dependencies:
detect-libc "^2.0.0"
expand-template "^2.0.3"
github-from-package "0.0.0"
minimist "^1.2.3"
mkdirp-classic "^0.5.3"
napi-build-utils "^1.0.1"
node-abi "^3.3.0"
pump "^3.0.0"
rc "^1.2.7"
simple-get "^4.0.0"
tar-fs "^2.0.0"
tunnel-agent "^0.6.0"
prelude-ls@~1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54"
@@ -14362,6 +14721,14 @@ promise-inflight@^1.0.1:
resolved "https://registry.yarnpkg.com/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3"
integrity sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==
promise-retry@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22"
integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==
dependencies:
err-code "^2.0.2"
retry "^0.12.0"
promise.allsettled@^1.0.0:
version "1.0.6"
resolved "https://registry.yarnpkg.com/promise.allsettled/-/promise.allsettled-1.0.6.tgz#8dc8ba8edf429feb60f8e81335b920e109c94b6e"
@@ -14624,7 +14991,7 @@ raw-loader@^4.0.2:
loader-utils "^2.0.0"
schema-utils "^3.0.0"
rc@^1.0.1, rc@^1.1.6, rc@~1.2.7:
rc@^1.0.1, rc@^1.1.6, rc@^1.2.7, rc@~1.2.7:
version "1.2.8"
resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.8.tgz#cd924bf5200a075b83c188cd6b9e211b7fc0d3ed"
integrity sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==
@@ -14939,6 +15306,11 @@ react-shallow-renderer@^16.13.1, react-shallow-renderer@^16.15.0:
object-assign "^4.1.1"
react-is "^16.12.0 || ^17.0.0 || ^18.0.0"
react-sub-unsub@^2.1.6:
version "2.1.11"
resolved "https://registry.yarnpkg.com/react-sub-unsub/-/react-sub-unsub-2.1.11.tgz#173d0803e1d7b29611cb29d95f47ed7798e93642"
integrity sha512-FNKy0uD5wSieRE+l5RXaS0bUu6cR8XAXLDwOJnvSDGBMHcWVb1dod8ZkXYjPKtKR74tjYCEpMWcEAWCOoWNXxQ==
react-test-renderer@17.0.2, react-test-renderer@~17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c"
@@ -14954,6 +15326,13 @@ react-timer-hook@^3.0.5:
resolved "https://registry.yarnpkg.com/react-timer-hook/-/react-timer-hook-3.0.5.tgz#a8d930f99b180cd88da245965a26a17df3e7457b"
integrity sha512-n+98SdmYvui2ne3KyWb3Ldu4k0NYQa3g/VzW6VEIfZJ8GAk/jJsIY700M8Nd2vNSTj05c7wKyQfJBqZ0x7zfiA==
react-use-precision-timer@^3.3.1:
version "3.3.1"
resolved "https://registry.yarnpkg.com/react-use-precision-timer/-/react-use-precision-timer-3.3.1.tgz#8e49b6f58d507647925bf633a673a7cb0b2b924d"
integrity sha512-PUCpFp48ftKoV2C+hz57mbqzqojE/Ol169Lyk2fFEIapsOH6tKIis8vZwmloedRe916qmJCOkXp+h9IB6QJY+A==
dependencies:
react-sub-unsub "^2.1.6"
react@18.1.0:
version "18.1.0"
resolved "https://registry.yarnpkg.com/react/-/react-18.1.0.tgz#6f8620382decb17fdc5cc223a115e2adbf104890"
@@ -15010,6 +15389,16 @@ read-pkg@^5.2.0:
string_decoder "~1.1.1"
util-deprecate "~1.0.1"
"readable-stream@>=1.0.33-1 <1.1.0-0":
version "1.0.34"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c"
integrity sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg==
dependencies:
core-util-is "~1.0.0"
inherits "~2.0.1"
isarray "0.0.1"
string_decoder "~0.10.x"
readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0:
version "3.6.0"
resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198"
@@ -15511,7 +15900,7 @@ safe-regex@^1.1.0:
dependencies:
ret "~0.1.10"
"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0:
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0:
version "2.1.2"
resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@@ -15816,11 +16205,25 @@ side-channel@^1.0.4:
get-intrinsic "^1.0.2"
object-inspect "^1.9.0"
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3:
signal-exit@^3.0.0, signal-exit@^3.0.2, signal-exit@^3.0.3, signal-exit@^3.0.7:
version "3.0.7"
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9"
integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==
simple-concat@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/simple-concat/-/simple-concat-1.0.1.tgz#f46976082ba35c2263f1c8ab5edfe26c41c9552f"
integrity sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==
simple-get@^4.0.0:
version "4.0.1"
resolved "https://registry.yarnpkg.com/simple-get/-/simple-get-4.0.1.tgz#4a39db549287c979d352112fa03fd99fd6bc3543"
integrity sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==
dependencies:
decompress-response "^6.0.0"
once "^1.3.1"
simple-concat "^1.0.0"
simple-plist@^1.1.0:
version "1.3.1"
resolved "https://registry.yarnpkg.com/simple-plist/-/simple-plist-1.3.1.tgz#16e1d8f62c6c9b691b8383127663d834112fb017"
@@ -15866,6 +16269,11 @@ slugify@^1.3.4:
resolved "https://registry.yarnpkg.com/slugify/-/slugify-1.6.5.tgz#c8f5c072bf2135b80703589b39a3d41451fbe8c8"
integrity sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ==
smart-buffer@^4.2.0:
version "4.2.0"
resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae"
integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==
snapdragon-node@^2.0.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/snapdragon-node/-/snapdragon-node-2.1.1.tgz#6c175f86ff14bdb0724563e8f3c1b021a286853b"
@@ -15947,6 +16355,23 @@ sockjs@0.3.20:
uuid "^3.4.0"
websocket-driver "0.6.5"
socks-proxy-agent@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz#dc069ecf34436621acb41e3efa66ca1b5fed15b6"
integrity sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==
dependencies:
agent-base "^6.0.2"
debug "^4.3.3"
socks "^2.6.2"
socks@^2.6.2:
version "2.7.1"
resolved "https://registry.yarnpkg.com/socks/-/socks-2.7.1.tgz#d8e651247178fde79c0663043e07240196857d55"
integrity sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==
dependencies:
ip "^2.0.0"
smart-buffer "^4.2.0"
soundfont-player@^0.12.0:
version "0.12.0"
resolved "https://registry.yarnpkg.com/soundfont-player/-/soundfont-player-0.12.0.tgz#2b26149f28aba471d2285d3df9a2e1e5793ceaf1"
@@ -16092,6 +16517,13 @@ ssri@^8.0.1:
dependencies:
minipass "^3.1.1"
ssri@^9.0.0:
version "9.0.1"
resolved "https://registry.yarnpkg.com/ssri/-/ssri-9.0.1.tgz#544d4c357a8d7b71a19700074b6883fcb4eae057"
integrity sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==
dependencies:
minipass "^3.1.1"
stable-hash@^0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/stable-hash/-/stable-hash-0.0.2.tgz#a909deaa5b9d430b100ca0a10132a533f2665e94"
@@ -16307,6 +16739,11 @@ string_decoder@^1.0.0, string_decoder@^1.1.1:
dependencies:
safe-buffer "~5.2.0"
string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==
string_decoder@~1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
@@ -16550,7 +16987,17 @@ tapable@^2.1.1, tapable@^2.2.0:
resolved "https://registry.yarnpkg.com/tapable/-/tapable-2.2.1.tgz#1967a73ef4060a82f12ab96af86d52fdb76eeca0"
integrity sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==
tar-stream@^2.0.1:
tar-fs@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/tar-fs/-/tar-fs-2.1.1.tgz#489a15ab85f1f0befabb370b7de4f9eb5cbe8784"
integrity sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==
dependencies:
chownr "^1.1.1"
mkdirp-classic "^0.5.2"
pump "^3.0.0"
tar-stream "^2.1.4"
tar-stream@^2.0.1, tar-stream@^2.1.4:
version "2.2.0"
resolved "https://registry.yarnpkg.com/tar-stream/-/tar-stream-2.2.0.tgz#acad84c284136b060dc3faa64474aa9aebd77287"
integrity sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==
@@ -16561,7 +17008,7 @@ tar-stream@^2.0.1:
inherits "^2.0.3"
readable-stream "^3.1.1"
tar@^6.0.2, tar@^6.0.5:
tar@^6.0.2, tar@^6.0.5, tar@^6.1.11, tar@^6.1.2:
version "6.1.13"
resolved "https://registry.yarnpkg.com/tar/-/tar-6.1.13.tgz#46e22529000f612180601a6fe0680e7da508847b"
integrity sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==
@@ -16748,6 +17195,14 @@ throat@^5.0.0:
resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b"
integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==
through2@^0.6.3:
version "0.6.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-0.6.5.tgz#41ab9c67b29d57209071410e1d7a7a968cd3ad48"
integrity sha512-RkK/CCESdTKQZHdmKICijdKKsCRVHs5KsLZ6pACAmF/1GPUQhonHSXWNERctxEp7RmvjdNbZTL5z9V7nSCXKcg==
dependencies:
readable-stream ">=1.0.33-1 <1.1.0-0"
xtend ">=4.0.0 <4.1.0-0"
through2@^2.0.0, through2@^2.0.1:
version "2.0.5"
resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd"
@@ -16929,6 +17384,13 @@ tty-browserify@0.0.0:
resolved "https://registry.yarnpkg.com/tty-browserify/-/tty-browserify-0.0.0.tgz#a157ba402da24e9bf957f9aa69d524eed42901a6"
integrity sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==
tunnel-agent@^0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.6.0.tgz#27a5dea06b36b04a0a9966774b290868f0fc40fd"
integrity sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==
dependencies:
safe-buffer "^5.0.1"
tunnel@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c"
@@ -17020,6 +17482,11 @@ typedarray@^0.0.6:
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==
typescript-collections@^1.3.3:
version "1.3.3"
resolved "https://registry.yarnpkg.com/typescript-collections/-/typescript-collections-1.3.3.tgz#62d50d93c018c094d425eabee649f00ec5cc0fea"
integrity sha512-7sI4e/bZijOzyURng88oOFZCISQPTHozfE2sUu5AviFYk5QV7fYGb6YiDl+vKjF/pICA354JImBImL9XJWUvdQ==
typescript@^4.6.3:
version "4.9.4"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.4.tgz#a2a3d2756c079abda241d75f149df9d561091e78"
@@ -17128,6 +17595,13 @@ unique-filename@^1.1.1:
dependencies:
unique-slug "^2.0.0"
unique-filename@^2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/unique-filename/-/unique-filename-2.0.1.tgz#e785f8675a9a7589e0ac77e0b5c34d2eaeac6da2"
integrity sha512-ODWHtkkdx3IAR+veKxFV+VBkUMcN+FaqzUUd7IZzt+0zhDZFPFxhlqwPF3YQvMHx1TD0tdgYl+kuPnJ8E6ql7A==
dependencies:
unique-slug "^3.0.0"
unique-slug@^2.0.0:
version "2.0.2"
resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-2.0.2.tgz#baabce91083fc64e945b0f3ad613e264f7cd4e6c"
@@ -17135,6 +17609,13 @@ unique-slug@^2.0.0:
dependencies:
imurmurhash "^0.1.4"
unique-slug@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/unique-slug/-/unique-slug-3.0.0.tgz#6d347cf57c8a7a7a6044aabd0e2d74e4d76dc7c9"
integrity sha512-8EyMynh679x/0gqE9fT9oilG+qEt+ibFyqjuVTsZn1+CMxH+XLlpvr2UZx4nVcCwTpx81nICr2JQFkM+HPLq4w==
dependencies:
imurmurhash "^0.1.4"
unique-string@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-1.0.0.tgz#9e1057cca851abb93398f8b33ae187b99caec11a"
@@ -17460,6 +17941,11 @@ vendors@^1.0.0:
resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e"
integrity sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==
vexflow@1.2.93:
version "1.2.93"
resolved "https://registry.yarnpkg.com/vexflow/-/vexflow-1.2.93.tgz#d6796d7a3fdae1bb06efb32a1390899931dd636f"
integrity sha512-LwHQDCc257Lwju35BhyZuPYcVWu0hIUqEdM7j9+B+bq91bSelssnAG5JR8odTUtgGuwwvGwLhXw37wtmHNCS6Q==
vfile-location@^3.0.0, vfile-location@^3.2.0:
version "3.2.0"
resolved "https://registry.yarnpkg.com/vfile-location/-/vfile-location-3.2.0.tgz#d8e41fbcbd406063669ebf6c33d56ae8721d0f3c"
@@ -17881,7 +18367,7 @@ which@^2.0.1, which@^2.0.2:
dependencies:
isexe "^2.0.0"
wide-align@^1.1.2:
wide-align@^1.1.2, wide-align@^1.1.5:
version "1.1.5"
resolved "https://registry.yarnpkg.com/wide-align/-/wide-align-1.1.5.tgz#df1d4c206854369ecf3c9a4898f1b23fbd9d15d3"
integrity sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==
@@ -18052,7 +18538,7 @@ xmlchars@^2.2.0:
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
"xtend@>=4.0.0 <4.1.0-0", xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.1:
version "4.0.2"
resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54"
integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==

16
grafana/datasource.yml Normal file
View File

@@ -0,0 +1,16 @@
# config file version
apiVersion: 1
deleteDatasources:
- name: loki
datasources:
- name: loki
type: loki
access: proxy
orgId: 1
url: http://loki:3100
basicAuth: false
isDefault: true
version: 1
editable: false

View File

@@ -0,0 +1,43 @@
version: "3.4"
services:
my-nginx-service:
image: nginx
container_name: my-nginx-service
ports:
- 8000:80
environment:
- FOO=bar
logging:
driver: loki
options:
loki-url: http://loki:3100/loki/api/v1/push
loki-external-labels: job=dockerlogs,environment=development
grafana:
image: grafana/grafana:7.2.2
container_name: grafana
volumes:
- ./datasource.yml:/etc/grafana/provisioning/datasources/datasource.yml
ports:
- "3000:3000"
loki:
image: grafana/loki:2.8.2
container_name: loki
volumes:
- ./loki.yaml:/etc/config/loki.yaml
entrypoint:
- /usr/bin/loki
- -config.file=/etc/config/loki.yaml
ports:
- "3100:3100"
logger-app:
image: mingrammer/flog
container_name: logger
logging:
driver: loki
options:
loki-url: http://loki:3100/loki/api/v1/push
loki-external-labels: job=dockerlogs,environment=development

54
grafana/loki.yaml Normal file
View File

@@ -0,0 +1,54 @@
auth_enabled: false
server:
http_listen_port: 3100
ingester:
lifecycler:
address: 127.0.0.1
ring:
kvstore:
store: inmemory
replication_factor: 1
final_sleep: 0s
chunk_idle_period: 5m
chunk_retain_period: 30s
schema_config:
configs:
- from: 2018-04-15
store: boltdb
object_store: filesystem
schema: v9
index:
prefix: index_
period: 168h
storage_config:
boltdb:
directory: /tmp/loki/index
filesystem:
directory: /tmp/loki/chunks
limits_config:
enforce_metric_name: false
reject_old_samples: true
reject_old_samples_max_age: 168h
chunk_store_config:
max_look_back_period: 0
table_manager:
chunk_tables_provisioning:
inactive_read_throughput: 0
inactive_write_throughput: 0
provisioned_read_throughput: 0
provisioned_write_throughput: 0
index_tables_provisioning:
inactive_read_throughput: 0
inactive_write_throughput: 0
provisioned_read_throughput: 0
provisioned_write_throughput: 0
retention_deletes_enabled: false
retention_period: 0

View File

@@ -0,0 +1,21 @@
[Metadata]
Name=Symphony No 9 in D Minor
Artist=Beethoven
Genre=Classical
Album=Symphony No 9
[Difficulties]
TwoHands=0
Rhythm=4
NoteCombo=0
Arpeggio=6
Distance=0
LeftHand=2
RightHand=1
LeadHandChange=0
ChordComplexity=0
ChordTiming=0
Length=1
PedalPoint=0
Precision=10

Binary file not shown.

Binary file not shown.

Binary file not shown.

21
musics/Short/Short.ini Normal file
View File

@@ -0,0 +1,21 @@
[Metadata]
Name=Short
Artist=Test
Genre=Abstract
Album=Trololol
[Difficulties]
TwoHands=0
Rhythm=0
NoteCombo=0
Arpeggio=0
Distance=0
LeftHand=0
RightHand=0
LeadHandChange=0
ChordComplexity=0
ChordTiming=0
Length=0
PedalPoint=0
Precision=0

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