58 Commits

Author SHA1 Message Date
Clément Le Bihan
aae0bbea3a Added a loading dispaly for the play button and removed onProgressUpdate from expo av and reusing the timestamp to advance the cursor despite perf issues 2024-01-13 18:38:15 +01:00
Clément Le Bihan
f5136ae59b fixed issues with bad state reset and play pause gestion 2024-01-13 17:55:37 +01:00
Clément Le Bihan
f632ed42a3 Play works on web websocket not reimplemented 2024-01-13 17:31:56 +01:00
Clément Le Bihan
00ee5cd531 started to implement jotai states to control and manage the playview 2024-01-13 12:17:40 +01:00
Clément Le Bihan
1d496301d9 Put margin and fixed profile username display 2024-01-13 12:17:40 +01:00
Clément Le Bihan
d44e75a83a Added missing Int parses 2024-01-13 01:10:58 +01:00
e487d6d91e Fix total score increment 2024-01-13 01:10:58 +01:00
7a63a66da5 Rework music list 2024-01-13 01:10:58 +01:00
17f64cd849 Add isLiked bool on the front 2024-01-13 01:10:58 +01:00
ec17aa741f Allow search query to be empty 2024-01-13 01:10:58 +01:00
Clément Le Bihan
358841abd5 Fixed misplaced translation 2024-01-10 21:27:12 +01:00
Clément Le Bihan
64e7dbc71e removed rate modification of the song since I have nothing to rely on 2024-01-10 21:27:12 +01:00
Clément Le Bihan
5a0809c1d0 removed SoundPlayerSlice 2024-01-10 21:27:12 +01:00
Clément Le Bihan
4b5e3d2b04 Removed piano keys sounds 2024-01-10 21:27:12 +01:00
Clément Le Bihan
5f24c6e7bd using smplr player on web and mp3 on mobile 2024-01-10 21:27:12 +01:00
Clément Le Bihan
8bdf8ce334 wip 2024-01-10 21:27:12 +01:00
Clément Le Bihan
9012a6a9d8 new script to generate mp3s 2024-01-10 21:27:12 +01:00
Clément Le Bihan
c5fd4aa7d5 modified mp3 generation & new mp3 generated 2024-01-10 21:27:12 +01:00
Clément Le Bihan
65cd04a494 moving partition magic from being timestamp controlled to be controlled by the audio 2024-01-10 21:27:12 +01:00
Clément Le Bihan
c79ae7c6e8 fix message display and now using the API to get the correct melody url 2024-01-10 21:27:12 +01:00
Clément Le Bihan
ddc97f0923 added route to get elody file of the song 2024-01-10 21:27:12 +01:00
Clément Le Bihan
a9b902a427 Moved score animation into its own component 2024-01-10 21:27:12 +01:00
Clément Le Bihan
96d8e649c8 removed dupliacte popupCC in dom and now using two function less 2024-01-10 21:27:12 +01:00
Clément Le Bihan
22c93b7571 Experiment with full mp3 2024-01-10 21:27:12 +01:00
Clément Le Bihan
0644d4b580 Added melodies 2024-01-10 21:27:12 +01:00
Clément Le Bihan
ee6a76cdd9 experiments with expo av 2024-01-10 21:27:12 +01:00
Clément Le Bihan
5ba815590a ci fix 2024-01-09 17:34:35 +01:00
Clément Le Bihan
dd09827d08 minor fixes 2024-01-09 17:34:35 +01:00
b5b94adc83 Format code 2024-01-09 17:34:35 +01:00
3c04e8bb39 Fix types 2024-01-09 17:34:35 +01:00
17a4328af5 Better safe area handling everywhere 2024-01-09 17:34:35 +01:00
e81f2c1f75 Handle safe areas with tabs 2024-01-09 17:34:35 +01:00
f77874bec4 Fix desktop scafold background color 2024-01-09 17:34:35 +01:00
cfc72b8bc1 Fix margins with the desktop scaffold 2024-01-09 17:34:35 +01:00
359b20fc6d Add a screen wrapper in desktop mode 2024-01-09 17:34:35 +01:00
a3659618ea Add the desktop scaffold 2024-01-09 17:34:35 +01:00
fa60fc65a9 Steal the createBottomNavigaton from react-navigation 2024-01-09 17:34:35 +01:00
b1727b7838 Fix gradient in white mode 2024-01-09 17:34:35 +01:00
a3f4703dae Fix routes for logged out users 2024-01-09 17:34:35 +01:00
038918c212 Use a custom tabbar 2024-01-09 17:34:35 +01:00
42a947dfb0 Remove all scafoldcc instances 2024-01-09 17:34:35 +01:00
5525110d39 Add a bottom tab navigator 2024-01-09 17:34:35 +01:00
7160b77607 Fix .env.example 2024-01-09 17:34:35 +01:00
b5183f84b4 wip 2024-01-09 17:34:35 +01:00
mathysPaul
13050e52f9 [IMP] lint, prettier, tsc 2024-01-08 01:27:39 +01:00
mathysPaul
5ef3885f72 [FIX] MusicList, MusicItem, IconButton: Prevent double-add on consecutive likes
Fixes the issue where consecutive likes on a track mistakenly
added it twice to the liked list. Now ensures correct toggling
between like and unlike.
2024-01-08 01:27:39 +01:00
mathysPaul
a103666caf [REF] MusicView.tsx: Refactor & create MusicListCC 2024-01-08 01:27:39 +01:00
mathysPaul
29da5c2788 [ADD] Music view with Favorite, Last played and suggestion tabs 2024-01-08 01:27:39 +01:00
Arthur Jamet
1880b89b0c CI: Trigger Job if their source file has changes 2024-01-07 10:34:56 +01:00
Arthur Jamet
e769ff1f13 CI: Attempt to fix Action's trigger 2024-01-07 10:34:56 +01:00
Clément Le Bihan
9e7873cdd7 Merge pull request #352 from Chroma-Case/feat/adc/search-history-V2
Feat/adc/search history v2
2024-01-04 22:24:34 +01:00
Clément Le Bihan
f46c2cfb4a fix ci 2024-01-04 22:22:56 +01:00
Clément Le Bihan
9f14061efd Now using european date format 2024-01-04 22:21:11 +01:00
danis
851ee7420f fix(../V2/SearchHistory): fixed hard coded color + lightmode thing 2024-01-04 19:31:36 +01:00
danis
ef57eb752d fix(../V2/SearchHistory): fixed background width of history type prop 2024-01-04 18:41:44 +01:00
danis
fcb29ae484 Merge branch 'main' into feat/adc/search-history-V2 2024-01-02 20:52:57 +01:00
danis
5c4847ae2c style(searchBarV2): fixed coding style eslint error 2024-01-02 20:24:48 +01:00
danis
3353a17611 feat(search histo v2): created search history component + historyRow + fetching da things 2023-12-06 22:41:02 +01:00
163 changed files with 1564 additions and 1620 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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

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.

