Compare commits
233 Commits
google-aut
...
feat/grafa
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d1b273335 | |||
| 2c68489c2f | |||
| b2ea764b96 | |||
|
|
45c07b68a0 | ||
|
|
5bc4289bdc | ||
|
|
9f17cd9f83 | ||
|
|
c82cdc0445 | ||
|
|
4d77007010 | ||
|
|
2ece5b44ac | ||
|
|
1fa3d77e8d | ||
|
|
7166bb46ed | ||
|
|
d0166c6b27 | ||
|
|
393ff3c2c7 | ||
|
|
ae4d1f00d9 | ||
|
|
8a3d4f9c25 | ||
|
|
4034d29056 | ||
|
|
cce560031a | ||
|
|
f6226ce127 | ||
|
|
20deb7ae99 | ||
|
|
3c3ed74995 | ||
|
|
c3e8fb1c45 | ||
|
|
97be3b9c76 | ||
|
|
5662082048 | ||
|
|
9dc2389c35 | ||
|
|
10f80b6191 | ||
|
|
b943b9a621 | ||
|
|
fb9467e58e | ||
|
|
bf4b84e1f8 | ||
|
|
03ac681dd6 | ||
|
|
9d60993f8d | ||
|
|
10e53abfc1 | ||
|
|
18cc79f4a2 | ||
|
|
5c8e35ba7d | ||
|
|
2db657dd59 | ||
|
|
cbe8d291dd | ||
|
|
e0bdd5fd8f | ||
|
|
030cbfc786 | ||
|
|
d0597f0e95 | ||
|
|
f640b0d6f8 | ||
|
|
c005ebbdc9 | ||
|
|
cf6b61e0e9 | ||
|
|
9a7c4405bb | ||
|
|
7716c5f9c6 | ||
|
|
42ab0f6ed6 | ||
|
|
8628e07be1 | ||
|
|
fa60fca466 | ||
|
|
a51aa60e20 | ||
|
|
4b44ef0c11 | ||
|
|
bb96d57f27 | ||
|
|
8ccc90eceb | ||
|
|
467f440c54 | ||
|
|
88cfd1ecde | ||
|
|
deaaaac2cd | ||
|
|
6871aaf759 | ||
|
|
31d3909e80 | ||
|
|
ae36edfff4 | ||
|
|
fe782a4f94 | ||
|
|
9d8bb499ba | ||
|
|
968ae149a5 | ||
|
|
50f6fe6851 | ||
|
|
eddbe6e2be | ||
|
|
c4bc7c795c | ||
|
|
509d079bce | ||
|
|
af3da974bf | ||
|
|
da0b43c348 | ||
|
|
1c76266444 | ||
|
|
9033fbe937 | ||
|
|
34d646021f | ||
|
|
3aa104a923 | ||
|
|
047cd054bd | ||
|
|
b0dddbe815 | ||
|
|
70b506c6c2 | ||
|
|
8ce1beb518 | ||
|
|
4ec8878e8a | ||
|
|
d9ede44d7d | ||
|
|
0cd8846e2c | ||
|
|
1b63d27f74 | ||
|
|
8cde4747a7 | ||
|
|
5a5654d4f5 | ||
|
|
9c8395b578 | ||
|
|
0ce17054fc | ||
|
|
87fecb7522 | ||
|
|
9d5060fc31 | ||
|
|
2f19c0e547 | ||
|
|
c5ba72229f | ||
|
|
067f5e711d | ||
|
|
f725a89c0c | ||
|
|
69fbbd5e00 | ||
|
|
a1a2b77a16 | ||
|
|
deda1f738b | ||
|
|
cf1e98f9e6 | ||
|
|
124f87c199 | ||
|
|
572bb0056d | ||
|
|
fbf85a635e | ||
|
|
92e439892d | ||
|
|
93a2141c7c | ||
|
|
dc9f74c047 | ||
|
|
950e4c7767 | ||
|
|
c9c95be60f | ||
|
|
d931d00187 | ||
|
|
a0040c26ca | ||
|
|
728bb3d6a2 | ||
|
|
84f91e0d7f | ||
|
|
5a42f098d6 | ||
|
|
0922e6038b | ||
|
|
c45f425a5d | ||
|
|
2accb7dd72 | ||
|
|
db5e62c6ab | ||
|
|
b0e01ffbed | ||
|
|
9cd6c90188 | ||
|
|
e108bf2c66 | ||
|
|
b43979dd58 | ||
|
|
7b20792a51 | ||
|
|
4f9a3a9333 | ||
|
|
a26efefd01 | ||
|
|
ac4d4f6f66 | ||
|
|
2764805c04 | ||
| e43a8fd111 | |||
|
|
29414b5392 | ||
|
|
ef5a74da3b | ||
|
|
39bb7ced04 | ||
|
|
6cc7090360 | ||
|
|
9dfc2881a2 | ||
|
|
4e26925113 | ||
|
|
46f4ac82a8 | ||
|
|
77a230c944 | ||
|
|
fd8b4c59de | ||
|
|
e4d998b0ff | ||
|
|
7722eba86f | ||
|
|
5ada22d267 | ||
|
|
8d665175fd | ||
|
|
8eb524cc81 | ||
|
|
8728707b28 | ||
|
|
1e667813ad | ||
|
|
aa8782a5de | ||
|
|
ac4012087c | ||
|
|
6cca70a290 | ||
|
|
0fd64bfba0 | ||
|
|
1fa43555df | ||
|
|
47629e3938 | ||
|
|
8abf3e339a | ||
|
|
9297a28d7a | ||
|
|
44411454b2 | ||
|
|
88dea2784c | ||
|
|
6e6dff526b | ||
|
|
185f415e8d | ||
|
|
ec4ee5b94a | ||
|
|
0e2d2cf51c | ||
|
|
fb5e313f6f | ||
|
|
f1f7500b44 | ||
|
|
08494936af | ||
|
|
e3ba076870 | ||
|
|
5ac118efbd | ||
|
|
7882deab0b | ||
|
|
58ac90d68d | ||
|
|
9bb5139f76 | ||
|
|
31771f18ff | ||
|
|
a3191eda3c | ||
|
|
5f34fc4310 | ||
|
|
86337d4525 | ||
|
|
a4299aadb9 | ||
|
|
aa0c7d9621 | ||
|
|
73695e2580 | ||
|
|
e8f1a34372 | ||
|
|
a7dc6a76e9 | ||
|
|
d051d36406 | ||
|
|
31e46904a8 | ||
|
|
e79fad1208 | ||
|
|
faf12839bc | ||
|
|
bebea61036 | ||
|
|
38aa680b82 | ||
|
|
9910f51c2a | ||
|
|
9d74673cff | ||
|
|
b9513ad154 | ||
|
|
7cb01a3cba | ||
|
|
3a32fcf559 | ||
|
|
445b949fa8 | ||
|
|
437d5c7b5c | ||
|
|
e7b9accb50 | ||
|
|
09fd62706b | ||
|
|
b2e11b013c | ||
|
|
057726617b | ||
|
|
b333ec676a | ||
|
|
9db8e84086 | ||
|
|
8a741b920b | ||
|
|
702d9bcaef | ||
|
|
165ef44c77 | ||
|
|
9edccf1fb4 | ||
|
|
12120fb25a | ||
|
|
f60172b160 | ||
|
|
3da5d927cf | ||
|
|
1b947a580a | ||
|
|
34c7205cfe | ||
|
|
c1f8ab51b0 | ||
|
|
da9570da65 | ||
|
|
dad54f81f2 | ||
|
|
db926a2747 | ||
|
|
7aec52ee43 | ||
|
|
71f7dae657 | ||
|
|
f8bb6ed1c0 | ||
|
|
81ac9b91ef | ||
|
|
870489a220 | ||
|
|
b8811a7ff7 | ||
|
|
8784e8de3c | ||
|
|
e9f6adab63 | ||
|
|
b6feab715b | ||
|
|
ed8be27b11 | ||
|
|
64b1355712 | ||
|
|
c29740dc2e | ||
|
|
d094c81418 | ||
|
|
885c819ab5 | ||
|
|
c3d2e0a4e5 | ||
|
|
aa72f34a6c | ||
|
|
efede253dc | ||
|
|
a9cd0f16ae | ||
|
|
6a10ad2398 | ||
|
|
f43561460d | ||
|
|
eb100e843b | ||
|
|
cc364cfe7a | ||
|
|
319295d2e5 | ||
|
|
7bf8f32805 | ||
|
|
7e463662be | ||
| f788872f9b | |||
| a9574cb75a | |||
| f24e43a392 | |||
| a0bf718e1d | |||
|
|
cf3c9b8c86 | ||
|
|
89d39812a6 | ||
|
|
b5584a12d0 | ||
|
|
3ca2bdaa90 | ||
|
|
d5b15cee13 | ||
|
|
8a332ede38 | ||
|
|
2bed2e1c64 |
17
.env.example
17
.env.example
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"hostType": "lan",
|
||||
"lanType": "ip",
|
||||
"dev": true,
|
||||
"minify": false,
|
||||
"urlRandomness": null,
|
||||
"https": false
|
||||
}
|
||||
9
.github/workflows/CI.yml
vendored
9
.github/workflows/CI.yml
vendored
@@ -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
4
.gitignore
vendored
@@ -11,4 +11,6 @@ report.html
|
||||
log.html
|
||||
.expo
|
||||
node_modules/
|
||||
./front/coverage
|
||||
./front/coverage
|
||||
.venv
|
||||
grafana/.data
|
||||
|
||||
20
back/prisma/migrations/20230227220027_/migration.sql
Normal file
20
back/prisma/migrations/20230227220027_/migration.sql
Normal 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;
|
||||
2
back/prisma/migrations/20230323054114_/migration.sql
Normal file
2
back/prisma/migrations/20230323054114_/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "isGuest" BOOLEAN NOT NULL DEFAULT false;
|
||||
2
back/prisma/migrations/20230323061423_/migration.sql
Normal file
2
back/prisma/migrations/20230323061423_/migration.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "partyPlayed" INTEGER NOT NULL DEFAULT 0;
|
||||
25
back/prisma/migrations/20230328185436_/migration.sql
Normal file
25
back/prisma/migrations/20230328185436_/migration.sql
Normal 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;
|
||||
16
back/prisma/migrations/20230405061222_/migration.sql
Normal file
16
back/prisma/migrations/20230405061222_/migration.sql
Normal 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");
|
||||
@@ -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 {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
|
||||
16
back/src/auth/dto/profile.dto.ts
Normal file
16
back/src/auth/dto/profile.dto.ts
Normal 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;
|
||||
}
|
||||
@@ -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,
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
24
back/src/models/setting.ts
Normal file
24
back/src/models/setting.ts
Normal 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;
|
||||
}
|
||||
@@ -6,7 +6,9 @@ export class User {
|
||||
@ApiProperty()
|
||||
username: string;
|
||||
@ApiProperty()
|
||||
password: string;
|
||||
@ApiProperty()
|
||||
email: string;
|
||||
@ApiProperty()
|
||||
isGuest: boolean;
|
||||
@ApiProperty()
|
||||
partyPlayed: number;
|
||||
}
|
||||
|
||||
20
back/src/settings/dto/update-setting.dto.ts
Normal file
20
back/src/settings/dto/update-setting.dto.ts
Normal 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;
|
||||
}
|
||||
18
back/src/settings/settings.controller.spec.ts
Normal file
18
back/src/settings/settings.controller.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
10
back/src/settings/settings.module.ts
Normal file
10
back/src/settings/settings.module.ts
Normal 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 {}
|
||||
18
back/src/settings/settings.service.spec.ts
Normal file
18
back/src/settings/settings.service.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
45
back/src/settings/settings.service.ts
Normal file
45
back/src/settings/settings.service.ts
Normal 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,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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,
|
||||
|
||||
75
back/test/robot/auth/guest.robot
Normal file
75
back/test/robot/auth/guest.robot
Normal 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
|
||||
@@ -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
|
||||
|
||||
27
back/test/robot/settings/settings.robot
Normal file
27
back/test/robot/settings/settings.robot
Normal 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}
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
214
front/API.ts
214
front/API.ts
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;'
|
||||
|
||||
@@ -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>>();
|
||||
149
front/components/BigActionButton.tsx
Normal file
149
front/components/BigActionButton.tsx
Normal 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;
|
||||
@@ -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}>
|
||||
|
||||
@@ -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';
|
||||
|
||||
30
front/components/GtkUI/Element.tsx
Normal file
30
front/components/GtkUI/Element.tsx
Normal 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} />;
|
||||
};
|
||||
63
front/components/GtkUI/ElementList.tsx
Normal file
63
front/components/GtkUI/ElementList.tsx
Normal 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;
|
||||
137
front/components/GtkUI/ElementTypes.tsx
Normal file
137
front/components/GtkUI/ElementTypes.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
154
front/components/GtkUI/RawElement.tsx
Normal file
154
front/components/GtkUI/RawElement.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
12
front/components/IconButton.tsx
Normal file
12
front/components/IconButton.tsx
Normal 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;
|
||||
@@ -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 }
|
||||
|
||||
136
front/components/PartitionView.tsx
Normal file
136
front/components/PartitionView.tsx
Normal 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;
|
||||
@@ -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 (
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
163
front/components/VirtualPiano/Octave.tsx
Normal file
163
front/components/VirtualPiano/Octave.tsx
Normal 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;
|
||||
102
front/components/VirtualPiano/PianoKeyComp.tsx
Normal file
102
front/components/VirtualPiano/PianoKeyComp.tsx
Normal 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;
|
||||
96
front/components/VirtualPiano/VirtualPiano.tsx
Normal file
96
front/components/VirtualPiano/VirtualPiano.tsx
Normal 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;
|
||||
118
front/components/forms/changeEmailForm.tsx
Normal file
118
front/components/forms/changeEmailForm.tsx
Normal 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;
|
||||
157
front/components/forms/changePasswordForm.tsx
Normal file
157
front/components/forms/changePasswordForm.tsx
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
225
front/components/navigators/TabRowNavigator.tsx
Normal file
225
front/components/navigators/TabRowNavigator.tsx
Normal 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);
|
||||
@@ -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') {
|
||||
|
||||
13
front/hooks/userSettings.ts
Normal file
13
front/hooks/userSettings.ts
Normal 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;
|
||||
@@ -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',
|
||||
};
|
||||
10
front/models/LocalSettings.ts
Normal file
10
front/models/LocalSettings.ts
Normal 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
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
interface Metrics {
|
||||
|
||||
}
|
||||
|
||||
export default Metrics;
|
||||
170
front/models/Piano.ts
Normal file
170
front/models/Piano.ts
Normal 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),
|
||||
];
|
||||
7
front/models/SearchHistory.ts
Normal file
7
front/models/SearchHistory.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
interface SearchHistory {
|
||||
query: string;
|
||||
userID: number;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export default SearchHistory;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -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
8
front/models/UserData.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
interface UserData {
|
||||
gamesPlayed: number;
|
||||
xp: number;
|
||||
avatar: string | undefined;
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
export default UserData;
|
||||
@@ -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
|
||||
@@ -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
25
front/nginx.conf.template
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
|
||||
@@ -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 }}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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' }}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
@@ -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> */}
|
||||
|
||||
227
front/views/StartPageView.tsx
Normal file
227
front/views/StartPageView.tsx
Normal 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;
|
||||
40
front/views/settings/GuestToUserView.tsx
Normal file
40
front/views/settings/GuestToUserView.tsx
Normal 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;
|
||||
80
front/views/settings/NotificationView.tsx
Normal file
80
front/views/settings/NotificationView.tsx
Normal 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;
|
||||
151
front/views/settings/PreferencesView.tsx
Normal file
151
front/views/settings/PreferencesView.tsx
Normal 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;
|
||||
65
front/views/settings/PrivacyView.tsx
Normal file
65
front/views/settings/PrivacyView.tsx
Normal 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;
|
||||
225
front/views/settings/SettingsProfileView.tsx
Normal file
225
front/views/settings/SettingsProfileView.tsx
Normal 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;
|
||||
184
front/views/settings/SettingsView.tsx
Normal file
184
front/views/settings/SettingsView.tsx
Normal 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;
|
||||
528
front/yarn.lock
528
front/yarn.lock
@@ -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
16
grafana/datasource.yml
Normal 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
|
||||
43
grafana/docker-compose.yaml
Normal file
43
grafana/docker-compose.yaml
Normal 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
54
grafana/loki.yaml
Normal 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
|
||||
21
musics/SCORO_TEST/SCORO_TEST.ini
Normal file
21
musics/SCORO_TEST/SCORO_TEST.ini
Normal 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
|
||||
|
||||
BIN
musics/SCORO_TEST/SCORO_TEST.midi
Normal file
BIN
musics/SCORO_TEST/SCORO_TEST.midi
Normal file
Binary file not shown.
BIN
musics/SCORO_TEST/SCORO_TEST.midi.bak
Normal file
BIN
musics/SCORO_TEST/SCORO_TEST.midi.bak
Normal file
Binary file not shown.
BIN
musics/SCORO_TEST/SCORO_TEST.mxl
Normal file
BIN
musics/SCORO_TEST/SCORO_TEST.mxl
Normal file
Binary file not shown.
21
musics/Short/Short.ini
Normal file
21
musics/Short/Short.ini
Normal 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
Reference in New Issue
Block a user