Compare commits
121 Commits
guest-user
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
74f804bb9a | ||
|
|
0b78772d0b | ||
|
|
9e3c2d1cca | ||
|
|
1b097163a4 | ||
|
|
c61d17baa7 | ||
|
|
be8867e12f | ||
|
|
00f98151c1 | ||
|
|
5d78d8b5dd | ||
|
|
0bd12bbf34 | ||
|
|
88cb7b2b65 | ||
|
|
69329118f7 | ||
|
|
2781276c12 | ||
|
|
a24a960184 | ||
|
|
9fd70d3110 | ||
|
|
c1d714e02a | ||
|
|
c08a1a2c74 | ||
|
|
23a1ff8d19 | ||
|
|
b80167001f | ||
|
|
8c2a53aa41 | ||
|
|
dcca780f2d | ||
|
|
9150817c05 | ||
|
|
d57606dd53 | ||
|
|
52f2c94fb7 | ||
|
|
1952625098 | ||
|
|
10dbfda8a4 | ||
|
|
234335cf61 | ||
|
|
52d40b43f0 | ||
|
|
50522bbe63 | ||
|
|
ce927ea1a4 | ||
|
|
aebf409cea | ||
|
|
5f0ea41c04 | ||
|
|
d3c7e4a0a1 | ||
|
|
a3893bdb2b | ||
|
|
4ba4303b1e | ||
| e779876f54 | |||
| bd9edaa60e | |||
|
|
f2ad34c8ab | ||
|
|
131d7bf688 | ||
|
|
d44e75a83a | ||
| e487d6d91e | |||
| 7a63a66da5 | |||
| 17f64cd849 | |||
| ec17aa741f | |||
|
|
38110d2840 | ||
|
|
fd60f2d171 | ||
|
|
86b2c1be50 | ||
|
|
627b8df658 | ||
|
|
3f0d0d523b | ||
|
|
29a9ffce74 | ||
|
|
a69e5ac009 | ||
|
|
caa3322676 | ||
|
|
358841abd5 | ||
|
|
64e7dbc71e | ||
|
|
5a0809c1d0 | ||
|
|
4b5e3d2b04 | ||
|
|
5f24c6e7bd | ||
|
|
8bdf8ce334 | ||
|
|
9012a6a9d8 | ||
|
|
c5fd4aa7d5 | ||
|
|
65cd04a494 | ||
|
|
c79ae7c6e8 | ||
|
|
ddc97f0923 | ||
|
|
a9b902a427 | ||
|
|
96d8e649c8 | ||
|
|
22c93b7571 | ||
|
|
0644d4b580 | ||
|
|
ee6a76cdd9 | ||
|
|
934010a0c1 | ||
|
|
29b2bedae0 | ||
|
|
5ba815590a | ||
|
|
dd09827d08 | ||
| b5b94adc83 | |||
| 3c04e8bb39 | |||
| 17a4328af5 | |||
| e81f2c1f75 | |||
| f77874bec4 | |||
| cfc72b8bc1 | |||
| 359b20fc6d | |||
| a3659618ea | |||
| fa60fc65a9 | |||
| b1727b7838 | |||
| a3f4703dae | |||
| 038918c212 | |||
| 42a947dfb0 | |||
| 5525110d39 | |||
| 7160b77607 | |||
| b5183f84b4 | |||
|
|
7a2b877714 | ||
|
|
9416393618 | ||
|
|
eb245118dc | ||
|
|
13050e52f9 | ||
|
|
5ef3885f72 | ||
|
|
a103666caf | ||
|
|
29da5c2788 | ||
|
|
40f16ab9ca | ||
|
|
a33d56bd61 | ||
|
|
c7c9250594 | ||
|
|
1b1659fe92 | ||
|
|
3c9d71a757 | ||
|
|
342099157e | ||
|
|
bb7a17fc22 | ||
|
|
1880b89b0c | ||
|
|
e769ff1f13 | ||
|
|
0ea8cb86bb | ||
|
|
90f9574a6f | ||
|
|
f2f7ec3f8d | ||
|
|
9e7873cdd7 | ||
|
|
f46c2cfb4a | ||
|
|
9f14061efd | ||
|
|
88b111529b | ||
|
|
851ee7420f | ||
|
|
ef57eb752d | ||
|
|
fcb29ae484 | ||
|
|
5c4847ae2c | ||
|
|
5fc937d81b | ||
|
|
b3853646cb | ||
|
|
dac9849ef5 | ||
|
|
11ed8f90fd | ||
|
|
5d103c6687 | ||
|
|
be926dcaed | ||
|
|
3353a17611 |
@@ -16,9 +16,10 @@ GOOGLE_CALLBACK_URL=http://localhost:19006/logged/google
|
||||
SMTP_TRANSPORT=smtps://toto:tata@relay
|
||||
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'
|
||||
IGNORE_MAILS=true
|
||||
API_KEYS=SCOROTEST,ROBOTO,SCORO
|
||||
API_KEYS=SCOROTEST,ROBOTO,SCORO,POPULATE
|
||||
API_KEY_SCORO_TEST=SCOROTEST
|
||||
API_KEY_ROBOT=ROBOTO
|
||||
API_KEY_SCORO=SCORO
|
||||
API_KEY_POPULATE=POPULATE
|
||||
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"
|
||||
# vi: ft=sh
|
||||
|
||||
23
.github/workflows/back.yml
vendored
23
.github/workflows/back.yml
vendored
@@ -12,27 +12,30 @@ jobs:
|
||||
pull-requests: read
|
||||
# Set job outputs to values from filter step
|
||||
outputs:
|
||||
backend: ${{ steps.filter.outputs.backend }}
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
scoro: ${{ steps.filter.outputs.scoro }}
|
||||
back: ${{ steps.filter.outputs.back }}
|
||||
front: ${{ steps.filter.outputs.front }}
|
||||
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||
steps:
|
||||
# For pull requests it's not necessary to checkout the code
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
backend:
|
||||
- 'backend/**'
|
||||
frontend:
|
||||
- 'frontend/**'
|
||||
scoro:
|
||||
back:
|
||||
- 'back/**'
|
||||
- '.github/workflows/back.yml'
|
||||
front:
|
||||
- 'front/**'
|
||||
- '.github/workflows/front.yml'
|
||||
scorometer:
|
||||
- 'scorometer/**'
|
||||
- '.github/workflows/scoro.yml'
|
||||
back_build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 10
|
||||
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.backend == 'true' }}
|
||||
if: ${{ needs.changes.outputs.back == 'true' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./back
|
||||
@@ -47,7 +50,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
needs: [ back_build ]
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
if: ${{ needs.changes.outputs.back == 'true' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
23
.github/workflows/front.yml
vendored
23
.github/workflows/front.yml
vendored
@@ -12,28 +12,31 @@ jobs:
|
||||
pull-requests: read
|
||||
# Set job outputs to values from filter step
|
||||
outputs:
|
||||
backend: ${{ steps.filter.outputs.backend }}
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
scoro: ${{ steps.filter.outputs.scoro }}
|
||||
back: ${{ steps.filter.outputs.back }}
|
||||
front: ${{ steps.filter.outputs.front }}
|
||||
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||
steps:
|
||||
# For pull requests it's not necessary to checkout the code
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
backend:
|
||||
- 'backend/**'
|
||||
frontend:
|
||||
- 'frontend/**'
|
||||
scoro:
|
||||
back:
|
||||
- 'back/**'
|
||||
- '.github/workflows/back.yml'
|
||||
front:
|
||||
- 'front/**'
|
||||
- '.github/workflows/front.yml'
|
||||
scorometer:
|
||||
- 'scorometer/**'
|
||||
- '.github/workflows/scoro.yml'
|
||||
front_check:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./front
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
if: ${{ needs.changes.outputs.front == 'true' }}
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
@@ -54,7 +57,7 @@ jobs:
|
||||
defaults:
|
||||
run:
|
||||
working-directory: ./front
|
||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
||||
if: ${{ needs.changes.outputs.front == 'true' }}
|
||||
needs: [ front_check ]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
21
.github/workflows/scoro.yml
vendored
21
.github/workflows/scoro.yml
vendored
@@ -11,25 +11,28 @@ jobs:
|
||||
pull-requests: read
|
||||
# Set job outputs to values from filter step
|
||||
outputs:
|
||||
backend: ${{ steps.filter.outputs.backend }}
|
||||
frontend: ${{ steps.filter.outputs.frontend }}
|
||||
scoro: ${{ steps.filter.outputs.scoro }}
|
||||
back: ${{ steps.filter.outputs.back }}
|
||||
front: ${{ steps.filter.outputs.front }}
|
||||
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||
steps:
|
||||
# For pull requests it's not necessary to checkout the code
|
||||
- uses: dorny/paths-filter@v2
|
||||
id: filter
|
||||
with:
|
||||
filters: |
|
||||
backend:
|
||||
- 'backend/**'
|
||||
frontend:
|
||||
- 'frontend/**'
|
||||
scoro:
|
||||
back:
|
||||
- 'back/**'
|
||||
- '.github/workflows/back.yml'
|
||||
front:
|
||||
- 'front/**'
|
||||
- '.github/workflows/front.yml'
|
||||
scorometer:
|
||||
- 'scorometer/**'
|
||||
- '.github/workflows/scoro.yml'
|
||||
scoro_test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.scoro == 'true' }}
|
||||
if: ${{ needs.changes.outputs.scorometer == 'true' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
19
assets/create_melodies.sh
Executable file
19
assets/create_melodies.sh
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Iterate through subfolders
|
||||
find . -type d | while read -r dir; do
|
||||
# Check if .midi file exists in the subfolder
|
||||
midi_file=$(find "$dir" -maxdepth 1 -type f -name '*.midi' | head -n 1)
|
||||
|
||||
if [ -n "$midi_file" ]; then
|
||||
# Create output file name (melody.mp3) in the same subfolder
|
||||
output_file="${dir}/melody.mp3"
|
||||
|
||||
# Run the given command
|
||||
#timidity "$midi_file" -Ow -o - | ffmpeg -i - -acodec libmp3lame -ab 64k "$output_file"
|
||||
fluidsynth -a alsa -T raw -F - "$midi_file" | ffmpeg -f s32le -i - "$output_file"
|
||||
|
||||
echo "Converted: $midi_file to $output_file"
|
||||
fi
|
||||
done
|
||||
|
||||
BIN
assets/musics/Bach Minuet in G Minor (BWV Anh. 115)/melody.mp3
Normal file
BIN
assets/musics/Bach Minuet in G Minor (BWV Anh. 115)/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Canon in D (easy)/melody.mp3
Normal file
BIN
assets/musics/Canon in D (easy)/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
assets/musics/French National Anthem La Marseillaise/melody.mp3
Normal file
BIN
assets/musics/French National Anthem La Marseillaise/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Jesus Alegria dos Homens/melody.mp3
Normal file
BIN
assets/musics/Jesus Alegria dos Homens/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Liebestraum (easy)/melody.mp3
Normal file
BIN
assets/musics/Liebestraum (easy)/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Mary, Did You Know/melody.mp3
Normal file
BIN
assets/musics/Mary, Did You Know/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/SCORO_TEST/melody.mp3
Normal file
BIN
assets/musics/SCORO_TEST/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Sarabande - William Gillock/melody.mp3
Normal file
BIN
assets/musics/Sarabande - William Gillock/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Short/melody.mp3
Normal file
BIN
assets/musics/Short/melody.mp3
Normal file
Binary file not shown.
BIN
assets/musics/Silent Night/melody.mp3
Normal file
BIN
assets/musics/Silent Night/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
BIN
assets/musics/Twinkle Twinkle Little Star/melody.mp3
Normal file
BIN
assets/musics/Twinkle Twinkle Little Star/melody.mp3
Normal file
Binary file not shown.
Binary file not shown.
@@ -1,3 +1,3 @@
|
||||
FROM node:17
|
||||
FROM node:18.10.0
|
||||
WORKDIR /app
|
||||
CMD npm i ; npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
||||
|
||||
10761
back/package-lock.json
generated
10761
back/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ import {
|
||||
Response,
|
||||
Query,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
} from "@nestjs/common";
|
||||
import { AuthService } from "./auth.service";
|
||||
import { JwtAuthGuard } from "./jwt-auth.guard";
|
||||
@@ -287,8 +288,8 @@ export class AuthController {
|
||||
@ApiOkResponse({ description: "Successfully added liked song" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Post("me/likes/:id")
|
||||
addLikedSong(@Request() req: any, @Param("id") songId: number) {
|
||||
return this.usersService.addLikedSong(+req.user.id, +songId);
|
||||
addLikedSong(@Request() req: any, @Param("id", ParseIntPipe) songId: number) {
|
||||
return this.usersService.addLikedSong(+req.user.id, songId);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@@ -296,8 +297,11 @@ export class AuthController {
|
||||
@ApiOkResponse({ description: "Successfully removed liked song" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Delete("me/likes/:id")
|
||||
removeLikedSong(@Request() req: any, @Param("id") songId: number) {
|
||||
return this.usersService.removeLikedSong(+req.user.id, +songId);
|
||||
removeLikedSong(
|
||||
@Request() req: any,
|
||||
@Param("id", ParseIntPipe) songId: number,
|
||||
) {
|
||||
return this.usersService.removeLikedSong(+req.user.id, songId);
|
||||
}
|
||||
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@@ -317,7 +321,7 @@ export class AuthController {
|
||||
@ApiOkResponse({ description: "Successfully added score" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@Patch("me/score/:score")
|
||||
addScore(@Request() req: any, @Param("id") score: number) {
|
||||
addScore(@Request() req: any, @Param("score", ParseIntPipe) score: number) {
|
||||
return this.usersService.addScore(+req.user.id, score);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Controller, Get } from "@nestjs/common";
|
||||
import { Controller, Get, Put } from "@nestjs/common";
|
||||
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { ScoresService } from "./scores.service";
|
||||
import { User } from "@prisma/client";
|
||||
@@ -13,4 +13,10 @@ export class ScoresController {
|
||||
getTopTwenty(): Promise<User[]> {
|
||||
return this.scoresService.topTwenty();
|
||||
}
|
||||
|
||||
// @ApiOkResponse{{description: "Successfully updated the user's total score"}}
|
||||
// @Put("/add")
|
||||
// addScore(): Promise<void> {
|
||||
// return this.ScoresService.add()
|
||||
// }
|
||||
}
|
||||
|
||||
@@ -2,9 +2,6 @@ import {
|
||||
Controller,
|
||||
DefaultValuePipe,
|
||||
Get,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Query,
|
||||
Request,
|
||||
@@ -16,15 +13,13 @@ import {
|
||||
ApiTags,
|
||||
ApiUnauthorizedResponse,
|
||||
} from "@nestjs/swagger";
|
||||
import { Artist, Genre, Song } from "@prisma/client";
|
||||
import { Artist, Song } from "@prisma/client";
|
||||
import { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||
import { SearchService } from "./search.service";
|
||||
import { Song as _Song } from "src/_gen/prisma-class/song";
|
||||
import { Genre as _Genre } from "src/_gen/prisma-class/genre";
|
||||
import { Artist as _Artist } from "src/_gen/prisma-class/artist";
|
||||
import { mapInclude } from "src/utils/include";
|
||||
import { SongController } from "src/song/song.controller";
|
||||
import { GenreController } from "src/genre/genre.controller";
|
||||
import { ArtistController } from "src/artist/artist.controller";
|
||||
|
||||
@ApiTags("search")
|
||||
@@ -33,21 +28,21 @@ import { ArtistController } from "src/artist/artist.controller";
|
||||
export class SearchController {
|
||||
constructor(private readonly searchService: SearchService) {}
|
||||
|
||||
@Get("songs/:query")
|
||||
@Get("songs")
|
||||
@ApiOkResponse({ type: _Song, isArray: true })
|
||||
@ApiOperation({ description: "Search a song" })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async searchSong(
|
||||
@Request() req: any,
|
||||
@Param("query") query: string,
|
||||
@Query("artistId") artistId: number,
|
||||
@Query("genreId") genreId: number,
|
||||
@Query("q") query: string | null,
|
||||
@Query("artistId", new ParseIntPipe({ optional: true })) artistId: number,
|
||||
@Query("genreId", new ParseIntPipe({ optional: true })) genreId: number,
|
||||
@Query("include") include: string,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Song[] | null> {
|
||||
): Promise<Song[]> {
|
||||
return await this.searchService.searchSong(
|
||||
query,
|
||||
query ?? "",
|
||||
artistId,
|
||||
genreId,
|
||||
mapInclude(include, req, SongController.includableFields),
|
||||
@@ -56,7 +51,7 @@ export class SearchController {
|
||||
);
|
||||
}
|
||||
|
||||
@Get("artists/:query")
|
||||
@Get("artists")
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@ApiOkResponse({ type: _Artist, isArray: true })
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
@@ -64,12 +59,12 @@ export class SearchController {
|
||||
async searchArtists(
|
||||
@Request() req: any,
|
||||
@Query("include") include: string,
|
||||
@Param("query") query: string,
|
||||
@Query("q") query: string | null,
|
||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||
): Promise<Artist[] | null> {
|
||||
): Promise<Artist[]> {
|
||||
return await this.searchService.searchArtists(
|
||||
query,
|
||||
query ?? "",
|
||||
mapInclude(include, req, ArtistController.includableFields),
|
||||
skip,
|
||||
take,
|
||||
|
||||
@@ -177,6 +177,31 @@ export class SongController {
|
||||
}
|
||||
}
|
||||
|
||||
@Get(":id/assets/melody")
|
||||
@ApiOperation({
|
||||
description: "Streams the mp3 file of the requested song",
|
||||
})
|
||||
@ApiNotFoundResponse({ description: "Song not found" })
|
||||
@ApiOkResponse({ description: "Returns the mp3 file succesfully" })
|
||||
@Header("Cache-Control", "max-age=86400")
|
||||
@Header("Content-Type", "audio/mpeg")
|
||||
@Public()
|
||||
async getMelody(@Param("id", ParseIntPipe) id: number) {
|
||||
const song = await this.songService.song({ id });
|
||||
if (!song) throw new NotFoundException("Song not found");
|
||||
|
||||
const path = song.musicXmlPath;
|
||||
// mp3 file is next to the musicXML file and called melody.mp3
|
||||
const pathWithoutFile = path.substring(0, path.lastIndexOf("/"));
|
||||
|
||||
try {
|
||||
const file = createReadStream(pathWithoutFile + "/melody.mp3");
|
||||
return new StreamableFile(file, { type: "audio/mpeg" });
|
||||
} catch {
|
||||
throw new InternalServerErrorException();
|
||||
}
|
||||
}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({
|
||||
description:
|
||||
|
||||
@@ -122,7 +122,7 @@ export class UsersService {
|
||||
return this.prisma.user.update({
|
||||
where: { id: where },
|
||||
data: {
|
||||
partyPlayed: {
|
||||
totalScore: {
|
||||
increment: score,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5,7 +5,6 @@ volumes:
|
||||
scoro_logs:
|
||||
meilisearch:
|
||||
|
||||
|
||||
services:
|
||||
back:
|
||||
#platform: linux/amd64
|
||||
@@ -67,6 +66,7 @@ services:
|
||||
- NGINX_PORT=4567
|
||||
ports:
|
||||
- "19006:19006"
|
||||
- "8081:8081"
|
||||
volumes:
|
||||
- ./front:/app
|
||||
depends_on:
|
||||
@@ -92,7 +92,7 @@ services:
|
||||
- "4567:4567"
|
||||
|
||||
meilisearch:
|
||||
image: getmeili/meilisearch:v1.4
|
||||
image: getmeili/meilisearch:v1.5
|
||||
volumes:
|
||||
- meilisearch:/meili_data
|
||||
env_file:
|
||||
@@ -103,4 +103,3 @@ services:
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
|
||||
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
|
||||
}
|
||||
107
front/API.ts
107
front/API.ts
@@ -1,5 +1,4 @@
|
||||
import Artist, { ArtistHandler } from './models/Artist';
|
||||
import Album from './models/Album';
|
||||
import Chapter from './models/Chapter';
|
||||
import Lesson from './models/Lesson';
|
||||
import Genre, { GenreHandler } from './models/Genre';
|
||||
@@ -24,6 +23,7 @@ import * as yup from 'yup';
|
||||
import { base64ToBlob } from './utils/base64ToBlob';
|
||||
import { ImagePickerAsset } from 'expo-image-picker';
|
||||
import { SongCursorInfos, SongCursorInfosHandler } from './models/SongCursorInfos';
|
||||
import { searchProps } from './views/V2/SearchView';
|
||||
|
||||
type AuthenticationInput = { username: string; password: string };
|
||||
type RegistrationInput = AuthenticationInput & { email: string };
|
||||
@@ -497,84 +497,6 @@ export default class API {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search a song by its name
|
||||
* @param query the string used to find the songs
|
||||
*/
|
||||
public static searchSongs(query: string): Query<Song[]> {
|
||||
return {
|
||||
key: ['search', 'song', query],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/songs/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(SongHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search artists by name
|
||||
* @param query the string used to find the artists
|
||||
*/
|
||||
public static searchArtists(query: string): Query<Artist[]> {
|
||||
return {
|
||||
key: ['search', 'artist', query],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/artists/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(ArtistHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Search Album by name
|
||||
* @param query the string used to find the album
|
||||
*/
|
||||
public static searchAlbum(query: string): Query<Album[]> {
|
||||
return {
|
||||
key: ['search', 'album', query],
|
||||
exec: async () => [
|
||||
{
|
||||
id: 1,
|
||||
name: 'Super Trooper',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'Kingdom Heart 365/2 OST',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'The Legend Of Zelda Ocarina Of Time OST',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Random Access Memories',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve music genres
|
||||
*/
|
||||
public static searchGenres(query: string): Query<Genre[]> {
|
||||
return {
|
||||
key: ['search', 'genre', query],
|
||||
exec: () =>
|
||||
API.fetch(
|
||||
{
|
||||
route: `/search/genres/${query}`,
|
||||
},
|
||||
{ handler: ListHandler(GenreHandler) }
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a lesson
|
||||
* @param lessonId the id to find the lesson
|
||||
@@ -779,4 +701,31 @@ export default class API {
|
||||
public static getPartitionSvgUrl(songId: number): string {
|
||||
return `${API.baseUrl}/song/${songId}/assets/partition`;
|
||||
}
|
||||
|
||||
public static searchSongs(query: searchProps, include?: SongInclude[]): Query<Song[]> {
|
||||
const queryParams: string[] = [];
|
||||
|
||||
if (query.query) queryParams.push(`q=${encodeURIComponent(query.query)}`);
|
||||
if (query.artist) queryParams.push(`artistId=${query.artist}`);
|
||||
if (query.genre) queryParams.push(`genreId=${query.genre}`);
|
||||
if (include) queryParams.push(`include=${include.join(',')}`);
|
||||
|
||||
const queryString = queryParams.length > 0 ? `?${queryParams.join('&')}` : '';
|
||||
|
||||
return {
|
||||
key: ['search', query.query, query.artist, query.genre, include],
|
||||
exec: () => {
|
||||
return API.fetch(
|
||||
{
|
||||
route: `/search/songs${queryString}`,
|
||||
},
|
||||
{ handler: ListHandler(SongHandler) }
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public static getPartitionMelodyUrl(songId: number): string {
|
||||
return `${API.baseUrl}/song/${songId}/assets/melody`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/* eslint-disable @typescript-eslint/ban-types */
|
||||
import { NativeStackScreenProps, createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import {
|
||||
NavigationProp,
|
||||
ParamListBase,
|
||||
useNavigation as navigationHook,
|
||||
} from '@react-navigation/native';
|
||||
import React, { useEffect, useMemo } from 'react';
|
||||
NativeStackNavigationProp,
|
||||
NativeStackScreenProps,
|
||||
createNativeStackNavigator,
|
||||
} from '@react-navigation/native-stack';
|
||||
import { ParamListBase, useNavigation as navigationHook } from '@react-navigation/native';
|
||||
import React, { ComponentProps, ComponentType, useEffect, useMemo } from 'react';
|
||||
import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
|
||||
import { RootState, useSelector } from './state/Store';
|
||||
import { useDispatch } from 'react-redux';
|
||||
@@ -33,153 +33,201 @@ import ForgotPasswordView from './views/ForgotPasswordView';
|
||||
import DiscoveryView from './views/V2/DiscoveryView';
|
||||
import MusicView from './views/MusicView';
|
||||
import Leaderboardiew from './views/LeaderboardView';
|
||||
import { LinearGradient } from 'expo-linear-gradient';
|
||||
import { createCustomNavigator } from './utils/navigator';
|
||||
import { Cup, Discover, Music, SearchNormal1, Setting2, User } from 'iconsax-react-native';
|
||||
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
||||
|
||||
const Stack = createNativeStackNavigator<AppRouteParams>();
|
||||
const Tab = createCustomNavigator<AppRouteParams>();
|
||||
|
||||
const Tabs = () => {
|
||||
return (
|
||||
<Tab.Navigator>
|
||||
{Object.entries(tabRoutes).map(([name, route], routeIndex) => (
|
||||
<Tab.Screen
|
||||
key={'route-' + routeIndex}
|
||||
name={name as keyof AppRouteParams}
|
||||
options={{ ...route.options, headerTransparent: true }}
|
||||
component={route.component}
|
||||
/>
|
||||
))}
|
||||
</Tab.Navigator>
|
||||
);
|
||||
};
|
||||
|
||||
// Util function to hide route props in URL
|
||||
const removeMe = () => '';
|
||||
|
||||
const protectedRoutes = () =>
|
||||
({
|
||||
Home: {
|
||||
component: DiscoveryView,
|
||||
options: { headerShown: false },
|
||||
link: '/',
|
||||
const tabRoutes = {
|
||||
Home: {
|
||||
component: DiscoveryView,
|
||||
options: { headerShown: false, tabBarIcon: Discover },
|
||||
link: '/',
|
||||
},
|
||||
User: {
|
||||
component: ProfileView,
|
||||
options: { headerShown: false, tabBarIcon: User },
|
||||
link: '/user',
|
||||
},
|
||||
Music: {
|
||||
component: MusicView,
|
||||
options: { headerShown: false, tabBarIcon: Music },
|
||||
link: '/music',
|
||||
},
|
||||
Search: {
|
||||
component: SearchView,
|
||||
options: { headerShown: false, tabBarIcon: SearchNormal1 },
|
||||
link: '/search/:query?',
|
||||
},
|
||||
Leaderboard: {
|
||||
component: Leaderboardiew,
|
||||
options: { title: translate('leaderboardTitle'), headerShown: false, tabBarIcon: Cup },
|
||||
link: '/leaderboard',
|
||||
},
|
||||
Settings: {
|
||||
component: SettingsTab,
|
||||
options: { headerShown: false, tabBarIcon: Setting2, subMenu: true },
|
||||
link: '/settings/:screen?',
|
||||
stringify: {
|
||||
screen: removeMe,
|
||||
},
|
||||
Music: {
|
||||
component: MusicView,
|
||||
options: { headerShown: false },
|
||||
link: '/music',
|
||||
},
|
||||
Play: {
|
||||
component: PlayView,
|
||||
options: { headerShown: false, title: translate('play') },
|
||||
link: '/play/:songId',
|
||||
},
|
||||
Settings: {
|
||||
component: SettingsTab,
|
||||
options: { headerShown: false },
|
||||
link: '/settings/:screen?',
|
||||
stringify: {
|
||||
screen: removeMe,
|
||||
},
|
||||
},
|
||||
Artist: {
|
||||
component: ArtistDetailsView,
|
||||
options: { title: translate('artistFilter') },
|
||||
link: '/artist/:artistId',
|
||||
},
|
||||
Genre: {
|
||||
component: GenreDetailsView,
|
||||
options: { title: translate('genreFilter') },
|
||||
link: '/genre/:genreId',
|
||||
},
|
||||
Search: {
|
||||
component: SearchView,
|
||||
options: { headerShown: false },
|
||||
link: '/search/:query?',
|
||||
},
|
||||
Leaderboard: {
|
||||
component: Leaderboardiew,
|
||||
options: { title: translate('leaderboardTitle'), headerShown: false },
|
||||
link: '/leaderboard',
|
||||
},
|
||||
Error: {
|
||||
component: ErrorView,
|
||||
options: { title: translate('error'), headerLeft: null },
|
||||
link: undefined,
|
||||
},
|
||||
User: { component: ProfileView, options: { headerShown: false }, link: '/user' },
|
||||
Verified: {
|
||||
component: VerifiedView,
|
||||
options: { title: 'Verify email', headerShown: false },
|
||||
link: '/verify',
|
||||
},
|
||||
}) as const;
|
||||
},
|
||||
};
|
||||
|
||||
const publicRoutes = () =>
|
||||
({
|
||||
Login: {
|
||||
component: SigninView,
|
||||
options: { title: translate('signInBtn'), headerShown: false },
|
||||
link: '/login',
|
||||
},
|
||||
Signup: {
|
||||
component: SignupView,
|
||||
options: { title: translate('signUpBtn'), headerShown: false },
|
||||
link: '/signup',
|
||||
},
|
||||
Google: {
|
||||
component: GoogleView,
|
||||
options: { title: 'Google signin', headerShown: false },
|
||||
link: '/logged/google',
|
||||
},
|
||||
PasswordReset: {
|
||||
component: PasswordResetView,
|
||||
options: { title: 'Password reset form', headerShown: false },
|
||||
link: '/password_reset',
|
||||
},
|
||||
ForgotPassword: {
|
||||
component: ForgotPasswordView,
|
||||
options: { title: 'Password reset form', headerShown: false },
|
||||
link: '/forgot_password',
|
||||
},
|
||||
}) as const;
|
||||
const protectedRoutes = {
|
||||
Tabs: {
|
||||
component: Tabs,
|
||||
options: { headerShown: false, path: '' },
|
||||
link: '',
|
||||
childRoutes: tabRoutes,
|
||||
},
|
||||
Play: {
|
||||
component: PlayView,
|
||||
options: { headerShown: false, title: translate('play') },
|
||||
link: '/play/:songId',
|
||||
},
|
||||
Artist: {
|
||||
component: ArtistDetailsView,
|
||||
options: { title: translate('artistFilter') },
|
||||
link: '/artist/:artistId',
|
||||
},
|
||||
Genre: {
|
||||
component: GenreDetailsView,
|
||||
options: { title: translate('genreFilter') },
|
||||
link: '/genre/:genreId',
|
||||
},
|
||||
Error: {
|
||||
component: ErrorView,
|
||||
options: { title: translate('error'), headerLeft: null },
|
||||
link: undefined,
|
||||
},
|
||||
Verified: {
|
||||
component: VerifiedView,
|
||||
options: { title: 'Verify email', headerShown: false },
|
||||
link: '/verify',
|
||||
},
|
||||
};
|
||||
|
||||
const publicRoutes = {
|
||||
Login: {
|
||||
component: SigninView,
|
||||
options: { title: translate('signInBtn'), headerShown: false },
|
||||
link: '/login',
|
||||
},
|
||||
Signup: {
|
||||
component: SignupView,
|
||||
options: { title: translate('signUpBtn'), headerShown: false },
|
||||
link: '/signup',
|
||||
},
|
||||
Google: {
|
||||
component: GoogleView,
|
||||
options: { title: 'Google signin', headerShown: false },
|
||||
link: '/logged/google',
|
||||
},
|
||||
PasswordReset: {
|
||||
component: PasswordResetView,
|
||||
options: { title: 'Password reset form', headerShown: false },
|
||||
link: '/password_reset',
|
||||
},
|
||||
ForgotPassword: {
|
||||
component: ForgotPasswordView,
|
||||
options: { title: 'Password reset form', headerShown: false },
|
||||
link: '/forgot_password',
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type Route<Props = any> = {
|
||||
component: (arg: RouteProps<Props>) => JSX.Element | (() => JSX.Element);
|
||||
component: ComponentType<Props>;
|
||||
options: object;
|
||||
link?: string;
|
||||
};
|
||||
|
||||
type OmitOrUndefined<T, K extends string> = T extends undefined ? T : Omit<T, K>;
|
||||
// if the component has no props, ComponentProps return unknown so we remove those
|
||||
type RemoveNonObjects<T> = [T] extends [{}] ? T : undefined;
|
||||
|
||||
type RouteParams<Routes extends Record<string, Route>> = {
|
||||
[RouteName in keyof Routes]: OmitOrUndefined<
|
||||
Parameters<Routes[RouteName]['component']>[0],
|
||||
keyof NativeStackScreenProps<{}>
|
||||
>;
|
||||
[RouteName in keyof Routes]: RemoveNonObjects<ComponentProps<Routes[RouteName]['component']>>;
|
||||
};
|
||||
|
||||
type PrivateRoutesParams = RouteParams<ReturnType<typeof protectedRoutes>>;
|
||||
type PublicRoutesParams = RouteParams<ReturnType<typeof publicRoutes>>;
|
||||
type AppRouteParams = PrivateRoutesParams & PublicRoutesParams;
|
||||
type PrivateRoutesParams = RouteParams<typeof protectedRoutes>;
|
||||
type PublicRoutesParams = RouteParams<typeof publicRoutes>;
|
||||
type TabsRoutesParams = RouteParams<typeof tabRoutes>;
|
||||
type AppRouteParams = Omit<PrivateRoutesParams, 'Tabs'> & {
|
||||
Tabs: { screen: keyof TabsRoutesParams };
|
||||
} & PublicRoutesParams &
|
||||
TabsRoutesParams & { Oops: undefined };
|
||||
|
||||
const Stack = createNativeStackNavigator<AppRouteParams & { Loading: never; Oops: never }>();
|
||||
const RouteToScreen = <T extends {}>(Component: Route<T>['component']) =>
|
||||
function Route(props: NativeStackScreenProps<T & ParamListBase>) {
|
||||
const colorScheme = useColorScheme();
|
||||
const insets = useSafeAreaInsets();
|
||||
|
||||
const RouteToScreen =
|
||||
<T extends {}>(component: Route<T>['component']) =>
|
||||
// eslint-disable-next-line react/display-name
|
||||
(props: NativeStackScreenProps<T & ParamListBase>) => (
|
||||
<>
|
||||
{component({ ...props.route.params, route: props.route } as Parameters<
|
||||
Route<T>['component']
|
||||
>[0])}
|
||||
</>
|
||||
);
|
||||
return (
|
||||
<LinearGradient
|
||||
colors={colorScheme === 'dark' ? ['#101014', '#6075F9'] : ['#cdd4fd', '#cdd4fd']}
|
||||
style={{
|
||||
flex: 1,
|
||||
paddingTop: insets.top,
|
||||
paddingBottom: insets.bottom,
|
||||
paddingLeft: insets.left,
|
||||
paddingRight: insets.right,
|
||||
}}
|
||||
>
|
||||
<Component {...(props.route.params as T)} route={props.route} />
|
||||
</LinearGradient>
|
||||
);
|
||||
};
|
||||
|
||||
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}
|
||||
options={{ ...route.options, headerTransparent: true }}
|
||||
component={RouteToScreen(route.component)}
|
||||
/>
|
||||
));
|
||||
|
||||
const routesToLinkingConfig = (
|
||||
routes: Partial<
|
||||
Record<keyof AppRouteParams, { link?: string; stringify?: Record<string, () => string> }>
|
||||
>
|
||||
) => {
|
||||
type RouteDescription = Record<
|
||||
string,
|
||||
{ link?: string; stringify?: Record<string, () => string>; childRoutes?: RouteDescription }
|
||||
>;
|
||||
|
||||
const routesToLinkingConfig = (routes: RouteDescription) => {
|
||||
// Too lazy to (find the) type
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const pagesToRoute = {} as Record<keyof AppRouteParams, any>;
|
||||
Object.keys(routes).forEach((route) => {
|
||||
const index = route as keyof AppRouteParams;
|
||||
if (routes[index]?.link) {
|
||||
if (routes[index]?.link !== undefined) {
|
||||
pagesToRoute[index] = {
|
||||
path: routes[index]!.link!,
|
||||
stringify: routes[index]!.stringify,
|
||||
screens: routes[index]!.childRoutes
|
||||
? routesToLinkingConfig(routes[index]!.childRoutes!).config.screens
|
||||
: undefined,
|
||||
};
|
||||
}
|
||||
});
|
||||
@@ -239,12 +287,6 @@ export const Router = () => {
|
||||
}
|
||||
return 'noAuth';
|
||||
}, [userProfile, accessToken]);
|
||||
const routes = useMemo(() => {
|
||||
if (authStatus == 'authed') {
|
||||
return protectedRoutes();
|
||||
}
|
||||
return publicRoutes();
|
||||
}, [authStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (accessToken) {
|
||||
@@ -257,13 +299,14 @@ export const Router = () => {
|
||||
return <LoadingView />;
|
||||
}
|
||||
|
||||
const routes = authStatus == 'authed' ? { ...protectedRoutes } : publicRoutes;
|
||||
return (
|
||||
<NavigationContainer
|
||||
linking={routesToLinkingConfig(routes)}
|
||||
fallback={<LoadingView />}
|
||||
theme={colorScheme == 'light' ? DefaultTheme : DarkTheme}
|
||||
>
|
||||
<Stack.Navigator>
|
||||
<Stack.Navigator screenOptions={{ navigationBarColor: 'transparent' }}>
|
||||
{authStatus == 'error' ? (
|
||||
<>
|
||||
<Stack.Screen
|
||||
@@ -272,7 +315,7 @@ export const Router = () => {
|
||||
<ProfileErrorView onTryAgain={() => userProfile.refetch()} />
|
||||
))}
|
||||
/>
|
||||
{routesToScreens(publicRoutes())}
|
||||
{routesToScreens(publicRoutes)}
|
||||
</>
|
||||
) : (
|
||||
routesToScreens(routes)
|
||||
@@ -282,6 +325,4 @@ export const Router = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export type RouteProps<T> = T & Pick<NativeStackScreenProps<T & ParamListBase>, 'route'>;
|
||||
|
||||
export const useNavigation = () => navigationHook<NavigationProp<AppRouteParams>>();
|
||||
export const useNavigation = () => navigationHook<NativeStackNavigationProp<AppRouteParams>>();
|
||||
|
||||
@@ -30,9 +30,8 @@ const phoneLightGlassmorphism = {
|
||||
900: 'rgb(248, 250, 254)',
|
||||
1000: 'rgb(252, 254, 254)',
|
||||
};
|
||||
const lightGlassmorphism =
|
||||
Platform.OS === 'web' ? defaultLightGlassmorphism : phoneLightGlassmorphism;
|
||||
const darkGlassmorphism = {
|
||||
|
||||
const defaultDarkGlassmorphism = {
|
||||
50: 'rgba(16,16,20,0.9)',
|
||||
100: 'rgba(16,16,20,0.1)',
|
||||
200: 'rgba(16,16,20,0.2)',
|
||||
@@ -46,6 +45,24 @@ const darkGlassmorphism = {
|
||||
1000: 'rgba(16,16,20,1)',
|
||||
};
|
||||
|
||||
const phoneDarkGlassmorphism = {
|
||||
50: 'rgb(10, 14, 38)',
|
||||
100: 'rgb(14, 18, 42)',
|
||||
200: 'rgb(18, 22, 46)',
|
||||
300: 'rgb(22, 26, 50)',
|
||||
400: 'rgb(26, 30, 54)',
|
||||
500: 'rgb(10, 20, 54)',
|
||||
600: 'rgb(14, 24, 58)',
|
||||
700: 'rgb(18, 28, 62)',
|
||||
800: 'rgb(22, 32, 66)',
|
||||
900: 'rgb(26, 36, 70)',
|
||||
1000: 'rgb(30, 40, 74)',
|
||||
};
|
||||
|
||||
const lightGlassmorphism =
|
||||
Platform.OS === 'web' ? defaultLightGlassmorphism : phoneLightGlassmorphism;
|
||||
const darkGlassmorphism = Platform.OS === 'web' ? defaultDarkGlassmorphism : phoneDarkGlassmorphism;
|
||||
|
||||
const ThemeProvider = ({ children }: { children: JSX.Element }) => {
|
||||
const colorScheme = useColorScheme();
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user