View File

@@ -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);
}
}

View File

@@ -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,

View File

@@ -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:

View File

@@ -122,7 +122,7 @@ export class UsersService {
return this.prisma.user.update({
where: { id: where },
data: {
partyPlayed: {
totalScore: {
increment: score,
},
},

View File

@@ -67,6 +67,7 @@ services:
- NGINX_PORT=4567
ports:
- "19006:19006"
- "8081:8081"
volumes:
- ./front:/app
depends_on:

View File

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

View File

@@ -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`;
}
}

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 {
NavigationProp,
ParamListBase,
useNavigation as navigationHook,
} from '@react-navigation/native';
import React, { useEffect, useMemo } from 'react';
NativeStackNavigationProp,
NativeStackScreenProps,
createNativeStackNavigator,
} from '@react-navigation/native-stack';
import { ParamListBase, useNavigation as navigationHook } from '@react-navigation/native';
import React, { ComponentProps, ComponentType, useEffect, useMemo } from 'react';
import { DarkTheme, DefaultTheme, NavigationContainer } from '@react-navigation/native';
import { RootState, useSelector } from './state/Store';
import { useDispatch } from 'react-redux';
@@ -33,153 +33,201 @@ import ForgotPasswordView from './views/ForgotPasswordView';
import DiscoveryView from './views/V2/DiscoveryView';
import MusicView from './views/MusicView';
import Leaderboardiew from './views/LeaderboardView';
import { LinearGradient } from 'expo-linear-gradient';
import { createCustomNavigator } from './utils/navigator';
import { Cup, Discover, Music, SearchNormal1, Setting2, User } from 'iconsax-react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const Stack = createNativeStackNavigator<AppRouteParams>();
const Tab = createCustomNavigator<AppRouteParams>();
const Tabs = () => {
return (
<Tab.Navigator>
{Object.entries(tabRoutes).map(([name, route], routeIndex) => (
<Tab.Screen
key={'route-' + routeIndex}
name={name as keyof AppRouteParams}
options={{ ...route.options, headerTransparent: true }}
component={route.component}
/>
))}
</Tab.Navigator>
);
};
// Util function to hide route props in URL
const removeMe = () => '';
const protectedRoutes = () =>
({
Home: {
component: DiscoveryView,
options: { headerShown: false },
link: '/',
const tabRoutes = {
Home: {
component: DiscoveryView,
options: { headerShown: false, tabBarIcon: Discover },
link: '/',
},
User: {
component: ProfileView,
options: { headerShown: false, tabBarIcon: User },
link: '/user',
},
Music: {
component: MusicView,
options: { headerShown: false, tabBarIcon: Music },
link: '/music',
},
Search: {
component: SearchView,
options: { headerShown: false, tabBarIcon: SearchNormal1 },
link: '/search/:query?',
},
Leaderboard: {
component: Leaderboardiew,
options: { title: translate('leaderboardTitle'), headerShown: false, tabBarIcon: Cup },
link: '/leaderboard',
},
Settings: {
component: SettingsTab,
options: { headerShown: false, tabBarIcon: Setting2, subMenu: true },
link: '/settings/:screen?',
stringify: {
screen: removeMe,
},
Music: {
component: MusicView,
options: { headerShown: false },
link: '/music',
},
Play: {
component: PlayView,
options: { headerShown: false, title: translate('play') },
link: '/play/:songId',
},
Settings: {
component: SettingsTab,
options: { headerShown: false },
link: '/settings/:screen?',
stringify: {
screen: removeMe,
},
},
Artist: {
component: ArtistDetailsView,
options: { title: translate('artistFilter') },
link: '/artist/:artistId',
},
Genre: {
component: GenreDetailsView,
options: { title: translate('genreFilter') },
link: '/genre/:genreId',
},
Search: {
component: SearchView,
options: { headerShown: false },
link: '/search/:query?',
},
Leaderboard: {
component: Leaderboardiew,
options: { title: translate('leaderboardTitle'), headerShown: false },
link: '/leaderboard',
},
Error: {
component: ErrorView,
options: { title: translate('error'), headerLeft: null },
link: undefined,
},
User: { component: ProfileView, options: { headerShown: false }, link: '/user' },
Verified: {
component: VerifiedView,
options: { title: 'Verify email', headerShown: false },
link: '/verify',
},
}) as const;
},
};
const publicRoutes = () =>
({
Login: {
component: SigninView,
options: { title: translate('signInBtn'), headerShown: false },
link: '/login',
},
Signup: {
component: SignupView,
options: { title: translate('signUpBtn'), headerShown: false },
link: '/signup',
},
Google: {
component: GoogleView,
options: { title: 'Google signin', headerShown: false },
link: '/logged/google',
},
PasswordReset: {
component: PasswordResetView,
options: { title: 'Password reset form', headerShown: false },
link: '/password_reset',
},
ForgotPassword: {
component: ForgotPasswordView,
options: { title: 'Password reset form', headerShown: false },
link: '/forgot_password',
},
}) as const;
const protectedRoutes = {
Tabs: {
component: Tabs,
options: { headerShown: false, path: '' },
link: '',
childRoutes: tabRoutes,
},
Play: {
component: PlayView,
options: { headerShown: false, title: translate('play') },
link: '/play/:songId',
},
Artist: {
component: ArtistDetailsView,
options: { title: translate('artistFilter') },
link: '/artist/:artistId',
},
Genre: {
component: GenreDetailsView,
options: { title: translate('genreFilter') },
link: '/genre/:genreId',
},
Error: {
component: ErrorView,
options: { title: translate('error'), headerLeft: null },
link: undefined,
},
Verified: {
component: VerifiedView,
options: { title: 'Verify email', headerShown: false },
link: '/verify',
},
};
const publicRoutes = {
Login: {
component: SigninView,
options: { title: translate('signInBtn'), headerShown: false },
link: '/login',
},
Signup: {
component: SignupView,
options: { title: translate('signUpBtn'), headerShown: false },
link: '/signup',
},
Google: {
component: GoogleView,
options: { title: 'Google signin', headerShown: false },
link: '/logged/google',
},
PasswordReset: {
component: PasswordResetView,
options: { title: 'Password reset form', headerShown: false },
link: '/password_reset',
},
ForgotPassword: {
component: ForgotPasswordView,
options: { title: 'Password reset form', headerShown: false },
link: '/forgot_password',
},
};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Route<Props = any> = {
component: (arg: RouteProps<Props>) => JSX.Element | (() => JSX.Element);
component: ComponentType<Props>;
options: object;
link?: string;
};
type OmitOrUndefined<T, K extends string> = T extends undefined ? T : Omit<T, K>;
// if the component has no props, ComponentProps return unknown so we remove those
type RemoveNonObjects<T> = [T] extends [{}] ? T : undefined;
type RouteParams<Routes extends Record<string, Route>> = {
[RouteName in keyof Routes]: OmitOrUndefined<
Parameters<Routes[RouteName]['component']>[0],
keyof NativeStackScreenProps<{}>
>;
[RouteName in keyof Routes]: RemoveNonObjects<ComponentProps<Routes[RouteName]['component']>>;
};
type PrivateRoutesParams = RouteParams<ReturnType<typeof protectedRoutes>>;
type PublicRoutesParams = RouteParams<ReturnType<typeof publicRoutes>>;
type AppRouteParams = PrivateRoutesParams & PublicRoutesParams;
type PrivateRoutesParams = RouteParams<typeof protectedRoutes>;
type PublicRoutesParams = RouteParams<typeof publicRoutes>;
type TabsRoutesParams = RouteParams<typeof tabRoutes>;
type AppRouteParams = Omit<PrivateRoutesParams, 'Tabs'> & {
Tabs: { screen: keyof TabsRoutesParams };
} & PublicRoutesParams &
TabsRoutesParams & { Oops: undefined };
const Stack = createNativeStackNavigator<AppRouteParams & { Loading: never; Oops: never }>();
const RouteToScreen = <T extends {}>(Component: Route<T>['component']) =>
function Route(props: NativeStackScreenProps<T & ParamListBase>) {
const colorScheme = useColorScheme();
const insets = useSafeAreaInsets();
const RouteToScreen =
<T extends {}>(component: Route<T>['component']) =>
// eslint-disable-next-line react/display-name
(props: NativeStackScreenProps<T & ParamListBase>) => (
<>
{component({ ...props.route.params, route: props.route } as Parameters<
Route<T>['component']
>[0])}
</>
);
return (
<LinearGradient
colors={colorScheme === 'dark' ? ['#101014', '#6075F9'] : ['#cdd4fd', '#cdd4fd']}
style={{
flex: 1,
paddingTop: insets.top,
paddingBottom: insets.bottom,
paddingLeft: insets.left,
paddingRight: insets.right,
}}
>
<Component {...(props.route.params as T)} route={props.route} />
</LinearGradient>
);
};
const routesToScreens = (routes: Partial<Record<keyof AppRouteParams, Route>>) =>
Object.entries(routes).map(([name, route], routeIndex) => (
<Stack.Screen
key={'route-' + routeIndex}
name={name as keyof AppRouteParams}
options={route.options}
options={{ ...route.options, headerTransparent: true }}
component={RouteToScreen(route.component)}
/>
));
const routesToLinkingConfig = (
routes: Partial<
Record<keyof AppRouteParams, { link?: string; stringify?: Record<string, () => string> }>
>
) => {
type RouteDescription = Record<
string,
{ link?: string; stringify?: Record<string, () => string>; childRoutes?: RouteDescription }
>;
const routesToLinkingConfig = (routes: RouteDescription) => {
// Too lazy to (find the) type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pagesToRoute = {} as Record<keyof AppRouteParams, any>;
Object.keys(routes).forEach((route) => {
const index = route as keyof AppRouteParams;
if (routes[index]?.link) {
if (routes[index]?.link !== undefined) {
pagesToRoute[index] = {
path: routes[index]!.link!,
stringify: routes[index]!.stringify,
screens: routes[index]!.childRoutes
? routesToLinkingConfig(routes[index]!.childRoutes!).config.screens
: undefined,
};
}
});
@@ -239,12 +287,6 @@ export const Router = () => {
}
return 'noAuth';
}, [userProfile, accessToken]);
const routes = useMemo(() => {
if (authStatus == 'authed') {
return protectedRoutes();
}
return publicRoutes();
}, [authStatus]);
useEffect(() => {
if (accessToken) {
@@ -257,13 +299,14 @@ export const Router = () => {
return <LoadingView />;
}
const routes = authStatus == 'authed' ? { ...protectedRoutes } : publicRoutes;
return (
<NavigationContainer
linking={routesToLinkingConfig(routes)}
fallback={<LoadingView />}
theme={colorScheme == 'light' ? DefaultTheme : DarkTheme}
>
<Stack.Navigator>
<Stack.Navigator screenOptions={{ navigationBarColor: 'transparent' }}>
{authStatus == 'error' ? (
<>
<Stack.Screen
@@ -272,7 +315,7 @@ export const Router = () => {
<ProfileErrorView onTryAgain={() => userProfile.refetch()} />
))}
/>
{routesToScreens(publicRoutes())}
{routesToScreens(publicRoutes)}
</>
) : (
routesToScreens(routes)
@@ -282,6 +325,4 @@ export const Router = () => {
);
};
export type RouteProps<T> = T & Pick<NativeStackScreenProps<T & ParamListBase>, 'route'>;
export const useNavigation = () => navigationHook<NavigationProp<AppRouteParams>>();
export const useNavigation = () => navigationHook<NativeStackNavigationProp<AppRouteParams>>();

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