Compare commits
58 Commits
sound-expe
...
clem-fixes
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aae0bbea3a | ||
|
|
f5136ae59b | ||
|
|
f632ed42a3 | ||
|
|
00ee5cd531 | ||
|
|
1d496301d9 | ||
|
|
d44e75a83a | ||
| e487d6d91e | |||
| 7a63a66da5 | |||
| 17f64cd849 | |||
| ec17aa741f | |||
|
|
358841abd5 | ||
|
|
64e7dbc71e | ||
|
|
5a0809c1d0 | ||
|
|
4b5e3d2b04 | ||
|
|
5f24c6e7bd | ||
|
|
8bdf8ce334 | ||
|
|
9012a6a9d8 | ||
|
|
c5fd4aa7d5 | ||
|
|
65cd04a494 | ||
|
|
c79ae7c6e8 | ||
|
|
ddc97f0923 | ||
|
|
a9b902a427 | ||
|
|
96d8e649c8 | ||
|
|
22c93b7571 | ||
|
|
0644d4b580 | ||
|
|
ee6a76cdd9 | ||
|
|
5ba815590a | ||
|
|
dd09827d08 | ||
| b5b94adc83 | |||
| 3c04e8bb39 | |||
| 17a4328af5 | |||
| e81f2c1f75 | |||
| f77874bec4 | |||
| cfc72b8bc1 | |||
| 359b20fc6d | |||
| a3659618ea | |||
| fa60fc65a9 | |||
| b1727b7838 | |||
| a3f4703dae | |||
| 038918c212 | |||
| 42a947dfb0 | |||
| 5525110d39 | |||
| 7160b77607 | |||
| b5183f84b4 | |||
|
|
13050e52f9 | ||
|
|
5ef3885f72 | ||
|
|
a103666caf | ||
|
|
29da5c2788 | ||
|
|
1880b89b0c | ||
|
|
e769ff1f13 | ||
|
|
9e7873cdd7 | ||
|
|
f46c2cfb4a | ||
|
|
9f14061efd | ||
|
|
851ee7420f | ||
|
|
ef57eb752d | ||
|
|
fcb29ae484 | ||
|
|
5c4847ae2c | ||
|
|
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.
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
@@ -39,15 +34,15 @@ export class SearchController {
|
||||
@ApiUnauthorizedResponse({ description: "Invalid token" })
|
||||
async searchSong(
|
||||
@Request() req: any,
|
||||
@Param("query") query: string,
|
||||
@Query("q") query: string | null,
|
||||
@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[] | null> {
|
||||
): Promise<Song[]> {
|
||||
return await this.searchService.searchSong(
|
||||
query,
|
||||
query ?? "",
|
||||
artistId,
|
||||
genreId,
|
||||
mapInclude(include, req, SongController.includableFields),
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -67,6 +67,7 @@ services:
|
||||
- NGINX_PORT=4567
|
||||
ports:
|
||||
- "19006:19006"
|
||||
- "8081:8081"
|
||||
volumes:
|
||||
- ./front:/app
|
||||
depends_on:
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
{
|
||||
"12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true,
|
||||
"40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true
|
||||
}
|
||||
@@ -779,4 +779,8 @@ export default class API {
|
||||
public static getPartitionSvgUrl(songId: number): string {
|
||||
return `${API.baseUrl}/song/${songId}/assets/partition`;
|
||||
}
|
||||
|
||||
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,35 +33,80 @@ 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 = () =>
|
||||
({
|
||||
const tabRoutes = {
|
||||
Home: {
|
||||
component: DiscoveryView,
|
||||
options: { headerShown: false },
|
||||
options: { headerShown: false, tabBarIcon: Discover },
|
||||
link: '/',
|
||||
},
|
||||
User: {
|
||||
component: ProfileView,
|
||||
options: { headerShown: false, tabBarIcon: User },
|
||||
link: '/user',
|
||||
},
|
||||
Music: {
|
||||
component: MusicView,
|
||||
options: { headerShown: false },
|
||||
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 = {
|
||||
Tabs: {
|
||||
component: Tabs,
|
||||
options: { headerShown: false, path: '' },
|
||||
link: '',
|
||||
childRoutes: tabRoutes,
|
||||
},
|
||||
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') },
|
||||
@@ -72,31 +117,19 @@ const protectedRoutes = () =>
|
||||
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 publicRoutes = {
|
||||
Login: {
|
||||
component: SigninView,
|
||||
options: { title: translate('signInBtn'), headerShown: false },
|
||||
@@ -122,64 +155,79 @@ const publicRoutes = () =>
|
||||
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: (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>>();
|
||||
|
||||
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.
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