3 Commits

Author SHA1 Message Date
Arthur Jamet
cec07b7e99 Front: Handle Error when guest username is already taken 2024-01-04 11:46:13 +01:00
Arthur Jamet
f93968c3eb Front: Add Username for Guest Mode 2024-01-04 11:33:11 +01:00
Arthur Jamet
f80253cea3 Back: Require Username for Guest Account Creation 2024-01-04 09:55:45 +01:00
182 changed files with 2462 additions and 12606 deletions

View File

@@ -16,10 +16,9 @@ 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,POPULATE
API_KEYS=SCOROTEST,ROBOTO,SCORO
API_KEY_SCORO_TEST=SCOROTEST
API_KEY_ROBOT=ROBOTO
API_KEY_SCORO=SCORO
API_KEY_POPULATE=POPULATE
MEILI_MASTER_KEY="ghvjkgisbgkbgskegblfqbgjkebbhgwkjfb"
# vi: ft=sh

View File

@@ -12,30 +12,27 @@ jobs:
pull-requests: read
# Set job outputs to values from filter step
outputs:
back: ${{ steps.filter.outputs.back }}
front: ${{ steps.filter.outputs.front }}
scorometer: ${{ steps.filter.outputs.scorometer }}
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
scoro: ${{ steps.filter.outputs.scoro }}
steps:
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
back:
- 'back/**'
- '.github/workflows/back.yml'
front:
- 'front/**'
- '.github/workflows/front.yml'
scorometer:
backend:
- 'backend/**'
frontend:
- 'frontend/**'
scoro:
- 'scorometer/**'
- '.github/workflows/scoro.yml'
back_build:
runs-on: ubuntu-latest
timeout-minutes: 10
needs: changes
if: ${{ needs.changes.outputs.back == 'true' }}
if: ${{ needs.changes.outputs.backend == 'true' }}
defaults:
run:
working-directory: ./back
@@ -50,7 +47,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 15
needs: [ back_build ]
if: ${{ needs.changes.outputs.back == 'true' }}
if: ${{ needs.changes.outputs.frontend == 'true' }}
steps:
- uses: actions/checkout@v3

View File

