Compare commits
121 Commits
sound-expe
...
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
|
SMTP_TRANSPORT=smtps://toto:tata@relay
|
||||||
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'
|
MAIL_AUTHOR='"Chromacase" <chromacase@octohub.app>'
|
||||||
IGNORE_MAILS=true
|
IGNORE_MAILS=true
|
||||||
API_KEYS=SCOROTEST,ROBOTO,SCORO
|
API_KEYS=SCOROTEST,ROBOTO,SCORO,POPULATE
|
||||||
API_KEY_SCORO_TEST=SCOROTEST
|
API_KEY_SCORO_TEST=SCOROTEST
|
||||||
API_KEY_ROBOT=ROBOTO
|
API_KEY_ROBOT=ROBOTO
|
||||||
API_KEY_SCORO=SCORO
|
API_KEY_SCORO=SCORO
|
||||||
|
API_KEY_POPULATE=POPULATE
|
||||||
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"
|
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"
|
||||||
# vi: ft=sh
|
# vi: ft=sh
|
||||||
|
|||||||
23
.github/workflows/back.yml
vendored
23
.github/workflows/back.yml
vendored
@@ -12,27 +12,30 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
# Set job outputs to values from filter step
|
# Set job outputs to values from filter step
|
||||||
outputs:
|
outputs:
|
||||||
backend: ${{ steps.filter.outputs.backend }}
|
back: ${{ steps.filter.outputs.back }}
|
||||||
frontend: ${{ steps.filter.outputs.frontend }}
|
front: ${{ steps.filter.outputs.front }}
|
||||||
scoro: ${{ steps.filter.outputs.scoro }}
|
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||||
steps:
|
steps:
|
||||||
# For pull requests it's not necessary to checkout the code
|
# For pull requests it's not necessary to checkout the code
|
||||||
- uses: dorny/paths-filter@v2
|
- uses: dorny/paths-filter@v2
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
backend:
|
back:
|
||||||
- 'backend/**'
|
- 'back/**'
|
||||||
frontend:
|
- '.github/workflows/back.yml'
|
||||||
- 'frontend/**'
|
front:
|
||||||
scoro:
|
- 'front/**'
|
||||||
|
- '.github/workflows/front.yml'
|
||||||
|
scorometer:
|
||||||
- 'scorometer/**'
|
- 'scorometer/**'
|
||||||
|
- '.github/workflows/scoro.yml'
|
||||||
back_build:
|
back_build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
|
|
||||||
needs: changes
|
needs: changes
|
||||||
if: ${{ needs.changes.outputs.backend == 'true' }}
|
if: ${{ needs.changes.outputs.back == 'true' }}
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./back
|
working-directory: ./back
|
||||||
@@ -47,7 +50,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 15
|
timeout-minutes: 15
|
||||||
needs: [ back_build ]
|
needs: [ back_build ]
|
||||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
if: ${{ needs.changes.outputs.back == 'true' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|||||||
23
.github/workflows/front.yml
vendored
23
.github/workflows/front.yml
vendored
@@ -12,28 +12,31 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
# Set job outputs to values from filter step
|
# Set job outputs to values from filter step
|
||||||
outputs:
|
outputs:
|
||||||
backend: ${{ steps.filter.outputs.backend }}
|
back: ${{ steps.filter.outputs.back }}
|
||||||
frontend: ${{ steps.filter.outputs.frontend }}
|
front: ${{ steps.filter.outputs.front }}
|
||||||
scoro: ${{ steps.filter.outputs.scoro }}
|
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||||
steps:
|
steps:
|
||||||
# For pull requests it's not necessary to checkout the code
|
# For pull requests it's not necessary to checkout the code
|
||||||
- uses: dorny/paths-filter@v2
|
- uses: dorny/paths-filter@v2
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
backend:
|
back:
|
||||||
- 'backend/**'
|
- 'back/**'
|
||||||
frontend:
|
- '.github/workflows/back.yml'
|
||||||
- 'frontend/**'
|
front:
|
||||||
scoro:
|
- 'front/**'
|
||||||
|
- '.github/workflows/front.yml'
|
||||||
|
scorometer:
|
||||||
- 'scorometer/**'
|
- 'scorometer/**'
|
||||||
|
- '.github/workflows/scoro.yml'
|
||||||
front_check:
|
front_check:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./front
|
working-directory: ./front
|
||||||
needs: changes
|
needs: changes
|
||||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
if: ${{ needs.changes.outputs.front == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-node@v3
|
- uses: actions/setup-node@v3
|
||||||
@@ -54,7 +57,7 @@ jobs:
|
|||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./front
|
working-directory: ./front
|
||||||
if: ${{ needs.changes.outputs.frontend == 'true' }}
|
if: ${{ needs.changes.outputs.front == 'true' }}
|
||||||
needs: [ front_check ]
|
needs: [ front_check ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|||||||
21
.github/workflows/scoro.yml
vendored
21
.github/workflows/scoro.yml
vendored
@@ -11,25 +11,28 @@ jobs:
|
|||||||
pull-requests: read
|
pull-requests: read
|
||||||
# Set job outputs to values from filter step
|
# Set job outputs to values from filter step
|
||||||
outputs:
|
outputs:
|
||||||
backend: ${{ steps.filter.outputs.backend }}
|
back: ${{ steps.filter.outputs.back }}
|
||||||
frontend: ${{ steps.filter.outputs.frontend }}
|
front: ${{ steps.filter.outputs.front }}
|
||||||
scoro: ${{ steps.filter.outputs.scoro }}
|
scorometer: ${{ steps.filter.outputs.scorometer }}
|
||||||
steps:
|
steps:
|
||||||
# For pull requests it's not necessary to checkout the code
|
# For pull requests it's not necessary to checkout the code
|
||||||
- uses: dorny/paths-filter@v2
|
- uses: dorny/paths-filter@v2
|
||||||
id: filter
|
id: filter
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
backend:
|
back:
|
||||||
- 'backend/**'
|
- 'back/**'
|
||||||
frontend:
|
- '.github/workflows/back.yml'
|
||||||
- 'frontend/**'
|
front:
|
||||||
scoro:
|
- 'front/**'
|
||||||
|
- '.github/workflows/front.yml'
|
||||||
|
scorometer:
|
||||||
- 'scorometer/**'
|
- 'scorometer/**'
|
||||||
|
- '.github/workflows/scoro.yml'
|
||||||
scoro_test:
|
scoro_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: changes
|
needs: changes
|
||||||
if: ${{ needs.changes.outputs.scoro == 'true' }}
|
if: ${{ needs.changes.outputs.scorometer == 'true' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- 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
|
WORKDIR /app
|
||||||
CMD npm i ; npx prisma generate ; npx prisma migrate dev ; npm run start:dev
|
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,
|
Response,
|
||||||
Query,
|
Query,
|
||||||
Param,
|
Param,
|
||||||
|
ParseIntPipe,
|
||||||
} from "@nestjs/common";
|
} from "@nestjs/common";
|
||||||
import { AuthService } from "./auth.service";
|
import { AuthService } from "./auth.service";
|
||||||
import { JwtAuthGuard } from "./jwt-auth.guard";
|
import { JwtAuthGuard } from "./jwt-auth.guard";
|
||||||
@@ -287,8 +288,8 @@ export class AuthController {
|
|||||||
@ApiOkResponse({ description: "Successfully added liked song" })
|
@ApiOkResponse({ description: "Successfully added liked song" })
|
||||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Post("me/likes/:id")
|
@Post("me/likes/:id")
|
||||||
addLikedSong(@Request() req: any, @Param("id") songId: number) {
|
addLikedSong(@Request() req: any, @Param("id", ParseIntPipe) songId: number) {
|
||||||
return this.usersService.addLikedSong(+req.user.id, +songId);
|
return this.usersService.addLikedSong(+req.user.id, songId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@@ -296,8 +297,11 @@ export class AuthController {
|
|||||||
@ApiOkResponse({ description: "Successfully removed liked song" })
|
@ApiOkResponse({ description: "Successfully removed liked song" })
|
||||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Delete("me/likes/:id")
|
@Delete("me/likes/:id")
|
||||||
removeLikedSong(@Request() req: any, @Param("id") songId: number) {
|
removeLikedSong(
|
||||||
return this.usersService.removeLikedSong(+req.user.id, +songId);
|
@Request() req: any,
|
||||||
|
@Param("id", ParseIntPipe) songId: number,
|
||||||
|
) {
|
||||||
|
return this.usersService.removeLikedSong(+req.user.id, songId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@@ -317,7 +321,7 @@ export class AuthController {
|
|||||||
@ApiOkResponse({ description: "Successfully added score" })
|
@ApiOkResponse({ description: "Successfully added score" })
|
||||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@Patch("me/score/:score")
|
@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);
|
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 { ApiOkResponse, ApiTags } from "@nestjs/swagger";
|
||||||
import { ScoresService } from "./scores.service";
|
import { ScoresService } from "./scores.service";
|
||||||
import { User } from "@prisma/client";
|
import { User } from "@prisma/client";
|
||||||
@@ -13,4 +13,10 @@ export class ScoresController {
|
|||||||
getTopTwenty(): Promise<User[]> {
|
getTopTwenty(): Promise<User[]> {
|
||||||
return this.scoresService.topTwenty();
|
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,
|
Controller,
|
||||||
DefaultValuePipe,
|
DefaultValuePipe,
|
||||||
Get,
|
Get,
|
||||||
InternalServerErrorException,
|
|
||||||
NotFoundException,
|
|
||||||
Param,
|
|
||||||
ParseIntPipe,
|
ParseIntPipe,
|
||||||
Query,
|
Query,
|
||||||
Request,
|
Request,
|
||||||
@@ -16,15 +13,13 @@ import {
|
|||||||
ApiTags,
|
ApiTags,
|
||||||
ApiUnauthorizedResponse,
|
ApiUnauthorizedResponse,
|
||||||
} from "@nestjs/swagger";
|
} 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 { JwtAuthGuard } from "src/auth/jwt-auth.guard";
|
||||||
import { SearchService } from "./search.service";
|
import { SearchService } from "./search.service";
|
||||||
import { Song as _Song } from "src/_gen/prisma-class/song";
|
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 { Artist as _Artist } from "src/_gen/prisma-class/artist";
|
||||||
import { mapInclude } from "src/utils/include";
|
import { mapInclude } from "src/utils/include";
|
||||||
import { SongController } from "src/song/song.controller";
|
import { SongController } from "src/song/song.controller";
|
||||||
import { GenreController } from "src/genre/genre.controller";
|
|
||||||
import { ArtistController } from "src/artist/artist.controller";
|
import { ArtistController } from "src/artist/artist.controller";
|
||||||
|
|
||||||
@ApiTags("search")
|
@ApiTags("search")
|
||||||
@@ -33,21 +28,21 @@ import { ArtistController } from "src/artist/artist.controller";
|
|||||||
export class SearchController {
|
export class SearchController {
|
||||||
constructor(private readonly searchService: SearchService) {}
|
constructor(private readonly searchService: SearchService) {}
|
||||||
|
|
||||||
@Get("songs/:query")
|
@Get("songs")
|
||||||
@ApiOkResponse({ type: _Song, isArray: true })
|
@ApiOkResponse({ type: _Song, isArray: true })
|
||||||
@ApiOperation({ description: "Search a song" })
|
@ApiOperation({ description: "Search a song" })
|
||||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
async searchSong(
|
async searchSong(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Param("query") query: string,
|
@Query("q") query: string | null,
|
||||||
@Query("artistId") artistId: number,
|
@Query("artistId", new ParseIntPipe({ optional: true })) artistId: number,
|
||||||
@Query("genreId") genreId: number,
|
@Query("genreId", new ParseIntPipe({ optional: true })) genreId: number,
|
||||||
@Query("include") include: string,
|
@Query("include") include: string,
|
||||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Song[] | null> {
|
): Promise<Song[]> {
|
||||||
return await this.searchService.searchSong(
|
return await this.searchService.searchSong(
|
||||||
query,
|
query ?? "",
|
||||||
artistId,
|
artistId,
|
||||||
genreId,
|
genreId,
|
||||||
mapInclude(include, req, SongController.includableFields),
|
mapInclude(include, req, SongController.includableFields),
|
||||||
@@ -56,7 +51,7 @@ export class SearchController {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get("artists/:query")
|
@Get("artists")
|
||||||
@UseGuards(JwtAuthGuard)
|
@UseGuards(JwtAuthGuard)
|
||||||
@ApiOkResponse({ type: _Artist, isArray: true })
|
@ApiOkResponse({ type: _Artist, isArray: true })
|
||||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||||
@@ -64,12 +59,12 @@ export class SearchController {
|
|||||||
async searchArtists(
|
async searchArtists(
|
||||||
@Request() req: any,
|
@Request() req: any,
|
||||||
@Query("include") include: string,
|
@Query("include") include: string,
|
||||||
@Param("query") query: string,
|
@Query("q") query: string | null,
|
||||||
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
|
||||||
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
|
||||||
): Promise<Artist[] | null> {
|
): Promise<Artist[]> {
|
||||||
return await this.searchService.searchArtists(
|
return await this.searchService.searchArtists(
|
||||||
query,
|
query ?? "",
|
||||||
mapInclude(include, req, ArtistController.includableFields),
|
mapInclude(include, req, ArtistController.includableFields),
|
||||||
skip,
|
skip,
|
||||||
take,
|
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()
|
@Post()
|
||||||
@ApiOperation({
|
@ApiOperation({
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export class UsersService {
|
|||||||
return this.prisma.user.update({
|
return this.prisma.user.update({
|
||||||
where: { id: where },
|
where: { id: where },
|
||||||
data: {
|
data: {
|
||||||
partyPlayed: {
|
totalScore: {
|
||||||
increment: score,
|
increment: score,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ volumes:
|
|||||||
scoro_logs:
|
scoro_logs:
|
||||||
meilisearch:
|
meilisearch:
|
||||||
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
back:
|
back:
|
||||||
#platform: linux/amd64
|
#platform: linux/amd64
|
||||||
@@ -22,7 +21,7 @@ services:
|
|||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
meilisearch:
|
meilisearch:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
env_file:
|
env_file:
|
||||||
@@ -67,6 +66,7 @@ services:
|
|||||||
- NGINX_PORT=4567
|
- NGINX_PORT=4567
|
||||||
ports:
|
ports:
|
||||||
- "19006:19006"
|
- "19006:19006"
|
||||||
|
- "8081:8081"
|
||||||
volumes:
|
volumes:
|
||||||
- ./front:/app
|
- ./front:/app
|
||||||
depends_on:
|
depends_on:
|
||||||
@@ -92,15 +92,14 @@ services:
|
|||||||
- "4567:4567"
|
- "4567:4567"
|
||||||
|
|
||||||
meilisearch:
|
meilisearch:
|
||||||
image: getmeili/meilisearch:v1.4
|
image: getmeili/meilisearch:v1.5
|
||||||
volumes:
|
volumes:
|
||||||
- meilisearch:/meili_data
|
- meilisearch:/meili_data
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:7700/health"]
|
test: ["CMD", "curl", "-f", "http://localhost:7700/health"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 5
|
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 Artist, { ArtistHandler } from './models/Artist';
|
||||||
import Album from './models/Album';
|
|
||||||
import Chapter from './models/Chapter';
|
import Chapter from './models/Chapter';
|
||||||
import Lesson from './models/Lesson';
|
import Lesson from './models/Lesson';
|
||||||
import Genre, { GenreHandler } from './models/Genre';
|
import Genre, { GenreHandler } from './models/Genre';
|
||||||
@@ -24,6 +23,7 @@ import * as yup from 'yup';
|
|||||||
import { base64ToBlob } from './utils/base64ToBlob';
|
import { base64ToBlob } from './utils/base64ToBlob';
|
||||||
import { ImagePickerAsset } from 'expo-image-picker';
|
import { ImagePickerAsset } from 'expo-image-picker';
|
||||||
import { SongCursorInfos, SongCursorInfosHandler } from './models/SongCursorInfos';
|
import { SongCursorInfos, SongCursorInfosHandler } from './models/SongCursorInfos';
|
||||||
|
import { searchProps } from './views/V2/SearchView';
|
||||||
|
|
||||||
type AuthenticationInput = { username: string; password: string };
|
type AuthenticationInput = { username: string; password: string };
|
||||||
type RegistrationInput = AuthenticationInput & { email: 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
|
* Retrieve a lesson
|
||||||
* @param lessonId the id to find the lesson
|
* @param lessonId the id to find the lesson
|
||||||
@@ -779,4 +701,31 @@ export default class API {
|
|||||||
public static getPartitionSvgUrl(songId: number): string {
|
public static getPartitionSvgUrl(songId: number): string {
|
||||||
return `${API.baseUrl}/song/${songId}/assets/partition`;
|
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`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,4 +4,4 @@ WORKDIR /app
|
|||||||
RUN yarn global add npx expo-cli
|
RUN yarn global add npx expo-cli
|
||||||
|
|
||||||
ENV DEVAPIURL http://back:3000
|
ENV DEVAPIURL http://back:3000
|
||||||
CMD npx expo start --web
|
CMD npx expo start --web
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/ban-types */
|
/* eslint-disable @typescript-eslint/ban-types */
|
||||||
import { NativeStackScreenProps, createNativeStackNavigator } from '@react-navigation/native-stack';
|
|
||||||
import {
|
import {
|
||||||
NavigationProp,
|
NativeStackNavigationProp,
|
||||||
ParamListBase,
|
NativeStackScreenProps,
|
||||||
useNavigation as navigationHook,
|
createNativeStackNavigator,
|
||||||
} from '@react-navigation/native';
|
} from '@react-navigation/native-stack';
|
||||||
import React, { useEffect, useMemo } from 'react';
|
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 { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
|
||||||
import { RootState, useSelector } from './state/Store';
|
import { RootState, useSelector } from './state/Store';
|
||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
@@ -33,153 +33,201 @@ import ForgotPasswordView from './views/ForgotPasswordView';
|
|||||||
import DiscoveryView from './views/V2/DiscoveryView';
|
import DiscoveryView from './views/V2/DiscoveryView';
|
||||||
import MusicView from './views/MusicView';
|
import MusicView from './views/MusicView';
|
||||||
import Leaderboardiew from './views/LeaderboardView';
|
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
|
// Util function to hide route props in URL
|
||||||
const removeMe = () => '';
|
const removeMe = () => '';
|
||||||
|
|
||||||
const protectedRoutes = () =>
|
const tabRoutes = {
|
||||||
({
|
Home: {
|
||||||
Home: {
|
component: DiscoveryView,
|
||||||
component: DiscoveryView,
|
options: { headerShown: false, tabBarIcon: Discover },
|
||||||
options: { headerShown: false },
|
link: '/',
|
||||||
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 = () =>
|
const protectedRoutes = {
|
||||||
({
|
Tabs: {
|
||||||
Login: {
|
component: Tabs,
|
||||||
component: SigninView,
|
options: { headerShown: false, path: '' },
|
||||||
options: { title: translate('signInBtn'), headerShown: false },
|
link: '',
|
||||||
link: '/login',
|
childRoutes: tabRoutes,
|
||||||
},
|
},
|
||||||
Signup: {
|
Play: {
|
||||||
component: SignupView,
|
component: PlayView,
|
||||||
options: { title: translate('signUpBtn'), headerShown: false },
|
options: { headerShown: false, title: translate('play') },
|
||||||
link: '/signup',
|
link: '/play/:songId',
|
||||||
},
|
},
|
||||||
Google: {
|
Artist: {
|
||||||
component: GoogleView,
|
component: ArtistDetailsView,
|
||||||
options: { title: 'Google signin', headerShown: false },
|
options: { title: translate('artistFilter') },
|
||||||
link: '/logged/google',
|
link: '/artist/:artistId',
|
||||||
},
|
},
|
||||||
PasswordReset: {
|
Genre: {
|
||||||
component: PasswordResetView,
|
component: GenreDetailsView,
|
||||||
options: { title: 'Password reset form', headerShown: false },
|
options: { title: translate('genreFilter') },
|
||||||
link: '/password_reset',
|
link: '/genre/:genreId',
|
||||||
},
|
},
|
||||||
ForgotPassword: {
|
Error: {
|
||||||
component: ForgotPasswordView,
|
component: ErrorView,
|
||||||
options: { title: 'Password reset form', headerShown: false },
|
options: { title: translate('error'), headerLeft: null },
|
||||||
link: '/forgot_password',
|
link: undefined,
|
||||||
},
|
},
|
||||||
}) as const;
|
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
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
type Route<Props = any> = {
|
type Route<Props = any> = {
|
||||||
component: (arg: RouteProps<Props>) => JSX.Element | (() => JSX.Element);
|
component: ComponentType<Props>;
|
||||||
options: object;
|
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>> = {
|
type RouteParams<Routes extends Record<string, Route>> = {
|
||||||
[RouteName in keyof Routes]: OmitOrUndefined<
|
[RouteName in keyof Routes]: RemoveNonObjects<ComponentProps<Routes[RouteName]['component']>>;
|
||||||
Parameters<Routes[RouteName]['component']>[0],
|
|
||||||
keyof NativeStackScreenProps<{}>
|
|
||||||
>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type PrivateRoutesParams = RouteParams<ReturnType<typeof protectedRoutes>>;
|
type PrivateRoutesParams = RouteParams<typeof protectedRoutes>;
|
||||||
type PublicRoutesParams = RouteParams<ReturnType<typeof publicRoutes>>;
|
type PublicRoutesParams = RouteParams<typeof publicRoutes>;
|
||||||
type AppRouteParams = PrivateRoutesParams & PublicRoutesParams;
|
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 =
|
return (
|
||||||
<T extends {}>(component: Route<T>['component']) =>
|
<LinearGradient
|
||||||
// eslint-disable-next-line react/display-name
|
colors={colorScheme === 'dark' ? ['#101014', '#6075F9'] : ['#cdd4fd', '#cdd4fd']}
|
||||||
(props: NativeStackScreenProps<T & ParamListBase>) => (
|
style={{
|
||||||
<>
|
flex: 1,
|
||||||
{component({ ...props.route.params, route: props.route } as Parameters<
|
paddingTop: insets.top,
|
||||||
Route<T>['component']
|
paddingBottom: insets.bottom,
|
||||||
>[0])}
|
paddingLeft: insets.left,
|
||||||
</>
|
paddingRight: insets.right,
|
||||||
);
|
}}
|
||||||
|
>
|
||||||
|
<Component {...(props.route.params as T)} route={props.route} />
|
||||||
|
</LinearGradient>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const routesToScreens = (routes: Partial<Record<keyof AppRouteParams, Route>>) =>
|
const routesToScreens = (routes: Partial<Record<keyof AppRouteParams, Route>>) =>
|
||||||
Object.entries(routes).map(([name, route], routeIndex) => (
|
Object.entries(routes).map(([name, route], routeIndex) => (
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
key={'route-' + routeIndex}
|
key={'route-' + routeIndex}
|
||||||
name={name as keyof AppRouteParams}
|
name={name as keyof AppRouteParams}
|
||||||
options={route.options}
|
options={{ ...route.options, headerTransparent: true }}
|
||||||
component={RouteToScreen(route.component)}
|
component={RouteToScreen(route.component)}
|
||||||
/>
|
/>
|
||||||
));
|
));
|
||||||
|
|
||||||
const routesToLinkingConfig = (
|
type RouteDescription = Record<
|
||||||
routes: Partial<
|
string,
|
||||||
Record<keyof AppRouteParams, { link?: string; stringify?: Record<string, () => string> }>
|
{ link?: string; stringify?: Record<string, () => string>; childRoutes?: RouteDescription }
|
||||||
>
|
>;
|
||||||
) => {
|
|
||||||
|
const routesToLinkingConfig = (routes: RouteDescription) => {
|
||||||
// Too lazy to (find the) type
|
// Too lazy to (find the) type
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const pagesToRoute = {} as Record<keyof AppRouteParams, any>;
|
const pagesToRoute = {} as Record<keyof AppRouteParams, any>;
|
||||||
Object.keys(routes).forEach((route) => {
|
Object.keys(routes).forEach((route) => {
|
||||||
const index = route as keyof AppRouteParams;
|
const index = route as keyof AppRouteParams;
|
||||||
if (routes[index]?.link) {
|
if (routes[index]?.link !== undefined) {
|
||||||
pagesToRoute[index] = {
|
pagesToRoute[index] = {
|
||||||
path: routes[index]!.link!,
|
path: routes[index]!.link!,
|
||||||
stringify: routes[index]!.stringify,
|
stringify: routes[index]!.stringify,
|
||||||
|
screens: routes[index]!.childRoutes
|
||||||
|
? routesToLinkingConfig(routes[index]!.childRoutes!).config.screens
|
||||||
|
: undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -239,12 +287,6 @@ export const Router = () => {
|
|||||||
}
|
}
|
||||||
return 'noAuth';
|
return 'noAuth';
|
||||||
}, [userProfile, accessToken]);
|
}, [userProfile, accessToken]);
|
||||||
const routes = useMemo(() => {
|
|
||||||
if (authStatus == 'authed') {
|
|
||||||
return protectedRoutes();
|
|
||||||
}
|
|
||||||
return publicRoutes();
|
|
||||||
}, [authStatus]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
@@ -257,13 +299,14 @@ export const Router = () => {
|
|||||||
return <LoadingView />;
|
return <LoadingView />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const routes = authStatus == 'authed' ? { ...protectedRoutes } : publicRoutes;
|
||||||
return (
|
return (
|
||||||
<NavigationContainer
|
<NavigationContainer
|
||||||
linking={routesToLinkingConfig(routes)}
|
linking={routesToLinkingConfig(routes)}
|
||||||
fallback={<LoadingView />}
|
fallback={<LoadingView />}
|
||||||
theme={colorScheme == 'light' ? DefaultTheme : DarkTheme}
|
theme={colorScheme == 'light' ? DefaultTheme : DarkTheme}
|
||||||
>
|
>
|
||||||
<Stack.Navigator>
|
<Stack.Navigator screenOptions={{ navigationBarColor: 'transparent' }}>
|
||||||
{authStatus == 'error' ? (
|
{authStatus == 'error' ? (
|
||||||
<>
|
<>
|
||||||
<Stack.Screen
|
<Stack.Screen
|
||||||
@@ -272,7 +315,7 @@ export const Router = () => {
|
|||||||
<ProfileErrorView onTryAgain={() => userProfile.refetch()} />
|
<ProfileErrorView onTryAgain={() => userProfile.refetch()} />
|
||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
{routesToScreens(publicRoutes())}
|
{routesToScreens(publicRoutes)}
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
routesToScreens(routes)
|
routesToScreens(routes)
|
||||||
@@ -282,6 +325,4 @@ export const Router = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export type RouteProps<T> = T & Pick<NativeStackScreenProps<T & ParamListBase>, 'route'>;
|
export const useNavigation = () => navigationHook<NativeStackNavigationProp<AppRouteParams>>();
|
||||||
|
|
||||||
export const useNavigation = () => navigationHook<NavigationProp<AppRouteParams>>();
|
|
||||||
|
|||||||
@@ -30,9 +30,8 @@ const phoneLightGlassmorphism = {
|
|||||||
900: 'rgb(248, 250, 254)',
|
900: 'rgb(248, 250, 254)',
|
||||||
1000: 'rgb(252, 254, 254)',
|
1000: 'rgb(252, 254, 254)',
|
||||||
};
|
};
|
||||||
const lightGlassmorphism =
|
|
||||||
Platform.OS === 'web' ? defaultLightGlassmorphism : phoneLightGlassmorphism;
|
const defaultDarkGlassmorphism = {
|
||||||
const darkGlassmorphism = {
|
|
||||||
50: 'rgba(16,16,20,0.9)',
|
50: 'rgba(16,16,20,0.9)',
|
||||||
100: 'rgba(16,16,20,0.1)',
|
100: 'rgba(16,16,20,0.1)',
|
||||||
200: 'rgba(16,16,20,0.2)',
|
200: 'rgba(16,16,20,0.2)',
|
||||||
@@ -46,6 +45,24 @@ const darkGlassmorphism = {
|
|||||||
1000: 'rgba(16,16,20,1)',
|
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 ThemeProvider = ({ children }: { children: JSX.Element }) => {
|
||||||
const colorScheme = useColorScheme();
|
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.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user