@@ -12,31 +12,28 @@ jobs:
pull-requests: read
# Set job outputs to values from filter step
outputs:
back: ${{ steps.filter.outputs.back }}
front: ${{ steps.filter.outputs.front }}
scorometer: ${{ steps.filter.outputs.scorometer }}
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
scoro: ${{ steps.filter.outputs.scoro }}
steps:
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
back:
- 'back/**'
- '.github/workflows/back.yml'
front:
- 'front/**'
- '.github/workflows/front.yml'
scorometer:
backend:
- 'backend/**'
frontend:
- 'frontend/**'
scoro:
- 'scorometer/**'
- '.github/workflows/scoro.yml'
front_check:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./front
needs: changes
if: ${{ needs.changes.outputs.front == 'true' }}
if: ${{ needs.changes.outputs.frontend == 'true' }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
@@ -57,7 +54,7 @@ jobs:
defaults:
run:
working-directory: ./front
if: ${{ needs.changes.outputs.front == 'true' }}
if: ${{ needs.changes.outputs.frontend == 'true' }}
needs: [ front_check ]
steps:
- uses: actions/checkout@v3

View File

@@ -11,28 +11,25 @@ jobs:
pull-requests: read
# Set job outputs to values from filter step
outputs:
back: ${{ steps.filter.outputs.back }}
front: ${{ steps.filter.outputs.front }}
scorometer: ${{ steps.filter.outputs.scorometer }}
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
scoro: ${{ steps.filter.outputs.scoro }}
steps:
# For pull requests it's not necessary to checkout the code
- uses: dorny/paths-filter@v2
id: filter
with:
filters: |
back:
- 'back/**'
- '.github/workflows/back.yml'
front:
- 'front/**'
- '.github/workflows/front.yml'
scorometer:
backend:
- 'backend/**'
frontend:
- 'frontend/**'
scoro:
- 'scorometer/**'
- '.github/workflows/scoro.yml'
scoro_test:
runs-on: ubuntu-latest
needs: changes
if: ${{ needs.changes.outputs.scorometer == 'true' }}
if: ${{ needs.changes.outputs.scoro == 'true' }}
steps:
- uses: actions/checkout@v3

View File

@@ -1,19 +0,0 @@
#!/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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,3 +1,3 @@
FROM node:18.10.0
FROM node:17
WORKDIR /app
CMD npm i ; npx prisma generate ; npx prisma migrate dev ; npm run start:dev

10761
back/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -21,7 +21,6 @@ import {
Response,
Query,
Param,
ParseIntPipe,
} from "@nestjs/common";
import { AuthService } from "./auth.service";
import { JwtAuthGuard } from "./jwt-auth.guard";
@@ -52,6 +51,7 @@ import { PasswordResetDto } from "./dto/password_reset.dto ";
import { mapInclude } from "src/utils/include";
import { SongController } from "src/song/song.controller";
import { ChromaAuthGuard } from "./chroma-auth.guard";
import { GuestDto } from "./dto/guest.dto";
@ApiTags("auth")
@Controller("auth")
@@ -163,8 +163,8 @@ export class AuthController {
@HttpCode(200)
@ApiOperation({ description: "Login as a guest account" })
@ApiOkResponse({ description: "Successfully logged in", type: JwtToken })
async guest(): Promise<JwtToken> {
const user = await this.usersService.createGuest();
async guest(@Body() guestdto: GuestDto): Promise<JwtToken> {
const user = await this.usersService.createGuest(guestdto.username);
await this.settingsService.createUserSetting(user.id);
return this.authService.login(user);
}
@@ -288,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", ParseIntPipe) songId: number) {
return this.usersService.addLikedSong(+req.user.id, songId);
addLikedSong(@Request() req: any, @Param("id") songId: number) {
return this.usersService.addLikedSong(+req.user.id, +songId);
}
@UseGuards(JwtAuthGuard)
@@ -297,11 +297,8 @@ export class AuthController {
@ApiOkResponse({ description: "Successfully removed liked song" })
@ApiUnauthorizedResponse({ description: "Invalid token" })
@Delete("me/likes/:id")
removeLikedSong(
@Request() req: any,
@Param("id", ParseIntPipe) songId: number,
) {
return this.usersService.removeLikedSong(+req.user.id, songId);
removeLikedSong(@Request() req: any, @Param("id") songId: number) {
return this.usersService.removeLikedSong(+req.user.id, +songId);
}
@UseGuards(JwtAuthGuard)
@@ -321,7 +318,7 @@ export class AuthController {
@ApiOkResponse({ description: "Successfully added score" })
@ApiUnauthorizedResponse({ description: "Invalid token" })
@Patch("me/score/:score")
addScore(@Request() req: any, @Param("score", ParseIntPipe) score: number) {
addScore(@Request() req: any, @Param("id") score: number) {
return this.usersService.addScore(+req.user.id, score);
}
}

View File

@@ -0,0 +1,8 @@
import { IsNotEmpty } from "class-validator";
import { ApiProperty } from "@nestjs/swagger";
export class GuestDto {
@ApiProperty()
@IsNotEmpty()
username: string;
}

View File

@@ -1,4 +1,4 @@
import { Controller, Get, Put } from "@nestjs/common";
import { Controller, Get } from "@nestjs/common";
import { ApiOkResponse, ApiTags } from "@nestjs/swagger";
import { ScoresService } from "./scores.service";
import { User } from "@prisma/client";
@@ -13,10 +13,4 @@ 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()
// }
}

View File

@@ -2,6 +2,9 @@ import {
Controller,
DefaultValuePipe,
Get,
InternalServerErrorException,
NotFoundException,
Param,
ParseIntPipe,
Query,
Request,
@@ -13,13 +16,15 @@ import {
ApiTags,
ApiUnauthorizedResponse,
} from "@nestjs/swagger";
import { Artist, Song } from "@prisma/client";
import { Artist, Genre, 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")
@@ -28,21 +33,21 @@ import { ArtistController } from "src/artist/artist.controller";
export class SearchController {
constructor(private readonly searchService: SearchService) {}
@Get("songs")
@Get("songs/:query")
@ApiOkResponse({ type: _Song, isArray: true })
@ApiOperation({ description: "Search a song" })
@ApiUnauthorizedResponse({ description: "Invalid token" })
async searchSong(
@Request() req: any,
@Query("q") query: string | null,
@Query("artistId", new ParseIntPipe({ optional: true })) artistId: number,
@Query("genreId", new ParseIntPipe({ optional: true })) genreId: number,
@Param("query") query: string,
@Query("artistId") artistId: number,
@Query("genreId") genreId: number,
@Query("include") include: string,
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Song[]> {
): Promise<Song[] | null> {
return await this.searchService.searchSong(
query ?? "",
query,
artistId,
genreId,
mapInclude(include, req, SongController.includableFields),
@@ -51,7 +56,7 @@ export class SearchController {
);
}
@Get("artists")
@Get("artists/:query")
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: _Artist, isArray: true })
@ApiUnauthorizedResponse({ description: "Invalid token" })
@@ -59,12 +64,12 @@ export class SearchController {
async searchArtists(
@Request() req: any,
@Query("include") include: string,
@Query("q") query: string | null,
@Param("query") query: string,
@Query("skip", new DefaultValuePipe(0), ParseIntPipe) skip: number,
@Query("take", new DefaultValuePipe(20), ParseIntPipe) take: number,
): Promise<Artist[]> {
): Promise<Artist[] | null> {
return await this.searchService.searchArtists(
query ?? "",
query,
mapInclude(include, req, ArtistController.includableFields),
skip,
take,

View File

@@ -177,31 +177,6 @@ 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:

View File

@@ -6,7 +6,7 @@ import {
import { User, Prisma } from "@prisma/client";
import { PrismaService } from "src/prisma/prisma.service";
import * as bcrypt from "bcryptjs";
import { createHash, randomUUID } from "crypto";
import { createHash } from "crypto";
import { createReadStream, existsSync } from "fs";
import fetch from "node-fetch";
@@ -46,10 +46,10 @@ export class UsersService {
});
}
async createGuest(): Promise<User> {
async createGuest(displayName: string): Promise<User> {
return this.prisma.user.create({
data: {
username: `Guest ${randomUUID()}`,
username: displayName,
isGuest: true,
// Not realyl clean but better than a separate table or breaking the api by adding nulls.
email: null,
@@ -122,7 +122,7 @@ export class UsersService {
return this.prisma.user.update({
where: { id: where },
data: {
totalScore: {
partyPlayed: {
increment: score,
},
},

View File

@@ -9,7 +9,7 @@ Resource ./auth.resource
*** Test Cases ***
LoginAsGuest
[Documentation] Login as a guest
&{res}= POST /auth/guest
&{res}= POST /auth/guest {"username": "i-am-a-guest"}
Output
Integer response status 200
String response body access_token
@@ -20,12 +20,13 @@ LoginAsGuest
Integer response status 200
Boolean response body isGuest true
Integer response body partyPlayed 0
String response body username "i-am-a-guest"
[Teardown] DELETE /auth/me
TwoGuests
[Documentation] Login as a guest
&{res}= POST /auth/guest
&{res}= POST /auth/guest {"username": "i-am-another-guest"}
Output
Integer response status 200
String response body access_token
@@ -36,8 +37,9 @@ TwoGuests
Integer response status 200
Boolean response body isGuest true
Integer response body partyPlayed 0
String response body username "i-am-another-guest"
&{res2}= POST /auth/guest
&{res2}= POST /auth/guest {"username": "i-am-a-third-guest"}
Output
Integer response status 200
String response body access_token
@@ -48,6 +50,7 @@ TwoGuests
Integer response status 200
Boolean response body isGuest true
Integer response body partyPlayed 0
String response body username "i-am-a-third-guest"
[Teardown] Run Keywords DELETE /auth/me
... AND Set Headers {"Authorization": "Bearer ${res.body.access_token}"}
@@ -55,7 +58,7 @@ TwoGuests
GuestToNormal
[Documentation] Login as a guest and convert to a normal account
&{res}= POST /auth/guest
&{res}= POST /auth/guest {"username": "i-will-be-a-real-user"}
Output
Integer response status 200
String response body access_token
@@ -65,11 +68,13 @@ GuestToNormal
Output
Integer response status 200
Boolean response body isGuest true
String response body username "i-will-be-a-real-user"
${res}= PUT /auth/me { "username": "toto", "password": "toto", "email": "awdaw@b.c"}
${res}= PUT /auth/me { "password": "toto", "email": "awdaw@b.c"}
Output
Integer response status 200
String response body username "toto"
Boolean response body isGuest false
String response body username "i-will-be-a-real-user"
[Teardown] DELETE /auth/me

View File

@@ -5,6 +5,7 @@ volumes:
scoro_logs:
meilisearch:
services:
back:
#platform: linux/amd64
@@ -21,7 +22,7 @@ services:
depends_on:
db:
condition: service_healthy
meilisearch:
condition: service_healthy
env_file:
@@ -66,7 +67,6 @@ services:
- NGINX_PORT=4567
ports:
- "19006:19006"
- "8081:8081"
volumes:
- ./front:/app
depends_on:
@@ -92,14 +92,15 @@ services:
- "4567:4567"
meilisearch:
image: getmeili/meilisearch:v1.5
image: getmeili/meilisearch:v1.4
volumes:
- meilisearch:/meili_data
env_file:
- .env
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7700/health"]
interval: 10s
timeout: 10s
retries: 5

View File

@@ -0,0 +1,4 @@
{
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
}

View File

@@ -1,4 +1,5 @@
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';
@@ -23,7 +24,6 @@ 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 };
@@ -187,12 +187,12 @@ export default class API {
});
}
public static async createAndGetGuestAccount(): Promise<AccessToken> {
public static async createAndGetGuestAccount(username: string): Promise<AccessToken> {
return API.fetch(
{
route: '/auth/guest',
method: 'POST',
body: undefined,
body: { username },
},
{ handler: AccessTokenResponseHandler }
)
@@ -497,6 +497,84 @@ 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
@@ -701,31 +779,4 @@ 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`;
}
}

View File

@@ -4,4 +4,4 @@ WORKDIR /app
RUN yarn global add npx expo-cli
ENV DEVAPIURL http://back:3000
CMD npx expo start --web
CMD npx expo start --web

View File

@@ -1,11 +1,11 @@
/* eslint-disable @typescript-eslint/ban-types */
import { NativeStackScreenProps, createNativeStackNavigator } from '@react-navigation/native-stack';
import {
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';
NavigationProp,
ParamListBase,
useNavigation as navigationHook,
} from '@react-navigation/native';
import React, { useEffect, useMemo } from 'react';
import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
import { RootState, useSelector } from './state/Store';
import { useDispatch } from 'react-redux';
@@ -33,201 +33,153 @@ 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 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,
const protectedRoutes = () =>
({
Home: {
component: DiscoveryView,
options: { headerShown: false },
link: '/',
},
},
};
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 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',
},
};
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;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Route<Props = any> = {
component: ComponentType<Props>;
component: (arg: RouteProps<Props>) => JSX.Element | (() => JSX.Element);
options: object;
link?: string;
};
// if the component has no props, ComponentProps return unknown so we remove those
type RemoveNonObjects<T> = [T] extends [{}] ? T : undefined;
type OmitOrUndefined<T, K extends string> = T extends undefined ? T : Omit<T, K>;
type RouteParams<Routes extends Record<string, Route>> = {
[RouteName in keyof Routes]: RemoveNonObjects<ComponentProps<Routes[RouteName]['component']>>;
[RouteName in keyof Routes]: OmitOrUndefined<
Parameters<Routes[RouteName]['component']>[0],
keyof NativeStackScreenProps<{}>
>;
};
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 };
type PrivateRoutesParams = RouteParams<ReturnType<typeof protectedRoutes>>;
type PublicRoutesParams = RouteParams<ReturnType<typeof publicRoutes>>;
type AppRouteParams = PrivateRoutesParams & PublicRoutesParams;
const RouteToScreen = <T extends {}>(Component: Route<T>['component']) =>
function Route(props: NativeStackScreenProps<T & ParamListBase>) {
const colorScheme = useColorScheme();
const insets = useSafeAreaInsets();
const Stack = createNativeStackNavigator<AppRouteParams & { Loading: never; Oops: never }>();
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 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])}
</>
);
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, headerTransparent: true }}
options={route.options}
component={RouteToScreen(route.component)}
/>
));
type RouteDescription = Record<
string,
{ link?: string; stringify?: Record<string, () => string>; childRoutes?: RouteDescription }
>;
const routesToLinkingConfig = (routes: RouteDescription) => {
const routesToLinkingConfig = (
routes: Partial<
Record<keyof AppRouteParams, { link?: string; stringify?: Record<string, () => string> }>
>
) => {
// 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 !== undefined) {
if (routes[index]?.link) {
pagesToRoute[index] = {
path: routes[index]!.link!,
stringify: routes[index]!.stringify,
screens: routes[index]!.childRoutes
? routesToLinkingConfig(routes[index]!.childRoutes!).config.screens
: undefined,
};
}
});
@@ -287,6 +239,12 @@ export const Router = () => {
}
return 'noAuth';
}, [userProfile, accessToken]);
const routes = useMemo(() => {
if (authStatus == 'authed') {
return protectedRoutes();
}
return publicRoutes();
}, [authStatus]);
useEffect(() => {
if (accessToken) {
@@ -299,14 +257,13 @@ 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 screenOptions={{ navigationBarColor: 'transparent' }}>
<Stack.Navigator>
{authStatus == 'error' ? (
<>
<Stack.Screen
@@ -315,7 +272,7 @@ export const Router = () => {
<ProfileErrorView onTryAgain={() => userProfile.refetch()} />
))}
/>
{routesToScreens(publicRoutes)}
{routesToScreens(publicRoutes())}
</>
) : (
routesToScreens(routes)
@@ -325,4 +282,6 @@ export const Router = () => {
);
};
export const useNavigation = () => navigationHook<NativeStackNavigationProp<AppRouteParams>>();
export type RouteProps<T> = T & Pick<NativeStackScreenProps<T & ParamListBase>, 'route'>;
export const useNavigation = () => navigationHook<NavigationProp<AppRouteParams>>();

View File

@@ -30,8 +30,9 @@ const phoneLightGlassmorphism = {
900: 'rgb(248, 250, 254)',
1000: 'rgb(252, 254, 254)',
};
const defaultDarkGlassmorphism = {
const lightGlassmorphism =
Platform.OS === 'web' ? defaultLightGlassmorphism : phoneLightGlassmorphism;
const darkGlassmorphism = {
50: 'rgba(16,16,20,0.9)',
100: 'rgba(16,16,20,0.1)',
200: 'rgba(16,16,20,0.2)',
@@ -45,24 +46,6 @@ const defaultDarkGlassmorphism = {
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();

BIN
front/assets/piano/a0.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/a7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/ab7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b0.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/b7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb0.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/bb7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/c8.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/d7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/db7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e1.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e2.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e3.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e4.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e5.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e6.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/e7.mp3 Normal file

Binary file not shown.

BIN
front/assets/piano/eb1.mp3 Normal file

Binary file not shown.

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